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.

TokenPairUse
--st-primary--st-primary-foregroundBrand color. Drives the default --st-ring and the --st-highlight tint
--st-success--st-success-foregroundPositive state
--st-warning--st-warning-foregroundCaution. Foreground stays dark across themes
--st-danger--st-danger-foregroundDestructive or error
--st-info--st-info-foregroundInformational

Surface tier

Background, foreground, and the three surface levels for stacked panels. --st-muted-foreground is the secondary text color paired with every surface.

TokenUse
--st-backgroundPage background
--st-foregroundPrimary text color
--st-surfaceDefault raised surface (cards, dialogs)
--st-surface-2Second-tier surface stacked on --st-surface
--st-surface-3Third-tier surface for the densest stacks
--st-borderHairline borders between surfaces
--st-muted-foregroundSecondary text on any surface

Interactional

Tokens that paint interactive states regardless of intent.

TokenUse
--st-neutralRest fill for filled-neutral controls. Paired with --st-neutral-foreground
--st-accentHover background over neutral or transparent surfaces. Paired with --st-accent-foreground
--st-highlightPersistent selected or current background, soft primary tint. Paired with --st-highlight-foreground
--st-ringFocus ring color. Defaults to --st-primary so brand swaps repaint focus

Geometry

TokenUse
--st-radiusDefault corner radius. --st-radius-sm and --st-radius-lg derive from it so brutalist (0) cascades cleanly
--st-shadowDefault raised-surface shadow
--st-densityMultiplier wrapped around every component's padding and height. 1 default, 0.875 compact, 1.125 comfortable

Type

TokenUse
--st-font-sansDefault UI font stack
--st-font-monoMonospace 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

BlockSlotsVanillaReact + Base UIVue
.checkbox(atomic)ShipsPlannedPlanned
.input(atomic)ShipsPlannedPlanned
.input-group__textShipsPlannedPlanned
.radio(atomic)ShipsPlannedPlanned
.select(atomic)ShipsPlannedPlanned
.slider(atomic, native range)ShipsPlannedPlanned
.switch__input, __labelShipsPlannedPlanned
.textarea(atomic)ShipsPlannedPlanned

Components

BlockSlotsVanillaReact + Base UIVue
.accordion__item, __header, __icon, __body, __body-innerShipsPlannedPlanned
.alert__heading, __description, __action, __linkShipsPlannedPlanned
.badge__iconShipsPlannedPlanned
.breadcrumb__itemShipsPlannedPlanned
.btn__iconShipsPlannedPlanned
.button-group(composes .btn children)ShipsPlannedPlanned
.card__header, __title, __subtitle, __body, __text, __footer, __image, __overlay, __linkShipsPlannedPlanned
.collapsible(atomic; pairs with a trigger)ShipsPlannedPlanned
.icon-box(atomic)ShipsPlannedPlanned
.kbd(atomic)ShipsPlannedPlanned
.link(atomic)ShipsPlannedPlanned
.list-group__itemShipsPlannedPlanned
.pagination__button, __ellipsisShipsPlannedPlanned
.placeholder(atomic, paired with sizing utilities)ShipsPlannedPlanned
.progress__barShipsPlannedPlanned
.spinner(atomic)ShipsPlannedPlanned
.table__head, __body, __rowShipsPlannedPlanned
.tabs__list, __trigger, __panelShipsPlannedPlanned
.toggle__iconShipsPlannedPlanned
.toggle-group(composes .toggle children)ShipsPlannedPlanned

Overlays

BlockSlotsVanillaReact + Base UIVue
.dialog__backdrop, __panel, __content, __header, __title, __body, __footer, __closeShipsPlannedPlanned
.drawer__backdrop, __content, __header, __title, __body, __footer, __closeShipsPlannedPlanned
.dropdown-menu__group, __header, __item, __icon, __indicator, __shortcut, __dividerShipsPlannedPlanned
.popover__title, __body, __close, __arrowShipsPlannedPlanned
.toast__icon, __content, __header, __body, __timestamp, __action, __closeShipsPlannedPlanned
.tooltip__inner, __arrowShipsPlannedPlanned

Layout

BlockSlotsVanillaReact + Base UIVue
.app-shell__body, __mainShipsPlannedPlanned
.navbar__brand, __toggle, __menu, __nav, __buttonShipsPlannedPlanned
.page__header, __title, __action, __section, __section-header, __section-titleShipsPlannedPlanned
.sidebar__header, __brand, __content, __footer, __menu, __group, __group-title, __group-action, __list, __item, __item-action, __button, __submenu, __caretShipsPlannedPlanned

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.

StateTriggerNotes
RestdefaultThe base painted by the component's tokens
Hover:hoverDerives at runtime via color-mix(in oklch, base 88%, black)
Active:activeDerives at runtime, typically 78% over black
Focus:focus-visibleRing derives from --st-ring. Never :focus for the visible ring
Disabled:disabled on form controls, aria-disabled="true" on link buttonsTone 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.

HookComponentsMeaning
data-state="open" / "closed"Accordion, Collapsible, Dialog, Drawer, Dropdown, Popover, TooltipDisclosure open or closed
data-state="active" / "inactive"Tabs, Toggle, Toggle groupSelected / current vs. not
data-state="checked"Checkbox, Radio, SwitchOn state of a binary control
.is-loadingButton, InputAsync work in flight
.is-collapsedSidebar, CollapsiblePersistent collapsed state distinct from open / closed
.is-valid / .is-invalidForm controlsValidation result
.is-indeterminateCheckbox, ProgressTri-state or unknown

Scales

Three knobs reshape the system proportionally. Implementations expose them as global tokens, and per-component overrides ride on top.

KnobTokenRange
Radius--st-radius0 (brutalist), 0.75rem (default), 1rem (soft). -sm and -lg derive multiplicatively
Density--st-density0.875 (compact), 1 (default), 1.125 (comfortable). Wraps every padding and height value
Brand--st-primaryAny 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 themeStays 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.

ImplementationStatusNotes
Vanilla CSS + JSShips in 3.0.0-beta.1Reference implementation. Installation
React + Base UIPlannedSame tokens, same class names, headless interactions via Base UI
VuePlannedSame 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.