StoryLite is a lightweight, Vite-powered alternative to Storybook for component stories in HTML, React, Svelte, Vue, and Solid. It gives projects a focused story workflow with a managed app shell, isolated preview iframe, story controls, static output, and optional framework renderer adapters.
Use it when you want story-driven component previews without the full Storybook addon platform or configuration surface. Start with HTML or web components, then add framework adapters only where your project needs them.
storylite dev, storylite build, and storylite preview.html and web-components.args, argTypes, controls, and per-story parameters.Install StoryLite in the package that owns your stories:
pnpm add -D @storylite/storylite
Add a framework adapter only when you need one:
pnpm add -D @storylite/renderer-react
pnpm add -D @storylite/renderer-svelte
pnpm add -D @storylite/renderer-vue
pnpm add -D @storylite/renderer-solid
Add scripts:
{
"scripts": {
"storylite": "storylite dev",
"storylite:build": "storylite build",
"storylite:preview": "storylite preview"
}
}
StoryLite exposes three commands:
storylite
storylite dev
storylite build
storylite preview
Running storylite without a command prints help. --help and -h are supported globally and
after each command.
| Option | Description |
|---|---|
-h, --help |
Print CLI usage help. |
storylite devStarts the managed Vite development server.
storylite dev --port 4103 --host 127.0.0.1
| Option | Description |
|---|---|
--port <port> |
Dev server port. Defaults to 3993, or PORT when it is set. |
--host [host] |
Host to listen on. Pass without a value to expose on all hosts. |
EXPOSE_HOST=1 and EXPOSE_HOST=true also expose the dev server on all hosts.
storylite buildBuilds the static StoryLite output into dist-storylite.
storylite build --base /docs/
| Option | Description |
|---|---|
--base <path> |
Public base path for generated asset and story URLs. Defaults to ./. |
STORYLITE_BASE can also set the build base path.
storylite previewServes dist-storylite with Vite preview.
storylite preview --port 4103 --host 127.0.0.1 --base /docs/
| Option | Description |
|---|---|
--port <port> |
Preview server port. Defaults to 3993, or PORT when it is set. |
--host [host] |
Host to listen on. Defaults to exposing on all hosts for preview. |
--base <path> |
Public base path used while serving the built output. Defaults to ./. |
Create .storylite/config.ts:
import { defineConfig } from '@storylite/storylite'
export default defineConfig({
stories: ['./src/**/*.stories.ts'],
css: ['./src/styles.css'],
})
Create a story:
import type { StoryLiteMeta, StoryLiteStoryDefinition } from '@storylite/storylite'
import buttonHtml from './button.html?raw'
export default {
title: 'Components/Button',
} satisfies StoryLiteMeta
export const Primary = {
args: {
label: 'Save changes',
},
argTypes: {
label: { control: 'text' },
},
render: (args) => buttonHtml.replace('{{ label }}', String(args.label)),
} satisfies StoryLiteStoryDefinition<{ label: string }>
Run StoryLite:
pnpm storylite
Build static output:
pnpm storylite:build
storylite build writes dist-storylite/index.html plus one default-args static page per story at
dist-storylite/stories/<story-id>/index.html. Static asset URLs are relative by default so the
output can be hosted from a subpath.
StoryLite reads .storylite/config.ts first, then .storylite/config.js. Export with
defineConfig for typed authoring:
import { defineConfig } from '@storylite/storylite'
export default defineConfig({
stories: ['./src/**/*.stories.{ts,tsx}'],
css: ['./src/styles.css'],
setup: './.storylite/setup.ts',
renderers: [],
vitePlugins: [],
storyId: (_path, suggestedId) => suggestedId,
})
| Option | Description |
|---|---|
stories |
Glob patterns for story modules. Required. |
css |
Shared CSS files injected into the preview iframe and static story pages. |
setup |
Optional module exporting setupPreview(window) for preview setup. |
renderers |
Optional renderer adapters, such as react(), svelte(), vue(), or solid(). |
vitePlugins |
StoryLite-specific Vite plugins. Use this for Tailwind, aliases, and other project transforms. |
storyId(path, suggestedId) |
Optional story ID rewrite hook. |
Story IDs strip the leading src/ segment by default. For example,
src/components/button.stories.ts becomes components-button--primary. Duplicate IDs are shown in
the dev UI and fail storylite build.
Configured css files are processed by Vite as ?inline, so Vite plugin transforms run before the
CSS string is injected into previews:
export default defineConfig({
css: ['./src/styles.css'],
})
Story-specific CSS can also be supplied through parameters.css:
export const Primary = {
parameters: {
css: '.button { border-radius: 8px; }',
},
}
If story-specific CSS needs Vite transforms, import it as ?inline instead of ?raw:
import css from './button.css?inline'
export const Primary = {
parameters: { css },
}
StoryLite runs an isolated Vite config for its managed app instead of merging the consuming
project's full vite.config.ts. Add StoryLite-specific Vite plugins with vitePlugins:
import { defineConfig } from '@storylite/storylite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
stories: ['./src/**/*.stories.tsx'],
css: ['./src/styles.css'],
vitePlugins: [tailwindcss()],
})
vitePlugins can also be a callback:
vitePlugins: ({ target, command, projectRoot }) => {
if (target === 'static') return []
return [tailwindcss()]
}
The callback receives:
| Field | Values |
|---|---|
target |
'manager', 'prerender', or 'static' |
command |
'serve' or 'build' |
projectRoot |
Absolute path to the consuming project |
Install Tailwind:
pnpm add -D tailwindcss @tailwindcss/vite
Configure StoryLite:
import { defineConfig } from '@storylite/storylite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
stories: ['./src/**/*.stories.tsx'],
css: ['./src/styles.css'],
vitePlugins: [tailwindcss()],
})
Add Tailwind to the configured stylesheet. When your utility classes live in story/component files,
explicitly register those files with @source:
@import 'tailwindcss';
@source './components';
StoryLite ships built-in support for html and web-components. Framework support is added with
renderer adapters so each project only installs the runtimes it uses.
import { defineConfig } from '@storylite/storylite'
import react from '@storylite/renderer-react'
export default defineConfig({
stories: ['./src/**/*.stories.tsx'],
css: ['./src/styles.css'],
renderers: [react()],
})
import svelte from '@storylite/renderer-svelte'
import vue from '@storylite/renderer-vue'
import solid from '@storylite/renderer-solid'
export default defineConfig({
renderers: [svelte(), vue(), solid()],
})
Each adapter owns its client renderer, optional static renderer, and adapter-specific Vite plugins.
Changing renderer adapters in .storylite/config.ts requires restarting storylite dev.
StoryLite supports a focused CSF-like subset:
import type { StoryLiteMeta, StoryLiteStoryDefinition } from '@storylite/storylite'
type ButtonArgs = {
label: string
variant: 'primary' | 'secondary'
disabled: boolean
}
export default {
title: 'Components/Button',
args: {
variant: 'primary',
disabled: false,
},
argTypes: {
label: { control: 'text' },
variant: { control: 'select', options: ['primary', 'secondary'] },
disabled: { control: 'boolean' },
},
parameters: {
renderer: 'html',
},
} satisfies StoryLiteMeta<ButtonArgs>
export const Primary = {
name: 'Primary',
args: {
label: 'Save changes',
},
render: (args) => `<button data-variant="${args.variant}">${args.label}</button>`,
} satisfies StoryLiteStoryDefinition<ButtonArgs>
| Field | Description |
|---|---|
title |
Story group title in the sidebar. |
component |
Optional component reference or web component tag name. |
args |
Default story args. |
argTypes |
Control metadata. |
parameters |
Default story parameters. |
| Field | Description |
|---|---|
name |
Optional display name. Defaults to the export name. |
component |
Optional story-specific component. |
args |
Args merged over default export args. |
argTypes |
Arg types merged over default export arg types. |
parameters |
Parameters merged over default export parameters. |
render(args, context) |
Story render function. |
Supported control types:
booleantextnumbercolorselectControls can be declared as a string:
argTypes: {
disabled: { control: 'boolean' },
}
Or as an object:
argTypes: {
variant: {
control: { type: 'select' },
options: ['primary', 'secondary'],
description: 'Visual treatment',
},
}
If no control is provided, StoryLite infers a simple control from the current arg value.
| Parameter | Description |
|---|---|
renderer |
Renderer name: html, web-components, or an adapter renderer such as react. |
css |
Per-story CSS string or array of strings. |
background |
Initial preview background value. |
defineCustomElements(window) |
Registers custom elements in the preview window. |
render(args, context) receives:
| Field | Description |
|---|---|
id |
Normalized story ID. |
title |
Story group title. |
name |
Story display name. |
canvas |
Canvas element where the story is mounted. |
document |
Preview document. |
window |
Preview window. |
For HTML stories, return a string, Node, or DocumentFragment. Framework adapter stories usually
use component and adapter-specific rendering instead.
Use the built-in web-components renderer when your component is a custom element:
export default {
title: 'Components/DemoButton',
component: 'demo-button',
parameters: {
renderer: 'web-components',
defineCustomElements: (window) => {
window.customElements.define('demo-button', DemoButton)
},
},
}
export const Primary = {
args: {
label: 'Save',
},
}
Web components should remain progressive enhancements: the light-DOM markup should be visible and styled before JavaScript upgrades behavior.
StoryLite's manager UI can be customized from ui:
export default defineConfig({
ui: {
brand: {
markHtml: '<span>UI</span>',
titleHtml: '<strong>Design System</strong>',
},
backgrounds: (defaults) => [...defaults, { label: 'Brand', value: '#eff6ff' }],
viewports: (defaults) =>
defaults.map((viewport) =>
viewport.icon === 'mobile' ? { ...viewport, width: 390 } : viewport,
),
css: '.brand__mark { color: var(--sl-primary); }',
},
})
| Option | Description |
|---|---|
brand.markHtml |
Trusted project HTML for the sidebar mark. |
brand.titleHtml |
Trusted project HTML for the sidebar title. |
backgrounds |
Replace or extend preview background presets. |
viewports |
Replace or extend toolbar viewport presets. |
css |
CSS injected into the StoryLite manager chrome. |
Viewport widths can be numbers or strings. Numeric widths are normalized to pixels. The built-in grid background can be tuned with preview CSS variables:
--storylite-grid-size--storylite-grid-major-size--storylite-grid-offset--storylite-grid-line-width--storylite-grid-line-color--storylite-grid-line-color-2--storylite-grid-background-colorui.toolbar adds project-defined tools to a separate toolbar group. StoryLite intentionally ships
no project-specific custom toolbar defaults.
export default defineConfig({
ui: {
toolbar: [
{
type: 'toggle',
id: 'a11y-outlines',
label: 'A11y outlines',
icon: 'accessibility',
defaultValue: false,
target: { type: 'preview-class', name: 'show-a11y-outlines' },
},
{
type: 'select',
id: 'density',
label: 'Density',
icon: 'layout',
options: [
{ label: 'Comfortable', value: 'comfortable' },
{ label: 'Compact', value: 'compact' },
],
target: { type: 'preview-class', prefix: 'density-' },
},
{
type: 'link',
id: 'repo',
label: 'Repository',
icon: 'external-link',
href: 'https://github.com/example/design-system',
target: '_blank',
rel: 'noreferrer',
},
],
},
})
Supported tools:
| Type | Description |
|---|---|
toggle |
Icon button with aria-pressed. |
select |
Icon button with a popover list of options. |
link |
Regular toolbar link. |
Supported toggle/select targets:
| Target | Description |
|---|---|
preview-class |
Applies a class to the preview body. |
preview-attribute |
Applies a data-* attribute to the preview body. |
manager-attribute |
Applies a data-* attribute to the StoryLite manager root. |
url-query |
Mirrors the value into the URL query string. |
url-hash |
Mirrors the value into the hash query string. |
Toggle/select values persist in storylite:toolbar-settings.customTools unless persist: false is
set. Stored values are validated against the current config at startup, so removed tools and invalid
select values fall back cleanly.
Supported built-in icon names:
;'accessibility' |
'bug' |
'external-link' |
'eye' |
'flag' |
'globe' |
'info' |
'layout' |
'monitor' |
'moon' |
'paint-bucket' |
'settings' |
'sun' |
'zap'
ui.menuLinks customizes the app menu opened from the sidebar. The default menu contains only
About:
const defaultLinks = [
{
id: 'about',
label: 'About',
href: 'https://github.com/itsjavi/storylite',
icon: 'info',
target: '_blank',
rel: 'noreferrer',
},
]
Extend or replace it with (defaultLinks) => newLinks:
export default defineConfig({
ui: {
menuLinks: (defaultLinks) => [
...defaultLinks,
{
id: 'docs',
label: 'Docs',
icon: 'external-link',
href: '/docs',
},
],
},
})
Menu links are regular links. They do not run project JavaScript.
StoryLite supports both config hooks and files in .storylite/.
Manager hooks customize the StoryLite chrome document:
export default defineConfig({
managerHtmlAttrs: (defaults) => ({ ...defaults, lang: 'en', 'data-library': 'components' }),
managerBodyAttrs: { 'data-shell': 'storylite' },
managerHead: '<meta name="storylite-project" content="component-library">',
managerBodyStart: '<script>window.beforeStoryLite = true</script>',
managerBodyEnd: '<script>window.afterStoryLite = true</script>',
})
Convention files:
.storylite/manager-head.html.storylite/manager-body-start.html.storylite/manager-body-end.html.storylite/manager.css.storylite/ui.cssmanager.css and ui.css are injected into the manager chrome.
Preview hooks customize the isolated iframe document:
export default defineConfig({
previewHtmlAttrs: (defaults) => ({ ...defaults, lang: 'en', 'data-preview': 'component' }),
previewBodyAttrs: { 'data-theme-root': true },
previewHead: '<meta name="storylite-preview" content="component">',
previewBodyStart: '<div data-preview-start></div>',
previewBodyEnd: '<script>window.previewReady = true</script>',
})
Convention files:
.storylite/preview-head.html.storylite/preview-body.html.storylite/preview-body-start.html.storylite/preview-body-end.htmlpreview-body.html is a backwards-compatible alias for preview-body-end.html.
HTML fragments can be strings or callbacks that receive the convention-file default:
previewHead: (defaultHead) => `${defaultHead}<meta name="extra" content="true">`
HTML fragments are trusted project source. Do not feed untrusted user content into these hooks.
Add .storylite/home.md to render a Markdown welcome page at #/:
---
title: Component Library
description: Component stories
---
# Component Library
Use the sidebar to browse components.
The home page is compiled with mdsvex. When present, it replaces the default initial story canvas and is included in the static build's prerendered manager shell. When absent, StoryLite starts on the first story and hides the toolbar home button.
StoryLite uses hash routes:
| Route | Description |
|---|---|
#/ |
Home page when .storylite/home.md exists. |
#/story/:storyId |
Normal isolated iframe preview. |
#/canvas/:storyId |
Direct non-iframe rendering in the manager document. |
In built output, the toolbar's open-canvas link points to the static page at
./stories/<story-id>/.
Press / to focus story search.
storylite build performs three jobs:
dist-storylite.index.html.dist-storylite/stories/<story-id>/index.html.Static pages include configured preview HTML hooks, shared CSS, story CSS, and the renderer's static HTML when the renderer supports static rendering.
play, loaders, decorators, docs/autodocs, actions, and addon APIs are not part of the current
story format.vite.config.ts. Add StoryLite-specific
plugins through vitePlugins.@source directives when utilities live in story/component files
and CSS is processed through StoryLite's configured css pipeline.storylite dev restart after certain component edits.storylite dev.