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.
- 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 {
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:
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
| 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