# Svelte DocSmith
> A documentation framework for Svelte. Interactive examples in one real, stateful app, markdown as routes, and a sidebar that builds itself.
# Introduction
## What is Svelte DocSmith?
Svelte DocSmith is the documentation framework for Svelte 5 library
authors. Your interactive examples live inside one real, stateful SvelteKit
app — not sandboxed as isolated islands, and not screenshots of a component
that used to work.
You write markdown under `src/routes/docs/`. DocSmith turns it into styled,
navigable, syntax-highlighted pages, and lets you drop the same components
your users import straight into the prose.
## Why another docs tool?
A library's docs are only as good as their examples. Screenshots go stale.
Sandboxed islands drift from the package your users install. When the
example is the real component, running in the same app as the docs, that
rot is gone by construction.
That is why a DocSmith page is a real SvelteKit route: so the button, form,
or chart in your docs is the same component your users import — running,
stateful, and impossible to let rot. Live examples are the reason to adopt
DocSmith.
The scaffolding holds its own. Drop a page under `src/routes/docs/` and the
sidebar builds itself from frontmatter — never a hand-maintained nav tree.
The shell brings the header, mobile nav, in-page TOC, and prev/next links;
the pipeline handles highlighting and anchors. You write content; the
chrome and navigation keep up.
## Highlights
- **Live examples.** Drop a component into a page; it runs, and its source is
shown from the same file, so the two can never drift.
- **Markdown as routes.** `.md` files compile to real Svelte components via
mdsvex. No loader, no catch-all route.
- **Syntax highlighting.** Shiki runs at build time on the HAST tree, with a
generous language set and dual light/dark themes.
- **Nav derives itself.** The sidebar is built from each page's frontmatter,
never hand-written.
- **The whole chrome.** Header, collapsible sidebar, mobile nav, in-page table
of contents, breadcrumbs, and prev/next links, all included.
- **Yours to theme.** One CSS import ships the Tailwind and shadcn token system;
override any token to make it your own.
## Where to next
{#snippet icon()}{/snippet}
Add DocSmith to a SvelteKit project and wire up the one-line CSS contract.
{#snippet icon()}{/snippet}
Register the pipeline and render your first page in four steps.
# Installation
## Start a new project
The fastest way to begin is the scaffolder. It creates a ready-to-run SvelteKit
project already wired with DocSmith: the markdown pipeline, the Vite plugin, the
style contract, a `DocsShell` layout, a 404 page, and a couple of sample pages.
```bash
pnpm create svelte-docsmith my-docs
```
```bash
npm create svelte-docsmith@latest my-docs
```
```bash
yarn create svelte-docsmith my-docs
```
```bash
bun create svelte-docsmith my-docs
```
Then install dependencies and start the dev server:
```bash
cd my-docs
npm install
npm run dev
```
That is the whole setup. Skip ahead to [Writing pages](/docs/writing-pages) to
start authoring. The rest of this page covers adding DocSmith to a project you
already have.
## Add to an existing project
Svelte DocSmith is a SvelteKit library. Install it with your package manager of
choice:
```bash
pnpm add -D svelte-docsmith
```
```bash
npm install -D svelte-docsmith
```
```bash
yarn add -D svelte-docsmith
```
```bash
bun add -D svelte-docsmith
```
DocSmith expects **Svelte 5**, **SvelteKit 2**, and **Tailwind CSS v4** as peer
dependencies, the same stack this documentation site runs on. A fresh
`npx sv create` app with Tailwind selected already has them.
## The CSS contract
Components are styled with Tailwind and shadcn design tokens. The whole contract
is one import in your app's stylesheet:
```css title="src/app.css"
@import 'tailwindcss';
@import 'svelte-docsmith/theme.css';
```
`theme.css` makes Tailwind scan the package (so the utility classes its
components use are generated), defines the shadcn theme tokens (`--background`,
`--primary`, `--radius`, and the rest) for `:root` and `.dark`, and pulls in the
typography and animation plugins.
That single import is also the whole customization surface: redefine any token
after it to rebrand the entire system. See [Theming](/docs/theming) for the full
token list and how dark mode is wired.
## Next
With the package installed, continue to the [Quick Start](/docs/quick-start) to
wire up the pipeline and render your first page.
# Quick Start
Four steps take you from an installed package to a live docs page in the
sidebar. Each one edits a single file.
`npm create svelte-docsmith` does every step on this page for you. This walk
through is for adding DocSmith to an existing app, or for understanding the
pieces. See [Installation](/docs/installation) for the scaffolder.
In `svelte.config.js`, add `.md` to your extensions and call `docsmith()`. It
bundles mdsvex, Shiki highlighting with a generous language set, heading
anchors, and the DocSmith page layout:
```js title="svelte.config.js"
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { docsmith } from 'svelte-docsmith/preprocess'; // [!code ++]
export default {
extensions: ['.svelte', '.md'], // [!code ++]
preprocess: [vitePreprocess(), docsmith()], // [!code ++]
kit: { adapter: adapter() }
};
```
In `vite.config.ts`, add `docsmith()`. It scans your pages' frontmatter into the
`svelte-docsmith/content` module and powers live examples:
```ts title="vite.config.ts"
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { docsmith } from 'svelte-docsmith/vite'; // [!code ++]
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [docsmith(), tailwindcss(), sveltekit()] // [!code ++]
});
```
In `src/routes/docs/+layout.svelte`, render `DocsShell`. It builds the sidebar
from the generated content index, so there is no nav array to maintain:
```svelte title="src/routes/docs/+layout.svelte"
{@render children()}
```
Create `src/routes/docs/getting-started/+page.md`. The frontmatter drives the
sidebar; everything below it is your content:
````md title="src/routes/docs/getting-started/+page.md"
---
title: Getting Started
description: Your first steps.
section: Guides
order: 1
---
## Hello
This is a real SvelteKit route. Code blocks are highlighted by Shiki, and you
can emphasise a line with the notation transformer:
```ts
const docs = loadDocs(); // [!code highlight]
```
````
## What you end up with
Those four files sit exactly here:
Drop a markdown file under `src/routes/docs/` and it appears in the sidebar,
styled, highlighted, with breadcrumbs and a table of contents. To embed a
running component, see [Writing pages](/docs/writing-pages), which covers
frontmatter, live examples, and code highlighting in full.
# Configuration
Everything you wire up once: what the package exports, the config object you pass
to the shell, and the two page-level components that own the site chrome. Props
for the authoring components (`Callout`, `Tabs`, `Badge`, and the rest) live on
their own pages in the [Components](/docs/components/callout) section.
## Package exports
- **`svelte-docsmith`**: every component, plus `defineConfig`, `createSearchEngine`, `generateSitemap`, `generateFeed`, `generateLlmsTxt`, `generateLlmsFullTxt`, and the types. Components are documented in [Components](/docs/components/callout); the rest is below.
- **`svelte-docsmith/preprocess`**: the mdsvex + Shiki preprocessor for `svelte.config.js`.
- **`svelte-docsmith/vite`**: the Vite plugin (content, search, llms and changelog indexes, plus the `?source` transform).
- **`svelte-docsmith/content`**: the generated sidebar index, exported as `docs`.
- **`svelte-docsmith/search`**: the generated full-text search index, exported as `docs` (lazy-load it; see [Search](/docs/search)).
- **`svelte-docsmith/llms`**: the generated per-page markdown index, exported as `docs` (see [SEO](/docs/seo)).
- **`svelte-docsmith/changelog`**: the generated release index, exported as `releases` (see [Changelog](/docs/changelog)).
- **`svelte-docsmith/mermaid`**: the diagram component, imported for you by a ` ```mermaid ` fence. You never import it directly.
- **`svelte-docsmith/theme.css`**: the base style contract.
- **`svelte-docsmith/themes/*.css`**: the pre-installed theme presets (see [Theming](/docs/theming)).
## defineConfig
Validates a `DocsmithConfig` and returns it unchanged, throwing a clear error on
an invalid or dynamically-built config instead of rendering a blank header.
```ts
const config = defineConfig({
title: 'My Library',
description: 'A short tagline, used as the default meta description.',
url: 'https://my-library.dev',
github: 'https://github.com/you/my-library',
version: '1.0.0'
});
```
Site title, shown in the header/sidebar and as the <title> suffix.
Default meta description, used for pages without their own. See SEO.
Canonical site origin. Enables <link rel="canonical"> and absolute Open Graph URLs.
Default social-share image (absolute, or a path resolved against url).
Base URL for the per-page “Edit this page” link, e.g.
https://github.com/you/repo/edit/main/apps/docs. Each page's source
path is appended. (“Last updated” is added from git automatically.)
GitHub URL; renders a link in the header when set.
Version string shown in the header.
Logo image src; falls back to the built-in book mark.
Top-level header navigation links.
A thin bar above the header. text is required; add a
tag for a leading pill (e.g. "New") and an
href to link it. It's dismissible by default and stays dismissed
until you change id (or the text), so bump id to
re-show a new announcement.
Footer copyright line, titled link columns, and the “Powered by” toggle.
This site runs one: the thin bar above the header is `config.announcement`. Its
`id` tracks the library version, so it returns after each release and stays out
of the way in between. Dismiss it and it holds until the next version.
## docsmith() preprocessor
From `svelte-docsmith/preprocess`, registered in `svelte.config.js`. It bundles
mdsvex, Shiki, heading anchors and the page layout, so markdown compiles to real
routes. Every option has a working default; pass none and it does the right
thing.
File extensions compiled as markdown.
Shiki themes for the dual light/dark render of markdown code fences. The
Vite plugin has its own themes for live example source; set
both to the same pair or the two will not match.
Extra Shiki languages on top of the built-in set. An unknown fence language
falls back to plain text rather than failing the build.
Absolute path to a custom mdsvex layout, or false for none. A
custom layout must export a pre component from its module
script, since code fences render through it.
Number every code block. A fence overrides it either way with
showLineNumbers or noLineNumbers.
Enable Twoslash on fences marked twoslash. Needs the optional
peer dependencies; see Code blocks.
Extra remark plugins, appended after DocSmith's own.
Extra rehype plugins, appended after DocSmith's own (slug, sectionize).
## docsmith() Vite plugin
From `svelte-docsmith/vite`, added to `plugins` in `vite.config.ts`. It scans
your pages into the generated indexes and serves the `?source` imports that
[Live Examples](/docs/live-examples) use.
Directory scanned for doc pages.
Routes root, used to derive each page's URL from its location.
Shiki themes for the ?source render behind
Live Examples. Separate from the
preprocessor's themes, because the two run from different
config files; set them to the same pair or your example source will not
match your code blocks.
Path to the changelog that feeds the release index. Point it at the package
you publish, or false to skip it.
Route the changelog is served at. Used to build feed links and to find
hand-written per-release pages.
## DocsShell
The full documentation shell: header, sidebar, content area, and table of
contents.
```svelte
{@render children()}
```
The site config (see defineConfig above).
The content index; the sidebar nav is derived from it.
The rendered page.
The generated version manifest. Pass it to scope the sidebar, search,
prev/next, and breadcrumbs to the version being read. See
Versioning.
Enable the ⌘K search palette by lazily providing the generated index, e.g.
{'() => import(\'svelte-docsmith/search\').then((m) => m.docs)'}.
Receives the active version id on a versioned site.
See Search.
Override the head tags for this page (title, meta description). Doc pages get
these from frontmatter automatically. See SEO.
Custom logo mark for the header and mobile menu.
Extra header controls, before the theme toggle.
Content rendered below the page column.
Render the decorative grid-and-glow page background.
Show the "Copy page" split button on doc pages (copy as Markdown, view the
raw .md, or open in ChatGPT / Claude). Needs the .md
endpoint. See SEO.
Show the estimated reading time on doc pages (computed at build time from
the page's word count). Set false to hide it.
void)"}>
Show the "Was this page helpful?" widget at the foot of doc pages. Pass
true for the UI alone, or a callback to record votes (wire it to
your analytics). Omit to hide it.
docs is the three-column shell; page is full-bleed
content with the same header and footer but no sidebar or TOC.
`ThemeProvider` and `ThemeToggle` handle light and dark with no consumer wiring.
`DocsShell` mounts the provider internally, so you never touch `mode-watcher`
yourself. Use `ThemeProvider` directly to wrap a page you build outside
`DocsShell`.
## ErrorPage
A styled 404 / error screen that keeps the site chrome (header, search, footer,
theme). Drop it into a SvelteKit `+error.svelte`:
```svelte title="src/routes/+error.svelte"
```
Same config as DocsShell, so the chrome matches.
Content index, so the header/footer nav still work.
HTTP status. Defaults to the current page's status.
Heading. Defaults to “Page not found” for 404, else “Something went wrong”.
Body line. Defaults to the error message, then a status-appropriate default.
Where the primary action links.
Label of the primary action.
The version manifest, same as DocsShell. Pass it so an error
under an archived prefix keeps that version's search scope and
noindex.
Enable the ⌘K palette on the error page (same loader as DocsShell).
## Types
- **`DocsmithConfig`**: the config object above.
- **`DocsContentItem`**: a content-index entry with `title`, `path`, and optional
`section`, `order`, `description`, `toc`.
- **`SearchDoc`** / **`SearchResult`** / **`SearchEngine`**: the search index
entry and the shape returned by [`createSearchEngine`](/docs/search).
- **`ResolvedVersion`**: one entry of the generated version manifest. See
[Versioning](/docs/versioning).
- **`CalloutVariant`** / **`BadgeVariant`**: the intent unions for `Callout` and `Badge`.
The vendored shadcn primitives and internal helpers (the TOC engine, the
clipboard utility, the markdown renderer map) are **not** part of the public API
and may change between releases.
# How it works
## Markdown as routes
DocSmith leans on mdsvex, which compiles markdown into real Svelte components. A
file becomes a page by its position on disk, with no loader and no catch-all
route:
The file above serves `/docs/guides/routing`. Because it is a normal SvelteKit
route, you can drop interactive Svelte components straight into the markdown and
they run as part of the same app.
## One name, two plugins
DocSmith ships two things both called `docsmith()`, imported from two entry
points. They do different jobs:
- `svelte-docsmith/preprocess` is a **Svelte preprocessor**, added in
`svelte.config.js`. It runs at compile time and turns each `.md` file into a
styled, highlighted page (mdsvex, Shiki, heading anchors, the page layout).
- `svelte-docsmith/vite` is a **Vite plugin**, added in `vite.config.ts`. It
runs at build time and generates the content index (below) plus the `?source`
transform that powers live examples.
The preprocessor and the Vite plugin are not interchangeable. The preprocessor
renders your pages; the Vite plugin builds the sidebar and live-example source.
Register one without the other and either your pages or your navigation goes
missing.
## Nav is derived, never written
There is no navigation array to maintain. The `docsmith()` Vite plugin reads
each page's frontmatter into the `svelte-docsmith/content` module, and
`DocsShell` groups the entries into the sidebar:
```ts
{
title: 'How it works',
description: 'The content model...',
section: 'Core Concepts', // sidebar group
order: 4 // sort key
}
```
`section` names the group; `order` sorts entries within it; groups are ordered
by the smallest `order` they contain. Add a page, and it slots into the sidebar
in the right place automatically.
## Two tables of contents, two jobs
DocSmith keeps two structures, and they never overlap:
- The **content index** owns build-time structure. The `docsmith()` Vite plugin
scans frontmatter into the sidebar navigation.
- The runtime **TOC engine** owns in-page scroll tracking. It scans the rendered
headings and highlights the section you are reading.
## The highlighting pipeline
Code blocks are highlighted by [Shiki](https://shiki.style) inside the
`docsmith()` preprocessor, with a generous default language set. Unknown
languages fall back to plain text instead of failing the build:
```python
def greet(name: str) -> str:
return f"Hello, {name}"
```
Highlighting is dual-theme: the same markup carries light and dark colors and
flips with the page theme, so your code reads correctly either way.
# Writing pages
## A page is a file
Every page is a `+page.md` file under `src/routes/docs/`. The directory name is
the URL, so this file serves `/docs/guides/routing`:
Create the file, and the page exists.
## Frontmatter
The frontmatter block at the top of each page drives the sidebar. Four fields:
| Field | Required | Purpose |
| ------------- | -------- | -------------------------------------------------------------------------- |
| `title` | yes | The sidebar label and page heading. |
| `description` | no | One-line summary, shown under the title. |
| `section` | no | Sidebar group, a string or a nested path. Omitted pages fall under "Docs". |
| `order` | no | Sort key within the group. |
```md
---
title: Routing
description: How pages map to URLs.
section: Guides
order: 1
---
```
`section` names the group and `order` sorts within it; groups themselves are
ordered by the smallest `order` they contain.
### Nested sections
Give `section` an array to nest a page inside a collapsible subsection. Each
entry is one level of the group path:
```md
---
title: Middleware
section: [Guides, Advanced]
order: 2
---
```
This puts "Middleware" under a collapsible **Advanced** group inside **Guides**.
Nesting can go as deep as you like, `order` still sorts each level, and a
subsection inherits the smallest `order` of its pages. The branch holding the
current page is expanded on load; the rest start collapsed.
A page with no `title` is skipped by the sidebar. If a page isn't showing up,
check its frontmatter before anything else: a stray indent or a typo'd key is
the usual culprit.
## Headings
Don't write an `#` (h1) in the body. The `title` from frontmatter is the page
heading, so start your content at `##`. Every heading gets an anchor id
automatically, and the in-page table of contents is built from `##` and `###`
headings as the page renders.
## Code blocks
Fenced code blocks are highlighted by Shiki at build time; tag the fence with a
language. To emphasise a line, append the comment `// [!code highlight]` to it
(a real comment in that language). Shiki strips the comment and highlights the
line, like the second line below:
```ts
const docs = loadDocs();
const current = docs.find((d) => d.active); // [!code highlight]
```
Unknown language tags fall back to plain text rather than failing the build, so
an unfamiliar fence won't break your site.
Line highlighting is just the start. See [Code blocks](/docs/code-blocks) for
diffs, focus, error and warning lines, and word highlighting.
## Diagrams
Tag a fence `mermaid` and it renders as a diagram instead of code, via
[Mermaid](https://mermaid.js.org), following the site's light and dark themes:
````md
```mermaid
flowchart LR
A[".md file"] --> B["mdsvex"] --> C["Svelte route"]
```
````
renders as:
```mermaid
flowchart LR
A[".md file"] --> B["mdsvex"] --> C["Svelte route"]
```
Mermaid runs in the browser, so add it to your project. It's an optional peer
dependency, pulled in only on pages that use it:
```bash
npm i -D mermaid
```
A diagram is only as readable as its labels for anyone using a screen reader.
Give one an accessible name and description with Mermaid's `accTitle` and
`accDescr` directives, and both land in the rendered SVG:
````md
```mermaid
flowchart LR
accTitle: Markdown build pipeline
accDescr: A .md file is processed by mdsvex, which produces a Svelte route.
A[".md file"] --> B["mdsvex"] --> C["Svelte route"]
```
````
If a diagram fails to parse, or `mermaid` isn't installed, the source is shown
as a plain code block instead so the page never loses the content.
## Live examples
To show a real, running component next to its source, put the component in
`src/lib/examples/`. Import `LiveExample`, then import your component twice: once
as the component, and once with the `?source` query for its build-time
highlighted source. Pass both to `LiveExample`:
```md
```
Both come from the same file, so the demo you render and the code you show can
never drift. See [Live Examples](/docs/live-examples) for a running one.
## Tabbed content
For alternatives such as package managers or framework variants, group blocks
with `Tabs` and `TabItem`. Pass the tab labels as `items`; each `TabItem`'s
`value` matches one label:
````md
```bash
npm i -D svelte-docsmith
```
```bash
pnpm add -D svelte-docsmith
```
````
See the [Components](/docs/components/callout) section for the full set you can
drop into a page: callouts, steps, cards, accordions, file trees, badges, and
more.
# Code blocks
Every fenced code block is highlighted by Shiki at build time. On top of that,
you annotate lines and words with comment markers written right in the code. The
marker comment is stripped from the rendered output, so what readers see stays
clean.
Each section below shows the markdown you write, then how it renders.
## Highlight a line
Append `// [!code highlight]` to a line (a real comment in the fence's language)
to give it a highlighted background.
You write:
```text
const config = defineConfig({
title: 'My Library' // [!code highlight]
});
```
Which renders as:
```ts
const config = defineConfig({
title: 'My Library' // [!code highlight]
});
```
## Additions and deletions
Mark a line with `// [!code ++]` for an addition or `// [!code --]` for a
deletion. They render with a colored background and a `+` / `-` gutter marker.
You write:
```text
export default {
preprocess: [vitePreprocess()], // [!code --]
preprocess: [vitePreprocess(), docsmith()], // [!code ++]
};
```
Which renders as:
```ts
export default {
preprocess: [vitePreprocess()], // [!code --]
preprocess: [vitePreprocess(), docsmith()] // [!code ++]
};
```
## Focus
`// [!code focus]` dims the other lines so the eye lands on what matters. Hover
the rendered block to bring the rest back.
You write:
```text
function setup() {
const app = createApp();
app.use(docsmith()); // [!code focus]
return app;
}
```
Which renders as:
```ts
function setup() {
const app = createApp();
app.use(docsmith()); // [!code focus]
return app;
}
```
## Errors and warnings
`// [!code error]` and `// [!code warning]` tint a line red or amber, for
call-outs like a deprecated call or a footgun.
You write:
```text
const ok = readFile('./page.md');
const bad = readFile(); // [!code error]
const risky = readFileSync('./page.md'); // [!code warning]
```
Which renders as:
```ts
const ok = readFile('./page.md');
const bad = readFile(); // [!code error]
const risky = readFileSync('./page.md'); // [!code warning]
```
## Highlight a word
`// [!code word:name]` highlights every occurrence of `name` on the next line.
Add a count like `word:name:2` to limit how many.
You write:
```text
const name = frontmatter.title; // [!code word:name]
```
Which renders as:
```ts
const name = frontmatter.title; // [!code word:name]
```
Write the marker inside a comment your language understands: `//` for
JS/TS/Svelte, `#` for bash or YAML, `` for HTML. Shiki strips the comment
along with the marker, so it never ships to the reader. (To show a marker
literally instead of applying it, as the "You write" blocks above do, put it in a
plain `text` block.)
## Highlight lines by number
Comment markers can't reach every line. Inside a Svelte template region an HTML
comment is stripped without highlighting anything, so mark those lines from the
fence itself instead. A single line, a list, or a range all work.
````md
```svelte {4}
import('svelte-docsmith/search').then((m) => m.docs)}
>
{@render children()}
```
````
renders as:
```svelte {4}
import('svelte-docsmith/search').then((m) => m.docs)}
>
{@render children()}
```
Ranges and lists use the same syntax: `{2-4}`, `{1,5}`, or both together.
## Filenames and line numbers
Add `title=` to label a block with the file it belongs to, and `showLineNumbers`
to number it. Pair `startLine=` with numbering when the snippet is lifted out of
a longer file, so the numbers match the real source.
````md
```ts title="vite.config.ts" showLineNumbers
import { docsmith } from 'svelte-docsmith/vite';
export default { plugins: [docsmith()] };
```
````
renders as:
```ts title="vite.config.ts" showLineNumbers
import { docsmith } from 'svelte-docsmith/vite';
export default { plugins: [docsmith()] };
```
Numbering every block by default is a preprocessor option, and a single fence
can still opt out with `noLineNumbers`:
```js title="svelte.config.js"
docsmith({ lineNumbers: true });
```
## Real types on hover
Add `twoslash` to a TypeScript or Svelte fence and the block is run through the
TypeScript compiler, so hovering a token shows its actual inferred type rather
than a hand-written guess.
````md
```ts twoslash
const version = '0.9.0';
const parts = version.split('.').map(Number);
```
````
renders as (hover `parts` or `version`):
```ts twoslash
const version = '0.9.0';
const parts = version.split('.').map(Number);
```
Twoslash is opt-in twice over: enable it in the preprocessor, then mark the
individual fences that want it.
```js title="svelte.config.js"
docsmith({ twoslash: true });
```
It needs three optional peer dependencies, pulled in only if you use it:
```bash
npm i -D @shikijs/twoslash twoslash-svelte typescript
```
Because the snippet really is compiled, it has to typecheck: an unresolved
import or a type error means there is no type to show. Rather than fail your
build over one block, DocSmith falls back to an ordinary highlight and warns
which block it was. Use `// @errors: 2322` to show an error deliberately,
`// @noErrors` to silence one, and `// ---cut---` to hide setup lines from the
rendered output while still compiling them.
# Live Examples
## A real, running component
The button below is a real Svelte component running as part of this app. It is
not a screenshot, and not a sandboxed iframe. Click it, then open the source:
## Single source of truth
The rendered component and the source panel above both come from **one file**,
`counter.svelte`. It is imported twice: once as a component (rendered) and once
as `?source` (highlighted at build time by the `docsmith()` plugin from
`svelte-docsmith/vite`), so the demo and its code can never drift.
The example in your docs is the same component your users import. When the
component changes, the rendered demo and its shown source both change with it,
so it can never decay into a screenshot of something that used to work.
See [Writing pages](/docs/writing-pages) for the import pattern to copy.
## API reference
The live, rendered component.
Pre-highlighted source HTML. Pass a ?source import of the same file.
# Theming
DocSmith ships its entire look as shadcn-style design tokens behind a single
stylesheet. You restyle the whole system by redefining tokens, not by touching
components, so a rebrand is a handful of CSS variables instead of a fork.
## One import, one contract
The style contract is one import on top of Tailwind:
```css title="src/app.css"
@import 'tailwindcss';
@import 'svelte-docsmith/theme.css';
```
`theme.css` does three things: it makes Tailwind scan the package so the
components' utility classes are generated, it registers the shadcn token set for
`:root` and `.dark`, and it pulls in the typography and animation plugins. Every
component reads those tokens, so the tokens are the only surface you style.
On its own, `theme.css` gives you the default theme, **Darkmatter**: a
near-monochrome shell with a warm orange primary. You import nothing else to get
it.
## The presets
Eleven presets ship in the box. Pick one below to preview it, and toggle the
site's dark mode to see both sides:
A preset is a stylesheet that redefines the color tokens, and for some the corner
radius, and nothing else. Import it after `theme.css` and it wins:
```css title="src/app.css"
@import 'tailwindcss';
@import 'svelte-docsmith/theme.css';
@import 'svelte-docsmith/themes/amethyst.css';
```
Darkmatter is already baked into `theme.css`, so you only import
`themes/darkmatter.css` to return to it after trying another preset.
Available: `darkmatter` (default), `tangerine`, `amethyst`, `graphite`,
`evergreen`, `rose`, `ocean`, `nord`, `claude`, `bubblegum`, and `mono`. Each
covers light and dark. Want your own brand color instead? Skip the preset and
override the tokens directly.
## Overriding tokens
Redefine any token after the import and it wins. Tokens are OKLCH, so change the
primary and every button, link, and accent follows:
```css title="src/app.css"
@import 'tailwindcss';
@import 'svelte-docsmith/theme.css';
:root {
--primary: oklch(0.55 0.2 265); /* your brand color */
--radius: 0.5rem; /* tighter corners */
}
```
If an override is not taking effect, import order is almost always the cause.
Your redefinition has to come after the `theme.css` import, or the package's own
value wins. The same rule applies to preset stylesheets.
## The token set
The tokens are standard shadcn. The ones you reach for most:
| Token | Controls |
| ------------------------------------ | ------------------------------------- |
| `--background` / `--foreground` | Page surface and body text |
| `--primary` / `--primary-foreground` | Brand color and text on it |
| `--muted` / `--muted-foreground` | Quiet fills and secondary text |
| `--accent` / `--accent-foreground` | Hover and highlight surfaces |
| `--border` / `--input` / `--ring` | Hairlines, field borders, focus rings |
| `--card` / `--popover` | Raised surfaces |
| `--sidebar*` | The docs sidebar, tokened separately |
| `--radius` | Corner rounding across the system |
## Dark mode
Dark mode is class-based: every token has a `.dark` variant, and DocSmith styles
respond to a `dark` class on the `` element. The docs site toggles it with
[`mode-watcher`](https://github.com/svecosystem/mode-watcher). Drop its
`` in your root layout and the theme toggle and system preference
work out of the box.
An override only covers the theme whose selector you write it under, so set a
token for both modes to change both:
```css
:root {
--primary: oklch(0.55 0.2 265);
}
.dark {
--primary: oklch(0.7 0.16 265); /* lighter, for the dark surface */
}
```
## Fonts
Three families are set as tokens: `--font-sans`, `--font-serif`, and
`--font-mono`. Point them at your own faces and load the fonts however you
normally would (a `` in `app.html`, `@fontsource`, or your host):
```css
:root {
--font-sans: 'Geist', sans-serif;
--font-mono: 'Geist Mono', monospace;
}
```
# Search
DocSmith builds a full-text search index from your pages at build time and ships
a ⌘K / Ctrl-K command palette that searches it. There is nothing to host and no
service to configure.
## Enable it
Pass a `search` loader to `DocsShell`. It hands back the generated index, which
DocSmith lazily fetches the first time the palette opens, so it never weighs down
your initial load:
```svelte title="src/routes/docs/+layout.svelte" {12}
import('svelte-docsmith/search').then((m) => m.docs)}
>
{@render children()}
```
That is the whole setup. A search button appears in the header, ⌘K (Ctrl-K on
Windows and Linux) opens the palette from anywhere, and results link straight to
the matching page. Omit the prop to leave search off.
Passing a function that dynamically imports `svelte-docsmith/search` lets your
bundler split the index into its own chunk. The index is fetched only when a
reader first opens search, not on every page view.
## What gets indexed
Each page contributes its `title`, its `h2`/`h3` headings, its frontmatter
`description`, and its body text, reduced to plain prose, with code blocks,
component markup, and markdown punctuation stripped out. Title and heading
matches rank above body matches.
## A custom search UI
The palette is the default, but the engine is exported if you want to build your
own input. `createSearchEngine` takes the generated index and returns a
`search(query, limit?)` that yields ranked results with a context snippet:
```ts
import { createSearchEngine } from 'svelte-docsmith';
import { docs } from 'svelte-docsmith/search';
const engine = createSearchEngine(docs);
for (const hit of engine.search('theming')) {
console.log(hit.title, hit.path, hit.snippet);
}
```
# SEO
`DocsShell` writes the head tags for every page (`
`, meta description,
canonical URL, and Open Graph / Twitter Card tags) with no per-page wiring. Doc
pages get theirs straight from frontmatter.
## Per-page, from frontmatter
A page's `title` becomes `Page · Site Title`, and its `description` becomes the
meta and social description. You already write both to drive the sidebar, so
there is nothing extra to add:
```md
---
title: Installation
description: Add Svelte DocSmith to a SvelteKit project.
section: Getting Started
order: 2
---
```
## Site-wide defaults
Set the defaults once in your `DocsmithConfig`. `url` is the piece that unlocks
absolute links (a canonical `` and absolute `og:url`/image), so search
engines and social scrapers resolve them correctly:
```ts
export const siteConfig = defineConfig({
title: 'My Library',
description: 'A short tagline, used when a page has no description.',
url: 'https://my-library.dev',
ogImage: '/og.png' // absolute, or resolved against `url`
});
```
| Field | Used for |
| ------------- | -------------------------------------------------------------------- |
| `description` | Default meta description for pages without their own |
| `url` | Canonical origin; enables `` and absolute URLs |
| `ogImage` | Default social-share image |
Without `url`, DocSmith can't build an absolute address, so it omits the
canonical and `og:url` tags rather than emit a wrong one. Set `url` to your
deployed origin to turn them on.
## Non-doc pages
Pages that aren't markdown (a landing page, a custom route) have no
frontmatter, so pass the `seo` prop to set or override the head:
```svelte
{@render children()}
```
## Sitemap
`generateSitemap` builds a `sitemap.xml` from your content index. Add a
`src/routes/sitemap.xml/+server.ts`:
```ts title="src/routes/sitemap.xml/+server.ts"
import { docs } from 'svelte-docsmith/content';
import { generateSitemap } from 'svelte-docsmith';
import { siteConfig } from '$lib/site-config';
export const prerender = true;
export function GET() {
const body = generateSitemap(siteConfig.url ?? '', [
{ path: '/' },
...docs.map((d) => ({ path: d.path, lastmod: d.lastUpdated }))
]);
return new Response(body, { headers: { 'content-type': 'application/xml' } });
}
```
Each entry gets a `` from the page's last git commit — or none, when
the page has no date (no frontmatter `lastUpdated`, and git could not supply
one). On a **shallow clone** (the default for `actions/checkout` and many
hosts), DocSmith omits every git-derived date rather than stamp every page with
the same build day, which would teach crawlers to ignore the field. Build from a
full history (`fetch-depth: 0` in GitHub Actions) when you want real
`` values, or set `lastUpdated` in frontmatter on pages that need one.
Then point crawlers at the sitemap from `static/robots.txt`:
```txt
User-agent: *
Allow: /
Sitemap: https://your-docs.dev/sitemap.xml
```
## llms.txt
The [llms.txt](https://llmstxt.org) standard gives AI tools a clean, plain-text
view of your docs. Svelte DocSmith generates the data at build time in the
`svelte-docsmith/llms` module, and two helpers turn it into the two files the
standard defines: `llms.txt` (a curated index of links) and `llms-full.txt`
(the full text of every page).
Add `src/routes/llms.txt/+server.ts`:
```ts title="src/routes/llms.txt/+server.ts"
import { docs } from 'svelte-docsmith/llms';
import { generateLlmsTxt } from 'svelte-docsmith';
import { siteConfig } from '$lib/site-config';
export const prerender = true;
export function GET() {
const body = generateLlmsTxt(
{ title: siteConfig.title, description: siteConfig.description, origin: siteConfig.url },
docs
);
return new Response(body, { headers: { 'content-type': 'text/plain; charset=utf-8' } });
}
```
And `src/routes/llms-full.txt/+server.ts`, identical but for `generateLlmsFullTxt`:
```ts title="src/routes/llms-full.txt/+server.ts"
import { docs } from 'svelte-docsmith/llms';
import { generateLlmsFullTxt } from 'svelte-docsmith';
import { siteConfig } from '$lib/site-config';
export const prerender = true;
export function GET() {
const body = generateLlmsFullTxt(
{ title: siteConfig.title, description: siteConfig.description, origin: siteConfig.url },
docs
);
return new Response(body, { headers: { 'content-type': 'text/plain; charset=utf-8' } });
}
```
Both follow your sidebar reading order, grouping pages by `section` and sorting
by `order`. Each page's title becomes an `h1`, and its `description` frontmatter
annotates the link in the index.
You are reading these docs through this exact pipeline. Open
[/llms.txt](/llms.txt) and [/llms-full.txt](/llms-full.txt) to see the output.
## Copy page
The same per-page markdown powers a "Copy page" button on every doc page. Turn
it on with the `copyPage` prop on `DocsShell`:
```svelte
{@render children()}
```
The split button copies the page as Markdown, and its dropdown links to the raw
`.md`, or opens the page in ChatGPT or Claude. It expects each page to be
available at `.md`, so add one catch-all endpoint,
`src/routes/[...slug].md/+server.ts`:
```ts title="src/routes/[...slug].md/+server.ts"
import { docs } from 'svelte-docsmith/llms';
import { error } from '@sveltejs/kit';
import type { EntryGenerator, RequestHandler } from './$types';
export const prerender = true;
export const entries: EntryGenerator = () =>
docs.map((doc) => ({ slug: doc.path.replace(/^\//, '') }));
export const GET: RequestHandler = ({ params }) => {
const doc = docs.find((d) => d.path === `/${params.slug}`);
if (!doc) error(404, 'Not found');
return new Response(doc.content, {
headers: { 'content-type': 'text/markdown; charset=utf-8' }
});
};
```
Try it: the "Copy page" button at the top of this page, or open
[this page as Markdown](/docs/seo.md).
# Changelog
Every release you ship already produces release notes. DocSmith reads them and
publishes them, so the changelog on your site can never fall behind what
actually went out.
## Where the entries come from
The `docsmith()` plugin parses a `CHANGELOG.md` into releases and serves them as
the `svelte-docsmith/changelog` module. The format Changesets writes is what it
expects: an `## ` heading per release, `### Minor Changes` style
groups, and one bullet per entry. Commit hashes are stripped, multi-paragraph
entries survive intact, and each entry's markdown is rendered to HTML at build
time so your site needs no runtime markdown renderer.
Release dates come from git: the commit that first introduced each version's
heading. No date is invented when the history is unavailable.
```ts title="vite.config.ts"
// Defaults to CHANGELOG.md beside your app. In a monorepo, point it at the
// package you actually publish.
docsmith({ changelog: '../../packages/my-lib/CHANGELOG.md' });
```
## Add the page
```svelte title="src/routes/changelog/+page.svelte"
```
## Add the feed
An Atom feed makes each release something readers can subscribe to rather than
something they have to check for.
```ts title="src/routes/changelog.xml/+server.ts"
import { releases } from 'svelte-docsmith/changelog';
import { generateFeed } from 'svelte-docsmith';
import { siteConfig } from '$lib/site-config';
export const prerender = true;
export function GET() {
const body = generateFeed(releases, {
url: siteConfig.url,
title: siteConfig.title
});
return new Response(body, {
headers: { 'content-type': 'application/atom+xml; charset=utf-8' }
});
}
```
Atom rather than RSS, because it requires an unambiguous id and timestamp per
entry, which a version and its release date give exactly.
## Write a proper post for a release
Generated entries are terse by design. When a release deserves more, add
`src/routes/changelog//` as a normal page and DocSmith links the
release to it instead of to its anchor. Dots are awkward in directory names, so
`0-9-0` works as well as `0.9.0`. Every other release stays generated, so
nothing goes undocumented while you write up the ones that matter.
# Versioning
Versioning sits outside the 1.0 stability promise. `DocsVersions`,
`currentOnly`, the archive marker, and `archive-version` may change in a minor
release until authors have used them for real. An archive freezes prose, code
samples and URLs — not what a page imports. Live examples on an archived page
keep resolving to whatever the app currently has installed.
## Why version your docs
Ship a breaking major and two readers need different things: the one still on
the old release needs its docs, and the one on the new release should not trip
over stale pages. Versioning keeps the old release live at its own URL, scopes
the sidebar and search to the version being read, and steers visitors who land
on an archived page to the current one.
It is opt-in, and it is designed so that turning it on changes nothing. Your
current docs stay exactly where they are.
## The model
The **current version** is the docs for your latest release. It lives at your
docs root and is served unprefixed, so `/docs/theming` is `/docs/theming`
forever. It is also the only version you edit.
An **archived version** is a frozen copy of the docs for a superseded release,
served under its own prefix at `/docs/v1/theming`. You never edit an archive.
You create one at the moment you ship a breaking release, and then carry on
editing the docs root.
That means adopting versioning never moves a URL, and shipping a new release
never moves one either. Only the archive is new.
```text
src/routes/docs/
introduction/+page.md → /docs/introduction (current)
theming/+page.md → /docs/theming (current)
v1/introduction/+page.md → /docs/v1/introduction (archived)
```
## Declare your versions
Versions live in the `docsmith()` Vite plugin, because it scans them at build
time to tag each page.
```ts
// vite.config.ts
import { docsmith } from 'svelte-docsmith/vite';
export default defineConfig({
plugins: [
docsmith({
versions: {
current: { id: 'v2', label: 'v2' },
archived: [{ id: 'v1', label: 'v1' }]
}
})
// tailwindcss(), sveltekit()
]
});
```
Each archived version's `id` is both its folder name under your docs directory
and its URL prefix. The current version has no folder of its own: its pages are
the ones that are not inside an archive.
Declaring only `current` is perfectly valid, and is how you start. Nothing
renders differently until the first archive exists.
### What makes a valid id
An id has to start with a letter or a digit, and can then hold letters, digits,
`.`, `-` and `_`. So `v1`, `v1.0`, `2.x` and `next` are all fine, while
`../evil`, `.hidden` and `api/v1` are rejected at build time.
The rule applies to `current` as well as to archives, even though the current
version's id never appears in a URL. Archiving turns today's current id into an
archive folder, so an id that was legal only as `current` would fail one release
later.
## When the config and your folders disagree
Versions are declared by hand, so your config and your docs folder are two
separate claims about which versions exist. The build checks that they agree and
fails if they do not, because neither mismatch shows up in the output: an
archive the config does not know about gets merged into your current docs, and a
section folder wrongly declared as a version disappears from your current
sidebar.
Telling the two apart takes a marker. `archive-version` writes a
`.docsmith-archive` file into every archive it creates, and the build reads it.
A folder under your docs root is an archive when it has that file and is
declared in `versions`. Any other combination is an error:
| On disk | In `versions` | Result |
| -------------- | ------------- | -------------------------------- |
| Has the marker | Declared | Served as an archived version |
| Has the marker | Not declared | Build fails: undeclared archive |
| No marker | Declared | Build fails: not an archive |
| No marker | Not declared | An ordinary section of your docs |
If you create an archive folder yourself rather than with `archive-version`, add
an empty `.docsmith-archive` file inside it. Without one the build treats it as
part of your current docs and refuses to accept it as a version.
## Wire it into the shell
The plugin emits a `versions` manifest next to your content. Pass both to
`DocsShell`.
```svelte
import('svelte-docsmith/search').then((m) => m.docs)}
>
{@render children()}
```
That is the whole wiring. No redirect, no route changes. `DocsShell` reads the
active version from the URL and scopes the sidebar, search, prev/next, and
breadcrumbs to it.
If you use [`ErrorPage`](/docs/configuration#errorpage), pass it `versions` too.
A 404 under an archived prefix then keeps that version's search scope and its
`noindex`, instead of falling back to the current version.
## What readers get
Once at least one archive exists, a **version switcher** appears in the header.
It keeps readers on the same page when they switch, falling back to that
version's home when the page has no equivalent there.
On an archived version, a **banner** tells readers what they are looking at and
links to the current equivalent. Archived pages also drop their "Edit this page"
link, since the archive is frozen.
## Search engines see one version
Archived docs should not compete with the current release in search results, but
they are real content people still look for. So archives stay indexable with a
self-canonical, while `sitemap.xml` and `llms.txt` list the current version
only. Scope those endpoints with `currentOnly`:
```ts
// src/routes/sitemap.xml/+server.ts
import { docs, versions } from 'svelte-docsmith/content';
import { generateSitemap, currentOnly } from 'svelte-docsmith';
import { siteConfig } from '$lib/site-config';
export function GET() {
const body = generateSitemap(siteConfig.url ?? '', [
{ path: '/' },
...currentOnly(docs, versions).map((d) => ({ path: d.path, lastmod: d.lastUpdated }))
]);
return new Response(body, { headers: { 'content-type': 'application/xml' } });
}
```
To keep a version out of search engines entirely, set `noindex: true` on it.
## Archive a release
When you ship a breaking release, freeze the docs that described the old one:
```bash
npx svelte-docsmith archive-version v1 --label v1
```
This copies your current docs into `v1/`, and does three things a plain copy
would get wrong. It rewrites in-content links so they stay inside the archive,
because an absolute link like `](/docs/theming)` would otherwise keep pointing
at your newest docs. It writes each page's real last-updated date into its
frontmatter, so the archive does not claim every page changed on the day you
created it. And it marks the folder as an archive.
Review the diff, then update your config to serve the archive and rename the
current version:
```ts
versions: {
current: { id: 'v2', label: 'v2' },
archived: [{ id: 'v1', label: 'v1' }]
}
```
Until you paste that in, the build fails and reports the new archive as
undeclared. That is deliberate: it is the reminder, and it arrives while you are
still looking at the terminal. The command prints the block to paste.
## What archives cost
Worth knowing before you keep many of them around:
- Archived pages are compiled by your current setup, so a breaking change to a
docs component changes how every archive renders.
- An archive freezes your content, not what it imports. A page importing a
component from `$lib` or a package keeps resolving to whatever your app has
installed, so a live example on an archived page demonstrates your current
library rather than the version the page documents. `archive-version` lists
the pages this applies to as it copies them.
- Each archive adds its pages to the content index and its text to the search
index, both of which grow linearly with the number of versions you keep.
- Archive folders are real files in your repo, permanently.
Keeping one or two archives is cheap. Keeping ten is a decision worth making
deliberately.
# Callout
Highlight something the reader shouldn't miss. Four intents, each with its own
icon and color. The body stays on the page's normal text color so it's always
legible.
This is a **note**, neutral informational context.
This is a **tip**, a helpful shortcut or best practice.
This is a **warning**, so proceed carefully.
This is a **danger** callout: something here can break or lose data.
## Usage
Import it and pick a `variant`. The default is `note`; pass `title` to override
the heading. Leave **blank lines** around the content so mdsvex parses the
markdown inside (bold, links, code). Without them it renders as literal text.
```svelte
You can override the heading with the `title` prop, and use **markdown**
inside, including `code` and [links](/docs/theming).
```
## API reference
Visual intent.
Heading above the body.
# Steps
A numbered walkthrough (a connecting line with numbered badges) for setup flows
where the order matters. Each step is a ``; the numbers are automatic.
Add `svelte-docsmith` to your SvelteKit project.
Add `docsmith()` in `svelte.config.js` and `vite.config.ts`.
Drop a `+page.md` under `src/routes/docs/` and it appears in the sidebar.
## Usage
`Steps` and `Step` are plain components. They work in a markdown page **and** in
any `.svelte` file, with no preprocessor required. Give each `Step` an optional
`title`; leave blank lines around markdown content so it's parsed.
```svelte
Do this.Do that.
```
### Markdown shortcut
Inside a markdown page only, you can skip the `` tags and use a plain
ordered list, and mdsvex turns it into steps:
```md
1. Do this.
2. Do that.
```
## API reference
### Steps
Wraps its `` children; no other props.
### Step
Optional heading for the step.
# Card & CardGrid
A `Card` groups a title and description, optionally as a link. Give it an `href`
and it becomes clickable, with a hover state and a trailing arrow. `CardGrid`
lays cards out in a responsive grid that reflows without breakpoints: as many
columns as fit, down to one on narrow screens.
What Svelte DocSmith is and who it's for.
Wire up the pipeline and render your first page.
Override tokens or pick a pre-installed theme.
## Usage
```svelte
Wire up the pipeline and render your first page.
Opens in a new tab.
```
## API reference
### Card
Card heading.
Makes the card a link.
Open the link in a new tab.
Optional leading icon.
### CardGrid
The Cards to lay out.
# Tabs
Group alternatives such as package managers, framework variants, or OS-specific
commands so the reader sees one at a time. Give each `TabItem` a `label`; `Tabs`
builds the tab row from them, so there is nothing to keep in sync.
```bash
npm i -D svelte-docsmith
```
```bash
pnpm add -D svelte-docsmith
```
```bash
yarn add -D svelte-docsmith
```
## Usage
Leave blank lines around the content inside each `TabItem` so the markdown (code
fences, prose) is parsed. The first tab is selected by default; pass `value` on
`Tabs` to start on a different one.
````svelte
```bash
npm i -D svelte-docsmith
```
```bash
pnpm add -D svelte-docsmith
```
````
## Synced tabs
Give related `Tabs` the same `syncKey` and they share one selection: pick `pnpm`
in any block and every block with that key switches to `pnpm`, across the page
and the rest of the site. The choice is remembered across reloads. Use it for
package managers, runtimes, or any choice a reader makes once and keeps.
Try it. These two blocks share `syncKey="demo-pm"`; changing one moves the other:
```bash
npm i -D svelte-docsmith
```
```bash
pnpm add -D svelte-docsmith
```
```bash
yarn add -D svelte-docsmith
```
```bash
npx sv add svelte-docsmith
```
```bash
pnpm dlx sv add svelte-docsmith
```
```bash
yarn dlx sv add svelte-docsmith
```
## API reference
### Tabs
Label of the tab selected by default.
Sync group. Blocks with the same key share their selection and remember it
across reloads.
### TabItem
The tab's trigger text.
Underlying value, only needed to disambiguate duplicate labels.
# Accordion
Fold FAQs, optional details, or long asides behind a heading the reader expands
on demand, one panel at a time by default, so the page stays scannable.
A documentation framework for Svelte 5. Your markdown files _are_ SvelteKit
routes, so examples run as part of one real app.
No. Nav is derived from each page's frontmatter, never hand-written.
Yes. Pass `multiple` to the `Accordion` and every panel toggles independently.
## Usage
Each `AccordionItem` takes a `title` (the always-visible summary) and claims
its own value automatically, so you never wire up ids. Leave **blank lines**
around the panel content so mdsvex parses the markdown inside.
```svelte
Answer with full **markdown**, including `code` and [links](/docs/theming).
Another answer.
```
Pass `multiple` to let panels stay open independently:
```svelte
```
## API reference
### Accordion
Allow several panels open at once.
### AccordionItem
The heading that toggles the panel.
# Badge
Label a status, version, or category inline. Six intents cover the usual docs
needs, neutral through danger, each driven by your theme tokens so they recolor
with the rest of the site.
## Usage
Import it and pick a `variant`. The default is `default`; pass `href` to turn the
badge into a link.
```svelte
StableBetaNew
```
## API reference
Visual intent.
Renders the badge as a link.
For a linked badge, open in a new tab with rel="noopener noreferrer".
# Kbd
Show a key or shortcut the way a keyboard does: a small, raised cap that reads as
_press this_, not as code.
Save with ⌘S, or open the palette with
CtrlK.
## Usage
Wrap each key in its own `Kbd`; combine several for a chord.
```svelte
Press CtrlK to search.
```
## API reference
The key label to show.
# File Tree
Sketch a folder layout so the reader sees where a file goes. Indentation and
folder icons carry the structure, no ASCII art to keep aligned by hand.
## Usage
Nest `FileTreeItem`s to nest directories; an item with children is a folder
automatically. Pass `folder` to style an empty directory, and `highlight` to
call out the entry a guide is about to touch.
```svelte
```
## API reference
### FileTree
Wraps its `` children; no other props.
### FileTreeItem
File or directory name.
Force folder styling for an empty directory.
Emphasize the entry.
# Props Table
Document a component's props the same way on every page: name, type, and
description in one styled table, so the API reference reads consistently instead
of drifting per author.
Visual intent of the component.
Turns the element into a link.
Prevents interaction.
## Usage
Wrap `Prop` rows in a `PropsTable`. Give each `Prop` a `name`; add `type`,
`default`, or `required` as needed, and put the description in the body.
```svelte
Visual intent of the component.
Turns the element into a link.
```
## API reference
### PropsTable
Optional caption, e.g. the component name when a page documents several.
### Prop
Property name.
Type signature, shown as code.
Default value, shown beside the type.
Marks the property as required.
# Landing Page
Five components compose the page in front of your docs: `Hero`, `FeatureGrid`
with `Feature`, `CTA`, and the `Action` link the first and third share. Each one
renders its own full-width `` with the container, padding, and vertical
rhythm already set, so a landing page is a handful of components rather than a
wall of layout classes.
They draw every colour from the same design tokens as your docs, so they follow
your theme preset and both colour schemes with nothing to configure.
## Usage
Pass `layout="page"` to `DocsShell` so the shell drops the sidebar and table of
contents and gives the sections the full width of the viewport.
```svelte
{#snippet actions()}
Get startedRead the docs
{/snippet}
Add a file, get a page.A command palette with no service to run.Eleven presets ship built in.
{#snippet actions()}
Quick start
{/snippet}
```
`npm create svelte-docsmith` scaffolds a landing page built from these, so the
fastest way to see them running is to start a project and edit the result.
## Hero
The opening section. Give it a `media` snippet and it becomes a two-column
split, with your headline on the left and a code sample, screenshot, or live
component on the right. Leave `media` off and the hero centres on a single
column instead, which suits a project that leads with a sentence rather than a
screenshot.
`title` and `description` each take a plain string or a snippet. Reach for the
snippet form when you want to mark part of the headline up, such as colouring
one word with `text-primary`.
The headline. Pass a snippet to mark part of it up.
Supporting paragraph below the headline.
A pill, badge, or announcement link above the headline.
Call-to-action buttons, usually one or two Action links.
The second column. Without it the hero centres on one column.
## Feature grid
A grid of short capability blurbs. `FeatureGrid` owns the section heading and
the layout; each `Feature` is one cell, with an optional icon rendered in a
tinted square beside the text.
Set `background="muted"` to tint the band and rule it top and bottom. Alternate
it against the sections either side and the page reads as distinct bands rather
than one continuous scroll.
Section heading above the grid.
Supporting line below the heading.
Columns at the widest breakpoint. The grid steps down to two, then one, on
narrower screens.
muted tints the band and adds a rule above and below.
The Feature cells.
The cell heading.
Optional leading icon, rendered in a tinted square.
The description.
## CTA
The closing panel: a bordered card with a soft glow behind the heading, so the
page ends on your primary colour instead of trailing off. Use `before` for a
note that should sit above the heading (for example a pre-release `Callout`) so
the section still ends on the actions; use `children` for content below them.
The closing heading.
Supporting line below the heading.
Call-to-action buttons.
Content above the heading — a pre-release note, for example — so the
section can still end on the actions.
Content below the actions, such as a secondary note.
## Action
The call-to-action link used by `Hero` and `CTA`. `primary` is the filled
button, `secondary` the outlined one that sits beside it. The primary variant
carries a trailing arrow that nudges on hover; turn it off with `arrow={false}`.
It is deliberately small. Anything more elaborate, such as a button that copies
an install command to the clipboard, is clearer written as your own markup
inside the `actions` snippet.
Where the link points.
Filled or outlined.
Open in a new tab with rel="noopener noreferrer".
Trailing arrow. Defaults to on for primary, off for secondary.
The link label.