Code Collapse

A collapsible code block wrapper, perfect for showing long code examples without overwhelming your documentation.

Source code

Click to see the source code for this component on GitHub. Feel free to copy it and adjust it for your own use.

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.

Features
  • 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:

ts

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:

ts

Use Form Field Composable

import {
  FieldContextKey,
  useFieldError,
  useIsFieldDirty,
  useIsFieldTouched,
  useIsFieldValid,
} from "vee-validate";
import { inject } from "vue";

import { FORM_ITEM_INJECTION_KEY } from "@/components/Ui/Form/Item.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:

vue

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: normalizeClass([props.color, props.class]) || undefined,
        }),
      ]"
    >
      <slot name="content">
        {{ text }}
      </slot>
    </span>
  </div>
</template>

<script lang="ts" setup>
  import { normalizeClass } from "vue";
  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-2 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

PropTypeDefaultDescription
iconstring"lucide:chevron-down"Icon displayed on the toggle button
namestring"Code"Name/label shown in the button text
openTextstring"Expand"Text shown when code is collapsed (clickable to expand)
closeTextstring"Collapse"Text shown when code is expanded (clickable to collapse)
classstring-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-expanded indicates current state
  • ARIA Labels: aria-label provides 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

When to Use Code Collapse
  • 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

  1. Use Descriptive Names - Make it clear what's being collapsed
    ::prose-code-collapse{name="Full Implementation"}
    
  2. Customize Button Text - Match your documentation tone
    ::prose-code-collapse{openText="Show" closeText="Hide"}
    
  3. Combine with Snippets - Keep docs in sync with source
    ::prose-code-collapse
    ::prose-code-snippet{file="/path/to/file.vue"}
    ::
    ::
    
  4. Use Sparingly - Don't collapse everything; save for truly long code
  5. Consider Context - Some readers want full code upfront, others prefer collapsed

ProseCodeSnippet - Import code from files or URL

ProseCodeGroup - Tabbed code snippets

ProseCodeTree - Interactive file tree with code preview