Install Setup

Just add this single script tag to your header which auto-injects the CSS and includes all the JS needed. <script src="dist/volty-embed.js"></script>

To install via npm, run npm i volty

Buttons

All button variants use --vt-color-brand tokens for colors, so they automatically update when the brand or theme changes. The focus ring uses color-mix() to create a translucent ring that always matches the current brand color.

Style variants

Size variants

Outline + size

Disabled states

Icon buttons

Buttons with icons

html
<!-- Style variants -->
<button class="vt-btn">Primary</button>
<button class="vt-btn vt-btn--outline">Outline</button>
<button class="vt-btn vt-btn--ghost">Ghost</button>
<button class="vt-btn vt-btn--surface">Surface</button>
<button class="vt-btn vt-btn--danger">Danger</button>

<!-- Size variants -->
<button class="vt-btn vt-btn--sm">Small</button>
<button class="vt-btn">Default</button>
<button class="vt-btn vt-btn--lg">Large</button>

<!-- Icon-only button -->
<button class="vt-btn vt-btn--icon" title="Add">
  <svg ...></svg>
</button>
<button class="vt-btn vt-btn--icon vt-btn--outline"><svg ...></svg></button>

<!-- Button with leading/trailing icon -->
<button class="vt-btn">
  <svg ...></svg>
  New Project
</button>

<!-- Disabled state -->
<button class="vt-btn" disabled>Primary</button>
<button class="vt-btn vt-btn--outline" disabled>Outline</button>

Cards

Cards use container-type: inline-size, making them self-aware of their own width. The footer layout switches from left-aligned to right-aligned automatically when the card is wider than 420px — no media queries needed.

Basic Card

The default card style — clean border, overlay surface background. Use this for most content.

Card body content sits here. It inherits --vt-color-text automatically and reacts to theme changes.

Raised Card

Shadow is computed from --vt-color-text via color-mix() so it works in both light and dark.

The box-shadow uses color-mix(in oklch, var(--vt-color-text) 8%, transparent) — always the right shade.

Interactive Card

Hover to see the brand-colored glow effect — driven entirely by CSS tokens.

The hover glow is color-mix(in oklch, var(--vt-color-brand) 15%, transparent) — automatically updates when you change brand color.

Card with Footer

Footer buttons move to the right when this card exceeds 420px width.

This uses @container card (min-width: 420px) — the card responds to its own width, not the page width.

Wide Card — Container Query Active

At this width (>420px) the footer aligns right. Resize the browser to see it shift.

Container Query

The layout decision is made by the card itself, not by the viewport. Narrow this card's container and the footer reverts to left-aligned — without touching any CSS or JavaScript.

html
<!-- Basic card -->
<div class="vt-card">
  <div class="vt-card__header">
    <h3 class="vt-card__title">Title</h3>
    <p class="vt-card__description">Description</p>
  </div>
  <div class="vt-card__body">Content here.</div>
</div>

<!-- Raised (box shadow) -->
<div class="vt-card vt-card--raised">...</div>

<!-- Interactive (hover glow from brand token) -->
<div class="vt-card vt-card--interactive">...</div>

<!-- Card with footer — footer aligns right above 420px container (container query) -->
<div class="vt-card">
  <div class="vt-card__header">
    <h3 class="vt-card__title">Title</h3>
  </div>
  <div class="vt-card__body">...</div>
  <div class="vt-card__footer">
    <button class="vt-btn vt-btn--surface vt-btn--sm">Cancel</button>
    <button class="vt-btn vt-btn--sm">Confirm</button>
  </div>
</div>

Badges

Badges come in subtle (default), solid, and semantic variants. Subtle colors use color-mix() at 15% opacity so they always feel on-brand without overpowering surrounding content.

Default (brand subtle)

Default Beta New Pro

Solid

Solid Featured Active

Surface (neutral)

Draft Archived v0.1.3

Semantic status

Success Warning Danger Deployed Degraded Incident

In context

API Gateway

Healthy

Production endpoint serving 2.4M requests/day

Image Processor

Degraded

Async image resizing and optimization pipeline

html
<!-- Default (brand subtle) -->
<span class="vt-badge">Beta</span>

<!-- Solid -->
<span class="vt-badge vt-badge--solid">Active</span>

<!-- Surface (neutral) -->
<span class="vt-badge vt-badge--surface">Draft</span>
<span class="vt-badge vt-badge--surface">v0.1.3</span>

<!-- Semantic status -->
<span class="vt-badge vt-badge--success">Deployed</span>
<span class="vt-badge vt-badge--warning">Degraded</span>
<span class="vt-badge vt-badge--danger">Incident</span>

<!-- With dot indicator -->
<span class="vt-badge vt-badge--success">
  <svg width="8" height="8" viewBox="0 0 8 8">
    <circle cx="4" cy="4" r="4" fill="currentColor"/>
  </svg>
  Healthy
</span>

Shorthand Commands

Drop any Volty component into a page without writing the markup by hand. Three equivalent syntaxes — pick whichever fits your workflow.

html — custom element
<!-- Navigation bar -->
<vt-nav logo="My App" version="v1.0"
  links='[{"href":"/","label":"Home"},{"href":"/docs","label":"Docs"},{"href":"/about","label":"About"}]'>
</vt-nav>

<!-- Card -->
<vt-card title="Card title" description="Subtitle">Body content here.</vt-card>
<vt-card title="Raised" variant="raised" footer='<button class="vt-btn vt-btn--sm">Action</button>'></vt-card>

<!-- Alert -->
<vt-alert variant="success" title="Deployed">Version 2.0 is live.</vt-alert>
<vt-alert variant="danger" title="Error" dismissible="false">Payment failed.</vt-alert>

<!-- Badge & Button -->
<vt-badge variant="success">Active</vt-badge>
<vt-button variant="outline" size="sm">Outline</vt-button>

<!-- Modal -->
<vt-modal id="confirm" title="Confirm" confirm="Yes, delete" cancel="Cancel" variant="danger">
  This cannot be undone.
</vt-modal>

<!-- Toast trigger -->
<vt-toast-trigger variant="success" message="Saved!" title="Done">Save changes</vt-toast-trigger>
html — data attribute
<!-- Works on any existing element — useful for CMS output, templates -->
<div data-vt-insert="nav"
     data-logo="My App"
     data-links='[{"href":"/","label":"Home"}]'></div>

<div data-vt-insert="alert"
     data-variant="warning"
     data-title="Heads up">Rate limit at 85%.</div>

<div data-vt-insert="card"
     data-title="Hello"
     data-description="Subtitle">Body content</div>
js — api
// Render into a target element (replaces innerHTML)
Volty.insert('nav', document.querySelector('header'), {
  logo: 'My App',
  links: [
    { href: '/', label: 'Home' },
    { href: '/docs', label: 'Docs', active: true },
    { href: 'https://github.com/org/repo', label: 'GitHub', external: true },
  ],
});

// Get raw HTML string from any template
const html = Volty.templates.card({
  title: 'Hello',
  description: 'World',
  content: '<p>Body text</p>',
  variant: 'raised',
});

// Re-scan for data-vt-insert after dynamic content is added
Volty.processInserts(document.getElementById('container'));

Form Fields

Input fields use color-mix() for the hover border (a blend of the current border and brand color) and a brand-colored focus ring via --vt-color-brand-ring. Error states swap the ring color to --vt-color-danger.

We'll never share your email with anyone.
Used on your public profile.
Username can only contain letters, numbers, and underscores.
Contact support to rotate your API key.
Press Cmd+K to focus from anywhere.
html
<!-- Default field -->
<div class="vt-field">
  <label class="vt-label" for="email">Email address</label>
  <input class="vt-input" id="email" type="email" placeholder="you@example.com">
  <span class="vt-hint">We'll never share your email.</span>
</div>

<!-- Required field -->
<div class="vt-field">
  <label class="vt-label vt-label--required" for="name">Full name</label>
  <input class="vt-input" id="name" type="text" placeholder="Jane Smith">
</div>

<!-- Error state -->
<div class="vt-field">
  <label class="vt-label" for="username">Username</label>
  <input class="vt-input vt-input--error" id="username" type="text" value="jane!smith">
  <span class="vt-hint vt-hint--error">Only letters, numbers, and underscores.</span>
</div>

<!-- Disabled -->
<div class="vt-field">
  <label class="vt-label" for="api-key">API Key</label>
  <input class="vt-input" id="api-key" type="text" value="sk-••••••••••••••••" disabled>
  <span class="vt-hint">Contact support to rotate your key.</span>
</div>

Switches

Switches use :has(input:checked) to react to checkbox state entirely in CSS — no JavaScript event listeners required. The thumb animation uses a spring cubic-bezier for a satisfying bounce.

Functional examples

Notification Preferences

Choose how you want to be notified about activity on your account.

html
<label class="vt-switch">
  <input type="checkbox" checked>
  <span class="vt-switch__track">
    <span class="vt-switch__thumb"></span>
  </span>
  <span class="vt-switch__label">Email notifications</span>
</label>

<!-- Unchecked -->
<label class="vt-switch">
  <input type="checkbox">
  <span class="vt-switch__track"><span class="vt-switch__thumb"></span></span>
  <span class="vt-switch__label">SMS alerts</span>
</label>

<!-- Disabled — :has(input:checked) drives state in CSS, no JS needed -->
<label class="vt-switch">
  <input type="checkbox" disabled>
  <span class="vt-switch__track"><span class="vt-switch__thumb"></span></span>
  <span class="vt-switch__label">Account activity (required)</span>
</label>

Alerts

Inline banners for feedback and status. Uses CSS :has() to collapse the grid columns when an icon or dismiss button is absent — no modifier class needed.

Semantic variants

Solid variant

html
<!-- Default (info) -->
<div class="vt-alert" role="alert">
  <div class="vt-alert__icon"><svg ...></svg></div>
  <div class="vt-alert__body">
    <p class="vt-alert__title">Info</p>
    <p class="vt-alert__desc">Your session will expire in 15 minutes.</p>
  </div>
  <button class="vt-alert__dismiss" aria-label="Dismiss"
    onclick="this.closest('.vt-alert').remove()"><svg ...></svg></button>
</div>

<!-- Semantic variants -->
<div class="vt-alert vt-alert--success" role="alert">...</div>
<div class="vt-alert vt-alert--warning" role="alert">...</div>
<div class="vt-alert vt-alert--danger"  role="alert">...</div>

<!-- Solid variant (higher contrast) -->
<div class="vt-alert vt-alert--solid vt-alert--success" role="alert">...</div>
<div class="vt-alert vt-alert--solid vt-alert--danger"  role="alert">...</div>

Tooltips

Two paths: data-tooltip for zero-markup single-line tips, and .vt-tooltip with CSS Anchor Positioning for rich multi-line content with automatic placement fallbacks.

data-tooltip — zero markup

Placement variants

html
<!-- data-tooltip — zero markup, pure CSS, no JS -->
<button class="vt-btn" data-tooltip="Saves your current progress">Save</button>
<button class="vt-btn" data-tooltip="Permanently deletes this item"
  data-tooltip-placement="bottom">Delete</button>

<!-- Placement variants: top (default) | bottom | left | right -->
<button data-tooltip="Appears above"  data-tooltip-placement="top">Top</button>
<button data-tooltip="Appears below"  data-tooltip-placement="bottom">Bottom</button>
<button data-tooltip="Appears left"   data-tooltip-placement="left">Left</button>
<button data-tooltip="Appears right"  data-tooltip-placement="right">Right</button>

Select

Styled native <select> — accessible by default, no JS. The chevron is pure CSS via clip-path. Wrap in .vt-field for label and hint layout.

Choose the region closest to your users.
Small size variant.
A role is required.
Contact an admin to change environments.
html
<!-- Wrap in .vt-field for label + hint layout -->
<div class="vt-field">
  <label class="vt-label" for="region">Region</label>
  <div class="vt-select">
    <select id="region">
      <option>US East (N. Virginia)</option>
      <option>EU (Ireland)</option>
    </select>
  </div>
  <span class="vt-field-hint">Closest to your users.</span>
</div>

<!-- Small size -->
<div class="vt-select vt-select--sm"><select>...</select></div>

<!-- Error state -->
<div class="vt-select vt-select--error">
  <select><option value="">Select a role…</option></select>
</div>
<span class="vt-field-error">A role is required.</span>

<!-- Disabled -->
<div class="vt-select">
  <select disabled><option>Production</option></select>
</div>

Modal

Built on the native <dialog> element with showModal(). Entry and exit animations use @starting-style — no JavaScript animation required. Click the backdrop to close.

Default modal

Publish changes

Your changes will go live immediately and be visible to all users. This action can be undone from the deployment history.

Delete project

This will permanently delete my-app-prod and all its data. This action cannot be undone.

Rename file

html
<!-- Trigger button -->
<button class="vt-btn" onclick="document.getElementById('my-modal').showModal()">
  Open modal
</button>

<!-- Default modal — entry/exit animation via @starting-style, no JS -->
<dialog class="vt-modal" id="my-modal">
  <div class="vt-modal__header">
    <h2 class="vt-modal__title">Publish changes</h2>
    <button class="vt-modal__close" aria-label="Close"
      onclick="this.closest('dialog').close()"><svg ...></svg></button>
  </div>
  <div class="vt-modal__body">
    <p>Your changes will go live immediately.</p>
  </div>
  <div class="vt-modal__footer">
    <button class="vt-btn vt-btn--surface" onclick="this.closest('dialog').close()">Cancel</button>
    <button class="vt-btn" onclick="this.closest('dialog').close()">Publish</button>
  </div>
</dialog>

<!-- Size variants -->
<dialog class="vt-modal vt-modal--sm" id="rename">...</dialog>
<dialog class="vt-modal vt-modal--lg" id="preview">...</dialog>

<!-- Danger variant -->
<dialog class="vt-modal vt-modal--danger" id="delete">...</dialog>
js — close on backdrop click
document.querySelectorAll('.vt-modal').forEach(modal => {
  modal.addEventListener('click', e => {
    if (e.target === modal) modal.close();
  });
});

Toasts

Notification toasts created programmatically via Volty.toast(). Entry animation via @starting-style, countdown bar via CSS animation, auto-dismiss after 4 seconds by default.

Try them

js
// Simple message
Volty.toast('Copied to clipboard.')

// With options
Volty.toast({ message: 'Changes saved.', variant: 'success', title: 'Saved' })
Volty.toast({ message: 'Rate limit at 85%.', variant: 'warning', title: 'Warning' })
Volty.toast({ message: 'Could not connect.', variant: 'danger', title: 'Error' })

// Persistent — stays until user dismisses (duration: 0)
Volty.toast({ message: 'Action required.', title: 'Notice', duration: 0 })

// Custom duration in ms (default: 4000)
Volty.toast({ message: 'Deploying…', duration: 8000 })

Dark Island

Apply data-theme="dark" to any element to make it render in dark mode — regardless of the page's current theme. Components inside inherit the scoped tokens automatically. No JavaScript, no class toggling.

data-theme="dark" — always dark regardless of page theme

Dark Card

Active

This card is always dark. The parent div has data-theme="dark" so all components inside inherit dark tokens.

Default Solid Success Warning Danger

Nested Card

Nested inside the dark island — still dark.

html
<!-- Any element with data-theme="dark" always renders dark -->
<div data-theme="dark">
  <div class="vt-card">...</div>
  <button class="vt-btn">Always dark</button>
  <span class="vt-badge vt-badge--success">Active</span>
</div>

<!-- Pin a section to always-light -->
<div data-theme="light">...</div>
js
// Override theme globally
Volty.setTheme('dark')    // force dark
Volty.setTheme('light')   // force light
Volty.setTheme('system')  // follow OS preference (default)

Volty.getTheme()          // → 'light' | 'dark' | 'system'
Volty.getSystemTheme()    // → 'light' | 'dark' (actual resolved value)

Brand Theming

Apply data-brand to any container to scope a different brand color. The entire color scale — hover states, focus rings, badge tints — all regenerate automatically via color-mix(). One token powers everything.

Blue (default)

Badge

Violet

Badge

Emerald

Badge

Rose

Badge

Amber

Badge

Cyan

Badge
html
<!-- Scoped brand on any container — entire token scale regenerates -->
<section data-brand="violet">...</section>
<section data-brand="emerald">...</section>
<section data-brand="rose">...</section>
<section data-brand="amber">...</section>
<section data-brand="cyan">...</section>
js
// Apply brand globally
Volty.setBrand('violet')
Volty.setBrand('emerald')
Volty.setBrand(null)    // reset to default

Volty.getBrand()        // → 'violet' | null
css — custom brand color
/* Override --vt-color-brand — entire scale (50–950) regenerates via color-mix() */
:root {
  --vt-color-brand: oklch(58% 0.21 310);
}

Color Scale

The brand scale from 50 to 950 is generated entirely via color-mix(in oklch, ...) from a single --vt-color-brand token. Change the brand color above — every swatch updates instantly.

Brand scale (auto-generated)

brand-50
brand-100
brand-200
brand-300
brand-400
brand-500 ←
brand-600
brand-700
brand-800
brand-900
brand-950

Semantic surface tokens

surface
surface-raised
surface-overlay
text
text-muted
border

Semantic status tokens

success
success-subtle
warning
warning-subtle
danger
danger-subtle
info
info-subtle
css
/* Surface tokens */
--vt-color-surface          /* page background */
--vt-color-surface-raised   /* elevated (sidebars, inputs) */
--vt-color-surface-overlay  /* highest (cards, modals) */

/* Text */
--vt-color-text
--vt-color-text-muted

/* Brand — entire scale (50–950) auto-generated via color-mix() */
--vt-color-brand
--vt-color-brand-subtle
--vt-color-brand-hover
--vt-color-brand-text

/* Semantic */
--vt-color-success  /  --vt-color-success-subtle
--vt-color-warning  /  --vt-color-warning-subtle
--vt-color-danger   /  --vt-color-danger-subtle
--vt-color-info     /  --vt-color-info-subtle

/* Override brand — entire scale regenerates automatically */
:root {
  --vt-color-brand: oklch(58% 0.21 310);
}

Type Scale

Each step uses clamp() with viewport-relative sizing. The type scale flows smoothly between breakpoints — resize the browser to see it adapt. All registered as @property tokens of type <length>.

text-xs The quick brown fox jumps over the lazy dog
text-sm The quick brown fox jumps over the lazy dog
text-base The quick brown fox jumps over the lazy dog
text-lg The quick brown fox jumps
text-xl The quick brown fox jumps
text-2xl The quick brown fox
text-3xl The quick brown
text-4xl The quick
text-5xl Volty
css
/* Type scale — all registered as @property <length> tokens */
--vt-text-xs    /* 12px */
--vt-text-sm    /* 14px */
--vt-text-base  /* 16px */
--vt-text-lg    /* 18px */
--vt-text-xl    /* 20px */
--vt-text-2xl   /* 24px */
--vt-text-3xl   /* 30px */
--vt-text-4xl   /* 36px */
--vt-text-5xl   /* 48px */

/* Font weight tokens */
--vt-font-weight-normal    /* 400 */
--vt-font-weight-medium    /* 500 */
--vt-font-weight-semibold  /* 600 */
--vt-font-weight-bold      /* 700 */

/* Usage example */
.my-heading {
  font-size: var(--vt-text-3xl);
  font-weight: var(--vt-font-weight-bold);
  letter-spacing: -0.02em;
  color: var(--vt-color-text);
}

Layer Cascade

Volty uses @layer to establish a deterministic cascade order. Later layers win. This means you can override any component style without needing higher specificity — just target the right layer.

Layer Order

Declared once at the top of the stylesheet.

1 volty.reset — box-sizing, margins
2 volty.tokens — CSS custom properties
3 volty.base — html, body defaults
4 volty.components — button, card, etc.
5 volty.utilities — your overrides go here

Why @property matters

Typed tokens unlock animation and proper fallbacks.

Color tokens can animate

Because --vt-color-brand is typed as <color>, CSS can interpolate between values — enabling smooth theme transitions.

Proper initial values

Unset tokens fall back to their initial-value rather than an empty string, preventing broken layouts.

Type validation

Setting a <length> token to a color value is silently ignored — no cascading breakage.

css
/* @layer order — declared once, deterministic cascade */
@layer
  volty.reset,       /* box-sizing, margin resets */
  volty.tokens,      /* @property custom properties */
  volty.base,        /* html, body defaults */
  volty.components,  /* .vt-btn, .vt-card, etc. */
  volty.utilities;   /* your overrides — always win */

/* Override any component without specificity battles */
@layer volty.utilities {
  .vt-btn { border-radius: 2px; }
}

/* @property registration (44 tokens total) */
@property --vt-color-brand {
  syntax: '<color>';
  inherits: true;
  initial-value: oklch(55% 0.2 250);
}

@property Token Transitions

Because every color token is registered as a typed <color> via @property, CSS can interpolate between values. Volty puts transition: --vt-color-* 350ms ease on :root — so every element on the page inherits the animation with zero component-level transition rules. No other library ships this. Try it — click a brand or theme above and watch the swatches below fade.

What you're seeing: These swatches use raw CSS custom property values — background: var(--vt-color-brand) etc. — with no transition rule of their own. They animate because :root transitions the token itself. Untyped custom properties cannot do this — the browser would snap instantly since it can't interpolate an untyped string. @property is what makes this possible.
--vt-color-brand
--vt-color-brand-hover
--vt-color-brand-subtle
--vt-color-surface
--vt-color-surface-raised
--vt-color-surface-overlay
--vt-color-text
--vt-color-text-muted
--vt-color-border

Full UI — changes animate because tokens do

Live Token Demo

Active

Switch the brand or theme — every color here fades in 350ms. No JS animation, no requestAnimationFrame.

Default Solid Success Warning Danger

brand-50 → brand-950 — all from one token, all animated

css
/* Volty registers all color tokens as typed <color> via @property */
@property --vt-color-brand {
  syntax: '<color>';
  inherits: true;
  initial-value: oklch(55% 0.2 250);
}

/* :root transitions the token itself — every element inherits the animation */
:root {
  transition:
    --vt-color-brand          350ms ease,
    --vt-color-surface        350ms ease,
    --vt-color-surface-raised 350ms ease,
    --vt-color-text           350ms ease;
}

/* No transition rule needed on components — they animate for free */
.vt-btn {
  background: var(--vt-color-brand); /* animates when brand changes */
  color: var(--vt-color-brand-text);
}
js
// Each call triggers a smooth 350ms color fade across the entire page
Volty.setBrand('violet')   // → all brand tokens animate to violet
Volty.setTheme('dark')     // → all surface + text tokens animate to dark values

Real Container Queries

Components adapt to their container's width, not the viewport. Drag the handles below to resize — the components respond to their own available space. No viewport media queries, no JavaScript resize observers.

Button — expands to full width below 280px container

container: ~260px → buttons go full width

container: ~400px → buttons are inline

Card — footer aligns right above 420px container

container: ~320px → footer stacks

Narrow card

Footer buttons stack to the start.

container: ~500px → footer aligns right

Wide card

Footer buttons align to the end.

Field — inline layout above 400px container

container: ~280px → label stacks above

container: ~500px → label inline with input

css — how it works
/* Components use container-type — they respond to their own container, not viewport */
.vt-card {
  container-type: inline-size;
  container-name: card;
}

/* Footer aligns right above 420px — no media query needed */
@container card (min-width: 420px) {
  .vt-card__footer { justify-content: flex-end; }
}

/* Buttons go full-width below 280px container */
@container (max-width: 280px) {
  .vt-btn { width: 100%; justify-content: center; }
}

/* Inline field layout above 400px container */
@container (min-width: 400px) {
  .vt-field--inline { flex-direction: row; align-items: center; }
}

Dark Mode Color Correctness

Most CSS libraries hardcode color-mix(... white) for subtle tints — which produces pale muddy grays in dark mode instead of dark tints. Volty mixes into --vt-color-surface so subtle colors are always correct: light tints in light mode, dark tints in dark mode.

data-theme="light" — always light

Success Warning Danger
Brand default Surface

subtle = color-mix(brand, surface-light) ✓

data-theme="dark" — always dark

Success Warning Danger
Brand default Surface

subtle = color-mix(brand, surface-dark) ✓

What mixing with white looks like in dark mode (the broken approach)

dark mode — wrong: color-mix(..., white)

Success — pale & washed out Danger — pale & washed out

dark mode — correct: color-mix(..., surface)

Success — rich dark tint Danger — rich dark tint
css
/* ❌ Broken approach — mixing into white produces pale muddy tints in dark mode */
background: color-mix(in oklch, var(--vt-color-success) 15%, white);

/* ✅ Volty's approach — mix into surface so dark mode gets rich dark tints */
background: color-mix(in oklch, var(--vt-color-success) 15%, var(--vt-color-surface));

/* This is how all subtle tokens are defined in Volty */
--vt-color-success-subtle: color-mix(
  in oklch,
  var(--vt-color-success) 15%,
  var(--vt-color-surface)   /* dark surface in dark mode → rich dark tint ✓ */
);

/* Usage — tokens adapt automatically in both modes */
.vt-badge--success {
  background: var(--vt-color-success-subtle);
  color: var(--vt-color-success);
}

Shadow DOM — tokens for free

CSS custom properties don't normally cross shadow boundaries. But @property with inherits: true makes the browser treat Volty tokens exactly like built-in inherited properties (color, font-size) — they pass through automatically. No CSS injection. No ::part(). Nothing.

Live Shadow DOM component

The card below lives inside a real Shadow Root. Change the brand color or theme above — it updates instantly because the tokens inherit across the boundary.

Why it works

All 44 Volty tokens are registered like this:

@property --vt-color-brand {
  syntax: "<color>";
  inherits: true/* ← this is the key */
  initial-value: oklch(55% 0.2 250);
}

inherits: true is what no other library does. It registers the property as an inherited CSS property — which the browser automatically propagates into shadow trees from the host element.

Tokens that cross the boundary

--vt-color-brand  <color>
--vt-color-surface  <color>
--vt-color-text  <color>
--vt-color-danger  <color>
--vt-color-success  <color>
--vt-color-border  <color>
--vt-space-4  <length> 16px
--vt-radius-*  <length>
--vt-duration-*  <time>

JS API

// Tokens work automatically. For component classes too:
const shadow = Volty.createShadow(hostElement);
shadow.innerHTML = `<div class="vt-card">...</div>`;
// Scoped token overrides cascade into shadow children:
Volty.setTokens(hostElement, {
  '--vt-color-brand': 'oklch(55% 0.22 10)'
});
js
// Attach shadow root — tokens flow in automatically via @property inherits: true
const shadow = Volty.createShadow(hostElement);
shadow.innerHTML = `<div class="vt-card">
  <div class="vt-card__header">
    <h3 class="vt-card__title">Shadow card</h3>
  </div>
</div>`;

// Adopt component styles into an existing shadow root
Volty.adoptStyles(existingShadowRoot);

// Scoped token override — cascades into all shadow children automatically
Volty.setTokens(hostElement, {
  '--vt-color-brand': 'oklch(55% 0.22 10)',
});
js — web component example
class MyCard extends HTMLElement {
  connectedCallback() {
    const shadow = Volty.createShadow(this);
    shadow.innerHTML = `
      <div class="vt-card vt-card--raised">
        <div class="vt-card__header">
          <h3 class="vt-card__title"><slot name="title"></slot></h3>
        </div>
        <div class="vt-card__body"><slot></slot></div>
      </div>`;
  }
}
customElements.define('my-card', MyCard);

// Usage — all Volty tokens flow in automatically
// <my-card>
//   <span slot="title">Hello from Shadow DOM</span>
//   Body content — tokens cross the boundary for free.
// </my-card>

Machine-readable by design

@property gives every token a declared type, a fallback, and an inheritance contract. That's a schema. Volty generates volty.schema.json and llms.txt at build time — so AI tools can reason about your design system with the same precision the browser uses.

volty.schema.json

All 44 tokens with their syntax, initial-value, and group. All 11 components with modifiers, elements, and usage notes. Auto-generated from @property declarations on every build — never out of sync.

View schema

llms.txt

7.7 kB compact reference — the entire Volty API in a format that fits in any context window. Paste it into Cursor, Claude, or Copilot to get accurate component output with no hallucinated class names.

View llms.txt

Typed tokens = browser validation

If an AI generates --vt-color-brand: 16px, the browser rejects it and falls back to initial-value. The type contract is enforced at parse time — not just in documentation.

Unique to @property

8 code-generation rules baked into llms.txt

All Volty classes use the .vt- prefix.
Token values must match their registered syntax — <color>, <length>, or <time>.
Use semantic tokens (--vt-color-danger, not hardcoded oklch values).
Modals use native <dialog> + el.showModal(). Never div-based modals.
Toasts are created with Volty.toast() — never hand-write .vt-toast HTML.
Add container-type: inline-size to parents that should trigger responsive behavior.
For form fields: wrap input/select in .vt-field with a .vt-label sibling.
All @property tokens have initial-value fallbacks — missing token references render correctly.