Use when building documentation sites with live code previews, component library docs, API reference pages, or interactive code examples using Next.js App Router and shadcn/ui.
Build beautiful, interactive documentation sites with live component previews and copy-paste code examples.
This skill implements a documentation system featuring:
Use this skill when:
Do NOT use when:
Prerequisites:
app/ directory)npx shadcn@latest init)src/app/docs/
āāā layout.tsx # Root docs layout
āāā page.tsx # Introduction page
āāā _components/
ā āāā docs.tsx # Core UI components
ā āāā docs-sidebar.tsx # Navigation sidebar
ā āāā docs-toc.tsx # Table of contents
ā āāā component-preview.tsx # Server-side preview wrapper
ā āāā component-preview-client.tsx # Client-side preview tabs
ā āāā code-block.tsx # Code display
ā āāā copy-button.tsx # Clipboard utility
ā āāā examples/ # Live example components
āāā [route]/page.tsx # Documentation pages
npm install shiki
npx shadcn@latest add sidebar table card tabs
See references/COMPONENTS.md for complete component implementations.
Every documentation page follows this structure:
import { Metadata } from "next";
import { DocsLayout, DocsSection, DocsPropTable } from "../_components/docs";
import { ComponentPreview } from "../_components/component-preview";
import { getExampleSource } from "@/lib/get-example-source";
import { MyExample } from "../_components/examples/my-example";
export const metadata: Metadata = { title: "Page Title" };
export default function PageName() {
const exampleSource = getExampleSource("my-example.tsx");
return (
<DocsLayout
title="Page Title"
description="Brief description of this page"
prev={{ title: "Previous", href: "/docs/previous" }}
next={{ title: "Next", href: "/docs/next" }}
toc={[
{ title: "Overview", slug: "overview" },
{ title: "Usage", slug: "usage" },
{ title: "API Reference", slug: "api-reference" },
]}
>
<DocsSection>
<p>Introduction paragraph.</p>
</DocsSection>
<DocsSection title="Overview">
<p>Section content here.</p>
</DocsSection>
<DocsSection title="Usage">
<ComponentPreview code={exampleSource}>
<MyExample />
</ComponentPreview>
</DocsSection>
<DocsSection title="API Reference">
<DocsPropTable
props={[
{
name: "propName",
type: "string",
default: "undefined",
description: "Description of the prop",
},
]}
/>
</DocsSection>
</DocsLayout>
);
}
Create example components in _components/examples/:
// Client-side example (with state)
"use client";
import { useState } from "react";
import { MyComponent } from "@/registry/my-component";
export function InteractiveExample() {
const [value, setValue] = useState("initial");
return (
<div className="h-[400px] w-full">
<MyComponent value={value} onChange={setValue} />
</div>
);
}
// Server-side example (no state)
import { MyComponent } from "@/registry/my-component";
export function SimpleExample() {
return (
<div className="h-[400px] w-full">
<MyComponent />
</div>
);
}
Key pattern: Always wrap examples in a fixed-height container (h-[400px]) for consistent preview rendering.
Define navigation in docs-navigation.ts:
import { BookOpen, Code, Settings } from "lucide-react";
export const docsNavigation = {
groups: [
{
title: "Getting Started",
items: [
{ title: "Introduction", href: "/docs", icon: BookOpen },
{ title: "Installation", href: "/docs/installation", icon: Code },
],
},
{
title: "Components",
items: [
{ title: "Button", href: "/docs/button", icon: Settings },
// Add more components...
],
},
],
};
Create lib/highlight.ts:
import { codeToHtml } from "shiki";
export async function highlightCode(code: string, lang = "tsx") {
return codeToHtml(code, {
lang,
themes: {
light: "github-light",
dark: "github-dark",
},
});
}
Create lib/get-example-source.ts:
import fs from "fs";
import path from "path";
export function getExampleSource(filename: string): string {
const filePath = path.join(
process.cwd(),
"src/app/docs/_components/examples",
filename
);
let content = fs.readFileSync(filePath, "utf-8");
// Transform import paths for user copy-paste
content = content.replace(
/@\/registry\//g,
"@/components/ui/"
);
return content;
}
The docs system uses these core components:
| Component | Purpose |
|---|---|
DocsLayout |
Page wrapper with prev/next nav and TOC |
DocsSection |
Content section with auto-generated slug IDs |
DocsHeader |
Page title and description |
DocsNote |
Highlighted callout boxes |
DocsCode |
Inline code styling |
DocsLink |
Styled links with external support |
DocsPropTable |
API reference tables |
ComponentPreview |
Live preview with code tab |
CodeBlock |
Standalone code display |
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ComponentPreview (Server Component) ā
ā - Receives code string ā
ā - Calls highlightCode() with Shiki ā
ā - Passes highlighted HTML to client ā
āāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
ā¼
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ComponentPreviewClient (Client Component) ā
ā - Renders tabs: Preview | Code ā
ā - Preview tab: renders children ā
ā - Code tab: shows highlighted code ā
ā - Copy button for code ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
src/app/docs/[page-name]/page.tsx_components/examples/[name]-example.tsxdocs-navigation.tsh-[400px] for consistent previews| Mistake | Fix |
|---|---|
Calling highlightCode in Client Component |
Move to Server Component - Shiki requires server-side execution |
| Missing fixed height on examples | Add h-[400px] wrapper for consistent preview rendering |
| Using internal import paths in examples | Use getExampleSource() to transform @/registry/ to @/components/ui/ |
| Forgetting to update navigation | Always add new pages to docs-navigation.ts |
| Not updating prev/next links | Check adjacent pages when adding/removing docs |
| Problem | Solution |
|---|---|
| shadcn/ui not installed | Run npx shadcn@latest init first, then add components |
| Shiki SSR errors | Ensure highlightCode is only called in Server Components |
| Dark mode not working | Add defaultColor: false to Shiki config and use CSS [data-theme] selectors |
| Preview height inconsistent | Always use fixed height (h-[400px]) on example wrappers |
| Copy button not working | Ensure HTTPS or localhost (clipboard API requirement) |
Before using this skill, verify:
app/ directory)cn() utility from shadcn/ui (lib/utils.ts)If missing shadcn/ui:
npx shadcn@latest init