Skip to main content

Component Skin

The component skin level restyles the built-in components. It builds on a specific styling stack and a stable hook contract.

The Styling Stack

The built-in UI uses four technologies together:

  • CSS Modules for component-scoped styles, with hashed class names that are explicitly not a public contract.
  • CSS Custom Properties for the semantic tokens (--st-*).
  • Cascade Layers to order the sources of truth.
  • Container Queries for layout that adapts to the component's own container, with sizes expressed in rem.

Themes target the hook attributes, never the generated class names.

Cascade Layer Order

All styles live in a fixed cascade layer order:

@layer reset, tokens, base, components, plugin-base, theme, user;

Later layers win over earlier ones, so the precedence is:

  1. reset — the base reset.
  2. tokens — the token definitions.
  3. base — element-level defaults.
  4. components — the built-in component styles.
  5. plugin-base — a layer for plugin-provided base styles.
  6. theme — the active theme's skin.
  7. user — the user's own overrides, which load last.

The user override stylesheet always loads last, so a broken or opinionated theme can never prevent the user from overriding it. In !important terms: the construct is forbidden in theme CSS except in the accessibility preferences layer, which belongs to the user-facing a11y modes.

The Hook Contract

Themes style components through four attributes, published by the host and versioned like the rest of the SDK:

<div
data-component="chat-message"
data-part="container"
data-role="assistant"
data-state="streaming"
></div>
  • data-component — the component kind.
  • data-part — the structural part inside a component.
  • data-role — a semantic role, such as a message role.
  • data-state — a state, such as open, closed, or streaming.

A theme's skin CSS then looks like this:

@layer theme {
[data-component='button'][data-variant='primary'] > [data-part='icon'] {
color: var(--st-color-accent-text);
}

[data-component='action-bar'] [data-part='group'][data-role='secondary'] {
color: var(--st-color-text-secondary);
}
}

The @neotavern/theme-sdk package exports the dataHook helper for building these attribute objects, so component authors and theme authors agree on the same names.

What Is Not a Contract

  • Generated CSS-module class names — hashed, unstable, and not part of the SDK. A theme that targets them breaks on the next build.
  • The internal React hierarchy — themes must not depend on component internals or DOM order beyond the documented hooks.
  • Numeric layout values — coordinates, grid schemes, and breakpoints are not styleable through the token contract; viewport breakpoints live in the registry and container queries must be written in rem.

Forbidden CSS

Theme stylesheets are scanned before they load. The forbidden constructs are rejected at install and validation time:

  • @import
  • javascript: URLs and expression().
  • -moz-binding and behavior:.
  • Remote or protocol-relative URLs (url(http:, url(https:, url(//).
  • data:text/html.
  • !important (except the a11y preferences layer).

This keeps theme CSS pure, local, and safe. For the tokens the skin should reference, see Design Tokens; for the named areas a skin can restyle, see Shell Contract.