This file contains the concatenated content of all cards to help LLMs understand the available documentation.
Deck turns a directory of Markdown into a scalable, zero-config component playground and documentation site.
It is designed to handle hundreds or even thousands of documentation files without a slow initial load time, making it ideal for large projects and component libraries.
deck-dev needs no bundler and no config. Plugins are also available for Vite, @web/dev-server, and webpack-dev-server / Rspack.deck-build command generates a fully static, production-ready site that can be hosted on any static hosting provider.<deck-demo>: A powerful custom element for embedding live, stateful, and hot-reloading component demos directly in your documentation.agents.md and agents.html — the whole deck as one document, with demo sources inlined — for LLM agents that cannot run a single-page app.A Deck project is typically created as a sub-project to document a larger library or application. It's common to create the deck inside a subdirectory like /deck or /docs within your main project.
This guide assumes you are setting up a new deck in a subdirectory.
npm install --save-dev @3sln/deck
That is the only dependency. Deck brings its own dev server and its own bundler.
In your package.json, add scripts and a @3sln/deck configuration block. You must provide a title for your documentation site.
{
"name": "my-cool-project-docs",
"private": true,
"type": "module",
"scripts": {
"dev": "deck-dev",
"build": "deck-build"
},
"devDependencies": {
"@3sln/deck": "^0.0.14"
},
"@3sln/deck": {
"title": "My Cool Project"
}
}
Create your documentation files using Markdown (.md). You can organize them in any directory structure you like.
my-project/
├── deck/ <-- Your Deck project lives here
│ ├── docs/
│ │ ├── introduction.md
│ │ └── components/
│ │ └── button.md
│ └── package.json
└── src/ < -- Your main project code
Start the development server from within your deck subdirectory and you're ready to go!
cd deck
npm run dev
npm run build
deck-build writes a static site to out/, ready for any file host.
deck-dev is the zero-config option, but a deck can also run inside a dev server your project already uses. Each of these is a drop-in replacement for step 4.
// vite.config.js
import {defineConfig} from 'vite';
import deck from '@3sln/deck/vite-plugin';
export default defineConfig({plugins: [deck()]});
Deck uses Vite's own HMR socket here rather than opening a second one.
// web-dev-server.config.mjs
import deck from '@3sln/deck/wds-plugin';
export default {
nodeResolve: true,
plugins: [deck()],
};
// rspack.config.js — webpack.config.js is identical
import {setupMiddlewares} from '@3sln/deck/webpack-plugin';
export default {
mode: 'development',
entry: {},
devServer: {
static: {directory: import.meta.dirname},
setupMiddlewares: setupMiddlewares({root: import.meta.dirname}),
},
};
Deck's dev server is a plain connect/express middleware, so express, connect, polka and Node's own http all work:
import {createDeckMiddleware} from '@3sln/deck/dev-core';
app.use(createDeckMiddleware({root: process.cwd()}));
<deck-demo> ElementThe <deck-demo> custom element is the heart of Deck's interactive documentation. It allows you to embed live, stateful, and hot-reloading component demos directly in your Markdown files.
To use it, you place the tag in your Markdown, give it an id,
and point its src attribute to the demo script.
<deck-demo id="my-awesome-demo" src="/demos/my-awesome-demo.js"></deck-demo>
The script referenced in src must have a default export that is a function. This function receives a driver object as its first argument.
// /demos/my-awesome-demo.js
export default driver => {
// Your demo logic goes here
};
The driver object is an API that allows your demo to interact with the <deck-demo> element's UI, which includes a source code viewer, property editor, and content panels.
driver.panel(name, renderFn)Creates a tabbed panel for rendering content.
name (string): The title of the tab.renderFn (function): A function that receives (container, signal).container: The HTML element to render your demo into.signal: An AbortSignal that fires when the demo is about to be unmounted. Use this for cleanup.driver.property(name, {type, defaultValue})Creates a reactive property control in the "Properties" panel. This allows users to interact with your demo. It returns an observable that provides the current value of the property input.
name (string): The name of the property.type (string, optional): The input type to render for the property.defaultValue (optional): The initial value to use if the property doesn't already exist.driver.signalAn AbortSignal that's aborted when the demo is being torn down.
Here is a simple demo script that shows a message and lets the user control its text and color.
import {p, reconcile} from '@3sln/dodo';
export default driver => {
const message$ = driver.property('Message', { type: 'text', value: 'Hello, Deck!' });
driver.panel('Demo', (container, signal) => {
const render = (message) => {
reconcile(container, [
p({ $styling: { color: currentProps.textColor } },
message
)
]);
};
const sub = message$.subscribe(message => {
render(message);
});
signal.addEventListener('abort', () => {
sub.unsubscribe();
reconcile(container, []);
});
});
};
This card embeds the demo below with a single tag. Edit /demos/counter-demo.js
while the dev server is running and it re-runs in place, keeping the properties
you have set.
import * as d from '@3sln/dodo';
import {cell, watch} from '@3sln/dodo/reactive';
/**
* The demo shown on the `<deck-demo>` card.
*
* A demo module's default export is called with a `driver` and re-called with
* the same argument whenever this file changes, so anything it sets up has to
* be torn down through `driver.signal`.
*/
export default driver => {
const step$ = driver.property('Step', {type: 'range', min: 1, max: 10, defaultValue: 1});
const label$ = driver.property('Label', {type: 'text', defaultValue: 'Clicks'});
driver.panel('Counter', (container, signal) => {
const count = cell(0);
let step = 1;
let label = 'Clicks';
const rerender = () => {
d.reconcile(container, [
watch(count, value =>
d.div(
{
$styling: {
display: 'flex',
'align-items': 'center',
gap: '1em',
'font-family': 'system-ui, sans-serif',
color: 'var(--text-color, #222)',
},
},
d.button(
{
$styling: {
padding: '0.5em 1em',
'border-radius': '6px',
border: '1px solid #8884',
cursor: 'pointer',
},
},
`+${step}`,
).on({click: () => count.update(n => n + step)}),
d.span(`${label}: ${value}`),
),
),
]);
};
const subscriptions = [
step$.subscribe(value => {
step = Number(value) || 1;
rerender();
}),
label$.subscribe(value => {
label = value || 'Clicks';
rerender();
}),
];
rerender();
signal.addEventListener('abort', () => {
subscriptions.forEach(subscription => subscription.unsubscribe());
d.reconcile(container, null);
});
});
};
Deck is configured through a @3sln/deck field in your project's package.json file.
Deck uses a simple override system for configuration. You can define a base configuration at the root of the @3sln/deck object. Then, you can create dev and build sub-objects to override any of those settings for a specific environment.
dev Block: Overrides for the development server (deck-dev, or whichever dev server plugin you use).build Block: Overrides for the production build (deck-build).When Deck loads, it merges the root configuration with the environment-specific block. For example, when running the deck-build command, Deck will merge the root {...} options with the build: {...} options.
Any of the following options can be placed at the root or within the dev and build blocks.
title (string): The title of your documentation site.
pinned (array of strings): A list of absolute paths to cards that should be pinned to the top of the card list.
importMap (object): An import map to be included in the index.html. This is essential for remapping module specifiers, especially for demos.
outDir (string): The output directory for the built site, relative to the project root. This is primarily useful in the build block. Defaults to out.
url (string): Where this deck is published, e.g. https://deck.3sln.com. A sitemap needs absolute URLs and only you know them, so setting this is what produces sitemap.xml; robots.txt and llms.txt are written either way, with relative links when it is unset.
description (string): One sentence about the deck. Used for the page's <meta name="description"> and as the summary line in llms.txt.
esbuild (object): Options passed to esbuild when it bundles your <deck-demo> modules — alias, define, loader, tsconfig, jsxImportSource and friends. Under the Vite plugin a demo is resolved by your own Vite config and this is not needed; every other dev server, and the build, bundle demos with esbuild and would otherwise not know what @app/button refers to.
{
"@3sln/deck": {
"esbuild": {
"alias": {"@app": "./src"},
"define": {"__DEV__": "true"}
}
}
}
pick (object): A map of source paths to destination paths, allowing you to copy files or directories into the build output. This is primarily useful in the build block to include assets or dependencies for your static site.
Here is an example demonstrating the override system.
{
"@3sln/deck": {
"title": "My Awesome Project",
"importMap": {
"imports": {
"my-lib": "/node_modules/my-lib/index.js"
}
},
"dev": {
"title": "My Awesome Project (DEV)"
},
"build": {
"outDir": "dist/docs",
"pick": {
"../node_modules/my-lib/dist": "lib/my-lib"
},
"importMap": {
"imports": {
"my-lib": "/lib/my-lib/index.js"
}
}
}
}
}
npm run dev: The title will be My Awesome Project (DEV) and my-lib will resolve to /node_modules/my-lib/index.js.npm run build: The title will be My Awesome Project, the output will go to dist/docs, and the importMap will be overridden to point my-lib to the locally copied version at /lib/my-lib/index.js.A deck is a single-page application: every card lives behind a ?c= query parameter and its body arrives by fetch. Anything that does not run JavaScript sees one nearly empty page. So deck-build also writes the content down where it can be read directly:
/llms.txt — the llmstxt.org convention. The deck's title, its description, and one line per card linking that card's Markdown file. This is the best starting point for an agent./agents.md and /agents.html — every card concatenated into one document, with the source of each live demo inlined where its <deck-demo> tag was./robots.txt — allows everything, and names the sitemap when url is set./sitemap.xml — the root plus the ?c= URL that opens each card. Written only when url is set.The generated index.html also carries <link rel="alternate"> tags pointing at /agents.md and /llms.txt, so an agent that starts at the page still finds them.
Deck's design is based on a few key architectural choices.
Deck stores documentation content in a client-side database (IndexedDB). The initial index.html file contains a manifest of all card paths along with a hash of their content.
Upon loading, the application compares this manifest with the data already stored in its IndexedDB, and fetches only the cards that are new or have changed. Nothing blocks on the whole deck arriving: the shell renders immediately, and cards fill in behind it.
Assets that are not cards, such as the JavaScript files for <deck-demo> elements, are not pre-fetched. They are fetched and cached by the service worker only when a user views them for the first time.
A published deck ships a search index built at publish time. The browser downloads it before any card, which is what lets the very first search cover the whole deck rather than the fraction of it that has finished loading. Because the index carries each card's title and summary, results render immediately, whether or not their bodies have arrived.
A dev server has no precompiled index — the deck is being edited, and an index built a moment ago already describes the previous version. There, search is answered from the IndexedDB index of whatever has been loaded, which is the right trade when the content changes every few seconds.
Which card arrives first matters more than how many arrive per second. A published deck queues every card at once and lets priority decide the order:
?c=, so a deep link opens immediately rather than after the rest of the deck has drained;Steps 2–4 are priorities on one queue, not phases. A search typed while the backlog is still draining moves its matches to the front of that queue in place, so clicking a result opens it instead of showing a spinner.
<deck-demo> ElementThe <deck-demo> custom element is used to embed live demos in Markdown files. Its src attribute points to a JavaScript file.
The script is executed within the element and is provided with a driver API. This API allows the demo to render content and manage its own state, keeping it isolated from the main Deck application and other demos.
In development the dev server serves two generated modules per demo: one that re-runs the demo when its file changes, and one that carries its source text for the Source panel. In a published deck the demo is bundled ahead of time, and the Source panel still shows the original file rather than the bundle.
Deck has two modes of operation:
Development: deck-dev serves a deck with no bundler and no config. Plugins for Vite (@3sln/deck/vite-plugin), @web/dev-server (@3sln/deck/wds-plugin) and webpack-dev-server / Rspack (@3sln/deck/webpack-plugin) do the same job inside a dev server you already run — and the middleware behind them is plain connect, so any Node server can host a deck. Deck's own client app is pre-bundled either way, so the host runtime is never asked to resolve Deck's own dependencies.
Production (deck-build): Creates a production-ready static site. It bundles the Deck application and every demo module with esbuild, copies the project's documentation files, precompiles the search index, writes agents.md and agents.html, and generates a static index.html for deployment.