Code Collapse
A collapsible code block wrapper, perfect for showing long code examples without overwhelming your documentation.
Overview
The ProseCodeCollapse component wraps code blocks and provides a toggle button to expand or collapse content. It's ideal for lengthy code examples that would take up too much vertical space, starting at a fixed height with a gradient fade effect.
- Clean Toggle - Simple expand/collapse functionality
- Gradient Fade - Visual indicator when content is collapsed
- Customizable Labels - Configure button text and labels
- Accessible - Proper ARIA attributes for screen readers
- Smart Defaults - Works great out of the box with sensible defaults
Basic Usage
Wrap any code block to make it collapsible:
Mermaid Plugin
import mermaid from "mermaid";
import type { MermaidConfig } from "mermaid";
export default defineNuxtPlugin(() => {
  /**
   * Mermaid initialization configuration
   */
  const mermaidInitConfig = {
    startOnLoad: false,
    themeVariables: {
      fontFamily: "var(--font-sans)",
      fontSize: "13px",
    },
    flowchart: {
      curve: "basis",
      useMaxWidth: true,
    },
    sequence: {
      actorMargin: 50,
      showSequenceNumbers: false,
    },
    suppressErrorRendering: true,
  } as MermaidConfig;
  /**
   * Initialize Mermaid with the specified configuration
   */
  mermaid.initialize(mermaidInitConfig);
  return {
    provide: {
      mermaidInstance: mermaid,
      mermaidInitConfig,
    },
  };
});
Custom Labels
Customize the button text and name:
<script setup lang="ts">
  import { computed, ref } from "vue";
  import type { User } from "@/types";
  const users = ref<User[]>([]);
  const loading = ref(false);
  const error = ref<string | null>(null);
  const activeUsers = computed(() => users.value.filter((u) => u.role !== "guest"));
  const totalUsers = computed(() => users.value.length);
  async function fetchUsers() {
    loading.value = true;
    error.value = null;
    try {
      const response = await fetch("/api/users");
      if (!response.ok) throw new Error("Failed to fetch");
      users.value = await response.json();
    } catch (e) {
      error.value = e instanceof Error ? e.message : "Unknown error";
    } finally {
      loading.value = false;
    }
  }
  async function deleteUser(id: string) {
    try {
      await fetch(`/api/users/${id}`, { method: "DELETE" });
      users.value = users.value.filter((u) => u.id !== id);
    } catch (e) {
      console.error("Delete failed:", e);
    }
  }
  onMounted(() => {
    fetchUsers();
  });
</script>
<template>
  <div class="user-manager">
    <h2>Users ({{ totalUsers }})</h2>
    <div v-if="loading">Loading...</div>
    <div v-else-if="error" class="error">{{ error }}</div>
    <div v-else class="user-grid">
      <UserCard v-for="user in activeUsers" :key="user.id" :user="user" @delete="deleteUser" />
    </div>
  </div>
</template>
With Code Snippet
Combine with ::prose-code-snippet to show actual source files:
Use Form Field Composable
import { FORM_ITEM_INJECTION_KEY } from "@/components/Ui/Form/Item.vue";
import {
  FieldContextKey,
  useFieldError,
  useIsFieldDirty,
  useIsFieldTouched,
  useIsFieldValid,
} from "vee-validate";
import { inject } from "vue";
export function useFormField() {
  const fieldContext = inject(FieldContextKey);
  const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY);
  const fieldState = {
    valid: useIsFieldValid(),
    isDirty: useIsFieldDirty(),
    isTouched: useIsFieldTouched(),
    error: useFieldError(),
  };
  if (!fieldContext) throw new Error("useFormField should be used within <FormField>");
  const { name } = fieldContext;
  const id = fieldItemContext;
  return {
    id,
    name,
    formItemId: `${id}-form-item`,
    formDescriptionId: `${id}-form-item-description`,
    formMessageId: `${id}-form-item-message`,
    ...fieldState,
  };
}
Custom Icon
Change the toggle icon:
Chip Component
<template>
  <div data-slot="chip" class="relative inline-flex shrink-0 items-center justify-center">
    <slot />
    <span
      v-if="localModel"
      :class="[styles({ position, size, inset, class: [props.color, props.class] })]"
    >
      <slot name="content">
        {{ text }}
      </slot>
    </span>
  </div>
</template>
<script lang="ts" setup>
  import type { HTMLAttributes } from "vue";
  defineOptions({ inheritAttrs: false });
  const props = withDefaults(
    defineProps<{
      /**
       * The text to display in the chip.
       *
       * Can be overridden by the `content` slot.
       */
      text?: string;
      /**
       * The color of the chip.
       *
       * @default `bg-primary`
       */
      color?: string;
      /**
       * The size of the chip.
       *
       * @default `sm`
       */
      size?: VariantProps<typeof styles>["size"];
      /**
       * The position of the chip.
       *
       * @default `top-right`
       */
      position?: VariantProps<typeof styles>["position"];
      /**
       * Whether the chip should be inset.
       *
       * @default `false`
       */
      inset?: boolean;
      /**
       * Whether the chip should be visible.
       *
       * Can be used with `v-model` to control visibility.
       *
       * @default `true`
       */
      show?: boolean;
      /**
       * Additional classes to apply to the chip.
       */
      class?: HTMLAttributes["class"];
    }>(),
    { show: true, color: "bg-primary", inset: false }
  );
  const localModel = defineModel<boolean>("show", { default: true });
  const styles = tv({
    base: "absolute flex items-center justify-center rounded-full font-medium whitespace-nowrap text-foreground ring-[2px] ring-background",
    variants: {
      position: {
        "top-right": "top-0 right-0",
        "bottom-right": "right-0 bottom-0",
        "top-left": "top-0 left-0",
        "bottom-left": "bottom-0 left-0",
      },
      inset: {
        true: "",
        false: "",
      },
      size: {
        "3xs": "h-[4px] min-w-[4px] p-px text-[4px]",
        "2xs": "h-[5px] min-w-[5px] p-px text-[5px]",
        xs: "h-1.5 min-w-[0.375rem] p-px text-[6px]",
        sm: "h-2 min-w-[0.5rem] p-0.5 text-[7px]",
        md: "h-2.5 min-w-2.5 p-0.5 text-[8px]",
        lg: "h-3 min-w-[0.75rem] p-0.5 text-[10px]",
        xl: "h-3.5 min-w-[0.875rem] p-1 text-[11px]",
        "2xl": "h-4 min-w-[1rem] p-1 text-[12px]",
        "3xl": "h-5 min-w-[1.25rem] p-1 text-[14px]",
      },
    },
    defaultVariants: {
      size: "sm",
      position: "top-right",
      inset: false,
    },
    compoundVariants: [
      {
        inset: false,
        position: "top-right",
        class: "translate-x-1/2 -translate-y-1/2 transform",
      },
      {
        inset: false,
        position: "bottom-right",
        class: "-translate-x-1/2 translate-y-1/2 transform",
      },
      {
        inset: false,
        position: "top-left",
        class: "-translate-x-1/2 -translate-y-1/2 transform",
      },
      {
        inset: false,
        position: "bottom-left",
        class: "-translate-x-1/2 translate-y-1/2 transform",
      },
    ],
  });
</script>
Props
| Prop | Type | Default | Description | 
|---|---|---|---|
| icon | string | "lucide:chevron-down" | Icon displayed on the toggle button | 
| name | string | "Code" | Name/label shown in the button text | 
| openText | string | "Expand" | Text shown when code is collapsed (clickable to expand) | 
| closeText | string | "Collapse" | Text shown when code is expanded (clickable to collapse) | 
| class | string | - | Additional CSS classes for the root container | 
Behavior
Initial State
- Code starts collapsed at 200px height
- Gradient fade overlay indicates more content below
- Button shows: "{openText} {name}" (e.g., "Expand Code")
Expanded State
- Code expands to full height (max 80vh)
- Gradient fade removed
- Button shows: "{closeText} {name}" (e.g., "Collapse Code")
Accessibility
The component includes proper accessibility features:
- ARIA Attributes: aria-expandedindicates current state
- ARIA Labels: aria-labelprovides context for screen readers
- Keyboard Support: Button is focusable and clickable via keyboard
- Icon Decoration: Chevron icon is marked aria-hidden="true"
- Semantic HTML: Uses proper button element for interaction
Use Cases
- Long Examples - Code snippets over ~30 lines
- Complete Files - Full component implementations
- Optional Details - Advanced examples users can expand if needed
- Tutorial Code - Progressive disclosure of complex solutions
- API References - Full endpoint implementations
Best Practices
- Use Descriptive Names - Make it clear what's being collapsed::prose-code-collapse{name="Full Implementation"}
- Customize Button Text - Match your documentation tone::prose-code-collapse{openText="Show" closeText="Hide"}
- Combine with Snippets - Keep docs in sync with source::prose-code-collapse ::prose-code-snippet{file="/path/to/file.vue"} :: ::
- Use Sparingly - Don't collapse everything; save for truly long code
- Consider Context - Some readers want full code upfront, others prefer collapsed
Related Components
ProseCodeSnippet - Import code from files or URL
ProseCodeGroup - Tabbed code snippets
ProseCodeTree - Interactive file tree with code preview