Prose
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.
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:
App Config
const repoBase = "https://github.com/BayBreezy/docd";
export default defineAppConfig({
docd: {
github: {
repo: repoBase,
branch: "main",
contentDir: "docs/content",
},
ui: {
borderType: "dashed",
header: {
title: "Docd",
logo: {
alt: "Docd Logo",
light: "/logos/docd-logo-dark.svg",
dark: "/logos/docd-logo-light.svg",
favicon: "/favicon.svg",
},
},
extraLinks: [
{ icon: "lucide:star", label: "Star on GitHub", external: true, href: repoBase },
{
icon: "lucide:bug",
label: "Report an issue",
external: true,
href: `${repoBase}/issues/new`,
},
],
transition: {
name: "fade",
},
},
},
});
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:
Browser Frame Component
<script setup lang="ts">
defineProps<{
/**
* URL displayed in the address bar.
* @example "https://example.com"
*/
url?: string;
}>();
const emit = defineEmits<{
/**
* Emitted when the close (red) traffic light button is clicked.
*/
close: [];
/**
* Emitted when the minimize (yellow) traffic light button is clicked.
*/
minimize: [];
/**
* Emitted when the maximize (green) traffic light button is clicked.
*/
maximize: [];
/**
* Emitted when the sidebar/panel toggle button is clicked.
*/
panel: [];
/**
* Emitted when the back navigation button is clicked.
*/
back: [];
/**
* Emitted when the forward navigation button is clicked.
*/
forward: [];
/**
* Emitted when the page refresh button is clicked.
*/
refresh: [];
/**
* Emitted when the download button is clicked.
*/
download: [];
/**
* Emitted when the share button is clicked.
*/
share: [];
/**
* Emitted when the new tab button is clicked.
*/
newTab: [];
/**
* Emitted when the copy button is clicked.
*/
copy: [];
}>();
defineSlots<{
/**
* Replaces the three traffic light buttons (close, minimize, maximize).
*/
"traffic-lights": () => void;
/**
* Replaces the sidebar panel toggle and back/forward navigation buttons.
*/
"nav-controls": () => void;
/**
* Replaces the address bar area.
*/
"address-bar": () => void;
/**
* Replaces the right-side action buttons (download, share, new tab, copy).
*/
actions: () => void;
/**
* Content rendered inside the browser frame body.
*/
default: () => void;
}>();
</script>
<template>
<div class="w-full overflow-hidden rounded-xl border shadow-md">
<div class="flex items-center gap-3 border-b px-3 py-2">
<!-- Traffic lights -->
<div class="flex shrink-0 items-center gap-1.5">
<slot name="traffic-lights">
<button
type="button"
aria-label="Close"
class="size-3 rounded-full bg-red-500 transition-opacity hover:opacity-80"
@click="emit('close')"
/>
<button
type="button"
aria-label="Minimize"
class="size-3 rounded-full bg-yellow-400 transition-opacity hover:opacity-80"
@click="emit('minimize')"
/>
<button
type="button"
aria-label="Maximize"
class="size-3 rounded-full bg-green-500 transition-opacity hover:opacity-80"
@click="emit('maximize')"
/>
</slot>
</div>
<!-- Panel + back/forward -->
<div class="flex shrink-0 items-center gap-0.5">
<slot name="nav-controls">
<button
type="button"
aria-label="Toggle sidebar"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('panel')"
>
<Icon name="lucide:panel-left" class="size-4" />
</button>
<button
type="button"
aria-label="Go back"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('back')"
>
<Icon name="lucide:chevron-left" class="size-4" />
</button>
<button
type="button"
aria-label="Go forward"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('forward')"
>
<Icon name="lucide:chevron-right" class="size-4" />
</button>
</slot>
</div>
<!-- Address bar -->
<div class="flex min-w-0 flex-1 justify-center">
<slot name="address-bar">
<div
class="flex w-full max-w-sm items-center gap-1.5 rounded-md border bg-background px-2.5 py-1 text-xs text-muted-foreground dark:border-muted/50 dark:bg-muted/50"
>
<Icon name="lucide:shield" class="size-3 shrink-0" />
<span class="min-w-0 flex-1 truncate text-center">{{ url ?? "about:blank" }}</span>
<button
type="button"
aria-label="Refresh"
class="shrink-0 transition-opacity hover:opacity-70"
@click="emit('refresh')"
>
<Icon name="lucide:rotate-cw" class="size-3" />
</button>
</div>
</slot>
</div>
<!-- Right-side actions -->
<div class="flex shrink-0 items-center gap-0.5">
<slot name="actions">
<button
type="button"
aria-label="Download"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('download')"
>
<Icon name="lucide:arrow-down-to-line" class="size-4" />
</button>
<button
type="button"
aria-label="Share"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('share')"
>
<Icon name="lucide:share" class="size-4" />
</button>
<button
type="button"
aria-label="New tab"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('newTab')"
>
<Icon name="lucide:plus" class="size-4" />
</button>
<button
type="button"
aria-label="Copy"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('copy')"
>
<Icon name="lucide:copy" class="size-4" />
</button>
</slot>
</div>
</div>
<div>
<slot mdc-unwrap="p" />
</div>
</div>
</template>
Custom Icon
Change the toggle icon:
Browser Frame Component
<script setup lang="ts">
defineProps<{
/**
* URL displayed in the address bar.
* @example "https://example.com"
*/
url?: string;
}>();
const emit = defineEmits<{
/**
* Emitted when the close (red) traffic light button is clicked.
*/
close: [];
/**
* Emitted when the minimize (yellow) traffic light button is clicked.
*/
minimize: [];
/**
* Emitted when the maximize (green) traffic light button is clicked.
*/
maximize: [];
/**
* Emitted when the sidebar/panel toggle button is clicked.
*/
panel: [];
/**
* Emitted when the back navigation button is clicked.
*/
back: [];
/**
* Emitted when the forward navigation button is clicked.
*/
forward: [];
/**
* Emitted when the page refresh button is clicked.
*/
refresh: [];
/**
* Emitted when the download button is clicked.
*/
download: [];
/**
* Emitted when the share button is clicked.
*/
share: [];
/**
* Emitted when the new tab button is clicked.
*/
newTab: [];
/**
* Emitted when the copy button is clicked.
*/
copy: [];
}>();
defineSlots<{
/**
* Replaces the three traffic light buttons (close, minimize, maximize).
*/
"traffic-lights": () => void;
/**
* Replaces the sidebar panel toggle and back/forward navigation buttons.
*/
"nav-controls": () => void;
/**
* Replaces the address bar area.
*/
"address-bar": () => void;
/**
* Replaces the right-side action buttons (download, share, new tab, copy).
*/
actions: () => void;
/**
* Content rendered inside the browser frame body.
*/
default: () => void;
}>();
</script>
<template>
<div class="w-full overflow-hidden rounded-xl border shadow-md">
<div class="flex items-center gap-3 border-b px-3 py-2">
<!-- Traffic lights -->
<div class="flex shrink-0 items-center gap-1.5">
<slot name="traffic-lights">
<button
type="button"
aria-label="Close"
class="size-3 rounded-full bg-red-500 transition-opacity hover:opacity-80"
@click="emit('close')"
/>
<button
type="button"
aria-label="Minimize"
class="size-3 rounded-full bg-yellow-400 transition-opacity hover:opacity-80"
@click="emit('minimize')"
/>
<button
type="button"
aria-label="Maximize"
class="size-3 rounded-full bg-green-500 transition-opacity hover:opacity-80"
@click="emit('maximize')"
/>
</slot>
</div>
<!-- Panel + back/forward -->
<div class="flex shrink-0 items-center gap-0.5">
<slot name="nav-controls">
<button
type="button"
aria-label="Toggle sidebar"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('panel')"
>
<Icon name="lucide:panel-left" class="size-4" />
</button>
<button
type="button"
aria-label="Go back"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('back')"
>
<Icon name="lucide:chevron-left" class="size-4" />
</button>
<button
type="button"
aria-label="Go forward"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('forward')"
>
<Icon name="lucide:chevron-right" class="size-4" />
</button>
</slot>
</div>
<!-- Address bar -->
<div class="flex min-w-0 flex-1 justify-center">
<slot name="address-bar">
<div
class="flex w-full max-w-sm items-center gap-1.5 rounded-md border bg-background px-2.5 py-1 text-xs text-muted-foreground dark:border-muted/50 dark:bg-muted/50"
>
<Icon name="lucide:shield" class="size-3 shrink-0" />
<span class="min-w-0 flex-1 truncate text-center">{{ url ?? "about:blank" }}</span>
<button
type="button"
aria-label="Refresh"
class="shrink-0 transition-opacity hover:opacity-70"
@click="emit('refresh')"
>
<Icon name="lucide:rotate-cw" class="size-3" />
</button>
</div>
</slot>
</div>
<!-- Right-side actions -->
<div class="flex shrink-0 items-center gap-0.5">
<slot name="actions">
<button
type="button"
aria-label="Download"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('download')"
>
<Icon name="lucide:arrow-down-to-line" class="size-4" />
</button>
<button
type="button"
aria-label="Share"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('share')"
>
<Icon name="lucide:share" class="size-4" />
</button>
<button
type="button"
aria-label="New tab"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('newTab')"
>
<Icon name="lucide:plus" class="size-4" />
</button>
<button
type="button"
aria-label="Copy"
class="rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
@click="emit('copy')"
>
<Icon name="lucide:copy" class="size-4" />
</button>
</slot>
</div>
</div>
<div>
<slot mdc-unwrap="p" />
</div>
</div>
</template>
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")
Prose Code Collapse API
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
icon | string | "lucide:chevron-down" | No | The icon displayed to toggle the code collapse state |
name | string | "Code" | No | The name/label displayed in the trigger button |
openText | string | "Expand" | No | The text displayed when the code is collapsed (clickable to expand) |
closeText | string | "Collapse" | No | The text displayed when the code is expanded (clickable to collapse) |
modelValue | boolean | false | No | Toggle state for the collapse/expand functionality |