Spec
The decisions every Stisla implementation agrees on. Names, anatomy, states, and scales that stay stable across ports.
What this page is
Stisla is a design specification. The Introduction covers why this is the shape. This page covers what is in it. The contract every implementation honors for the components it ships. The vanilla bundle is the first implementation, and the React + Base UI and Vue ports come against the same surface. Coverage varies per port, so check the matrix on each component table below.
The component pages (Button, Card, Dialog) document how to use the vanilla implementation. This page documents what is fixed for every implementation. Per-component CSS variables, default values, and Sass internals are implementation detail and live on the component pages.
Coverage
The spec describes the full design language, and each implementation ships a subset of it. CSS and HTTP work the same way. The spec catalogs features, the implementations cover what they cover, and a matrix tells you what to expect on a given stack.
Two guardrails keep the spec honest.
- A component lands here only when at least one implementation has committed to shipping it. The spec is not a wishlist.
- Every component row carries a coverage cell per port, so a reader on any stack can tell at a glance what they actually get.
The anatomy tables below use Ships for present in the current release, Planned for committed but not yet built, and a blank cell for components that are out of scope on that port. Token, state, scale, and theming surfaces above are foundational and apply equally to every implementation. Only component coverage varies.
Tokens
The token surface is flat. Every component reads from --st-* via var(), and every override flows through hover, active, and focus because state derivations run at runtime through color-mix(in oklch, …). The names are the spec. Values are implementation defaults. See Customization for the shipped ones and how to override them.
Intent
Five paired tokens for semantic color. Each pairs a base with a foreground so text contrast survives a base swap.
| Token | Pair | Use |
|---|---|---|
--st-primary | --st-primary-foreground | Brand color. Drives the default --st-ring and the --st-highlight tint |
--st-success | --st-success-foreground | Positive state |
--st-warning | --st-warning-foreground | Caution. Foreground stays dark across themes |
--st-danger | --st-danger-foreground | Destructive or error |
--st-info | --st-info-foreground | Informational |
Surface tier
Background, foreground, and the three surface levels for stacked panels. --st-muted-foreground is the secondary text color paired with every surface.
| Token | Use |
|---|---|
--st-background | Page background |
--st-foreground | Primary text color |
--st-surface | Default raised surface (cards, dialogs) |
--st-surface-2 | Second-tier surface stacked on --st-surface |
--st-surface-3 | Third-tier surface for the densest stacks |
--st-border | Hairline borders between surfaces |
--st-muted-foreground | Secondary text on any surface |
Interactional
Tokens that paint interactive states regardless of intent.
| Token | Use |
|---|---|
--st-neutral | Rest fill for filled-neutral controls. Paired with --st-neutral-foreground |
--st-accent | Hover background over neutral or transparent surfaces. Paired with --st-accent-foreground |
--st-highlight | Persistent selected or current background, soft primary tint. Paired with --st-highlight-foreground |
--st-ring | Focus ring color. Defaults to --st-primary so brand swaps repaint focus |
Geometry
| Token | Use |
|---|---|
--st-radius | Default corner radius. --st-radius-sm and --st-radius-lg derive from it so brutalist (0) cascades cleanly |
--st-shadow | Default raised-surface shadow |
--st-density | Multiplier wrapped around every component's padding and height. 1 default, 0.875 compact, 1.125 comfortable |
Type
| Token | Use |
|---|---|
--st-font-sans | Default UI font stack |
--st-font-mono | Monospace stack for code and kbd |
Per-component tokens
Every component exposes a --<block>-* surface that falls back to the global tokens above. --btn-radius falls back to --st-radius, --card-bg falls back to --st-surface, and so on. The names of those per-component tokens are part of the spec, but the defaults are set by each implementation. See the Customization section at the bottom of each component page for the catalog.
Component anatomy
Every component is a BEM block with a fixed set of element slots. A port can render those slots with any tag or framework primitive it likes, but the slot names are the contract. A user who learns Card on the vanilla port should find the same parts on the React port.
Forms
| Block | Slots | Vanilla | React + Base UI | Vue |
|---|---|---|---|---|
.checkbox | (atomic) | Ships | Planned | Planned |
.input | (atomic) | Ships | Planned | Planned |
.input-group | __text | Ships | Planned | Planned |
.radio | (atomic) | Ships | Planned | Planned |
.select | (atomic) | Ships | Planned | Planned |
.slider | (atomic, native range) | Ships | Planned | Planned |
.switch | __input, __label | Ships | Planned | Planned |
.textarea | (atomic) | Ships | Planned | Planned |
Components
| Block | Slots | Vanilla | React + Base UI | Vue |
|---|---|---|---|---|
.accordion | __item, __header, __icon, __body, __body-inner | Ships | Planned | Planned |
.alert | __heading, __description, __action, __link | Ships | Planned | Planned |
.badge | __icon | Ships | Planned | Planned |
.breadcrumb | __item | Ships | Planned | Planned |
.btn | __icon | Ships | Planned | Planned |
.button-group | (composes .btn children) | Ships | Planned | Planned |
.card | __header, __title, __subtitle, __body, __text, __footer, __image, __overlay, __link | Ships | Planned | Planned |
.collapsible | (atomic; pairs with a trigger) | Ships | Planned | Planned |
.icon-box | (atomic) | Ships | Planned | Planned |
.kbd | (atomic) | Ships | Planned | Planned |
.link | (atomic) | Ships | Planned | Planned |
.list-group | __item | Ships | Planned | Planned |
.pagination | __button, __ellipsis | Ships | Planned | Planned |
.placeholder | (atomic, paired with sizing utilities) | Ships | Planned | Planned |
.progress | __bar | Ships | Planned | Planned |
.spinner | (atomic) | Ships | Planned | Planned |
.table | __head, __body, __row | Ships | Planned | Planned |
.tabs | __list, __trigger, __panel | Ships | Planned | Planned |
.toggle | __icon | Ships | Planned | Planned |
.toggle-group | (composes .toggle children) | Ships | Planned | Planned |
Overlays
| Block | Slots | Vanilla | React + Base UI | Vue |
|---|---|---|---|---|
.dialog | __backdrop, __panel, __content, __header, __title, __body, __footer, __close | Ships | Planned | Planned |
.drawer | __backdrop, __content, __header, __title, __body, __footer, __close | Ships | Planned | Planned |
.dropdown-menu | __group, __header, __item, __icon, __indicator, __shortcut, __divider | Ships | Planned | Planned |
.popover | __title, __body, __close, __arrow | Ships | Planned | Planned |
.toast | __icon, __content, __header, __body, __timestamp, __action, __close | Ships | Planned | Planned |
.tooltip | __inner, __arrow | Ships | Planned | Planned |
Layout
| Block | Slots | Vanilla | React + Base UI | Vue |
|---|---|---|---|---|
.app-shell | __body, __main | Ships | Planned | Planned |
.navbar | __brand, __toggle, __menu, __nav, __button | Ships | Planned | Planned |
.page | __header, __title, __action, __section, __section-header, __section-title | Ships | Planned | Planned |
.sidebar | __header, __brand, __content, __footer, __menu, __group, __group-title, __group-action, __list, __item, __item-action, __button, __submenu, __caret | Ships | Planned | Planned |
States
Interactive surfaces answer to a fixed vocabulary. Implementations choose how each state paints (the tokens decide that), but the state names and the runtime hooks below are part of the contract.
Interactive states
Every interactive component supports the same six states. Pseudo-classes drive the first four, and the rest are explicit.
| State | Trigger | Notes |
|---|---|---|
| Rest | default | The base painted by the component's tokens |
| Hover | :hover | Derives at runtime via color-mix(in oklch, base 88%, black) |
| Active | :active | Derives at runtime, typically 78% over black |
| Focus | :focus-visible | Ring derives from --st-ring. Never :focus for the visible ring |
| Disabled | :disabled on form controls, aria-disabled="true" on link buttons | Tone reduces; pointer events block |
| Loading | .is-loading + aria-busy="true" | Spinner replaces or augments content. Click is blocked while applied |
Runtime state hooks
Two prefixes, split by origin. Radix-aligned states use [data-state], and Stisla-original states use .is-*. Pick one per concept and stick to it.
| Hook | Components | Meaning |
|---|---|---|
data-state="open" / "closed" | Accordion, Collapsible, Dialog, Drawer, Dropdown, Popover, Tooltip | Disclosure open or closed |
data-state="active" / "inactive" | Tabs, Toggle, Toggle group | Selected / current vs. not |
data-state="checked" | Checkbox, Radio, Switch | On state of a binary control |
.is-loading | Button, Input | Async work in flight |
.is-collapsed | Sidebar, Collapsible | Persistent collapsed state distinct from open / closed |
.is-valid / .is-invalid | Form controls | Validation result |
.is-indeterminate | Checkbox, Progress | Tri-state or unknown |
Scales
Three knobs reshape the system proportionally. Implementations expose them as global tokens, and per-component overrides ride on top.
| Knob | Token | Range |
|---|---|---|
| Radius | --st-radius | 0 (brutalist), 0.75rem (default), 1rem (soft). -sm and -lg derive multiplicatively |
| Density | --st-density | 0.875 (compact), 1 (default), 1.125 (comfortable). Wraps every padding and height value |
| Brand | --st-primary | Any OKLCH (or any color), repaints every primary-toned surface, hover, active, focus, and highlight |
Theming
Light and dark are deltas on the same token surface rather than separate themes. A port honors them by writing two blocks. A base, and a dark override scoped to [data-theme="dark"] or .dark.
| Flips per theme | Stays put |
|---|---|
Surface tier (--st-background, --st-foreground, --st-surface, --st-surface-2, --st-surface-3, --st-border, --st-muted-foreground) and the interactional pair (--st-neutral, --st-accent) | Intents (--st-primary, --st-success, --st-warning, --st-danger, --st-info) and their foregrounds. Geometry, density, and type |
Per-component -foreground pairings are mandatory. If a component introduces a new background variable, it introduces the paired foreground at the same time, so a token override never strands a text color.
Focus
One ring rule across the system. The visible ring derives from --st-ring, which defaults to --st-primary so a brand swap repaints every focus. The ring is rendered with box-shadow or outline, but not both. Only :focus-visible paints the ring. :focus on its own is invisible because it fires on mouse clicks too.
Motion
Disclosure components (Accordion, Collapsible, Dialog, Drawer, Dropdown, Popover, Toast, Tooltip) transition between data-state="open" and data-state="closed". The shape of that transition is up to the implementation. The contract is that both states are settled and addressable. prefers-reduced-motion collapses the transition to zero duration but keeps the start and end states intact.
Implementations
One spec, many implementations. Status as of 3.0.0-beta.1.
| Implementation | Status | Notes |
|---|---|---|
| Vanilla CSS + JS | Ships in 3.0.0-beta.1 | Reference implementation. Installation |
| React + Base UI | Planned | Same tokens, same class names, headless interactions via Base UI |
| Vue | Planned | Same tokens, same class names |
Every implementation reads from the same --st-* token surface and ships the same BEM class names. A page authored against one implementation should swap to another without rewriting markup.