Skip to content

@styles Theming Layer

@headless-primitives/styles is the optional visual layer on top of base.css. It provides the complete token system, brand colors, dark mode, shadows, typography scale, and all interactive states (hover, focus, pressed, disabled).

Architecture overview

┌─────────────────────────────────────────────────────────────┐
│  Your CSS / Tailwind                                         │
│  Override tokens or add classes — always wins               │
├─────────────────────────────────────────────────────────────┤
│  @headless-primitives/styles  (this layer)                  │
│  theme.css   ← all CSS custom properties live here          │
│  button.css, tabs.css, …  ← consume tokens from theme.css   │
├─────────────────────────────────────────────────────────────┤
│  @headless-primitives/utils/base.css                        │
│  Layout · Show/hide · Positioning · Animations              │
└─────────────────────────────────────────────────────────────┘

@styles overrides base.css where necessary (e.g., replacing currentColor fallbacks with proper token-based colors) and adds entirely new visual states.


Installation

bash
pnpm add @headless-primitives/styles
bash
npm install @headless-primitives/styles
bash
yarn add @headless-primitives/styles
bash
bun add @headless-primitives/styles

Import

css
/* Everything at once */
@import "@headless-primitives/styles/index.css";

/* Selective — theme.css must always come first */
@import "@headless-primitives/styles/theme.css";
@import "@headless-primitives/styles/button.css";
@import "@headless-primitives/styles/dialog.css";

Complete Token Reference

All properties are defined on :root in theme.css. They automatically switch for dark mode via @media (prefers-color-scheme: dark). You can override any of them at any selector scope.

Accent (Brand Color)

The accent controls the "selected" and "active" visual state across all interactive components: switches, checkboxes, radios, tabs, pressed buttons.

TokenLight defaultDark defaultWCAG ratioUsed in
--hp-accent#0369a1#38bdf85.93:1 (light) / 8.96:1 (dark)Switch on, checkbox checked, tab selected
--hp-accent-hover#075985#7dd3fc7.56:1 / —Hover over accent-colored elements
--hp-accent-active#0c4a6e#bae6fd10.4:1 / —Active/pressed state
--hp-accent-foreground#ffffff#0c1a29Text/icons on top of accent backgrounds

Override example — change the entire accent to violet:

css
:root {
  --hp-accent: #7c3aed; /* violet-700 → 5.08:1 vs white ✓ */
  --hp-accent-hover: #6d28d9; /* violet-800 */
  --hp-accent-active: #5b21b6; /* violet-900 */
  --hp-accent-foreground: #ffffff;
  --hp-focus-outline-color: #7c3aed; /* match the accent */
}

Surfaces

Background colors for different layers of the UI hierarchy.

TokenLight defaultDark defaultUsed in
--hp-bg#ffffff#0f172aPage background
--hp-bg-subtle#f8fafc#1e293bZones in resting state
--hp-bg-muted#f1f5f9#334155Button hover backgrounds, progress track
--hp-surface#ffffff#1e293bComponent surfaces (button, checkbox, dialog, toast)
--hp-surface-raised#ffffff#293548Elevated surfaces (dropdowns above surface)

Borders

Two distinct border semantic roles:

TokenLight defaultDark defaultWCAGUsed in
--hp-border#e2e8f0#334155— (non-interactive)Dividers, card edges, accordion separators
--hp-border-strong#64748b#64748b4.76:1 / 3.20:1Checkbox borders, radio borders, input borders

--hp-border is for visual separators (non-interactive). It does not need to pass the 3:1 UI component ratio. --hp-border-strong is for interactive component borders (checkbox, radio, input). It must pass 3:1 non-text contrast (WCAG 1.4.11).


Text

TokenLight defaultDark defaultWCAG ratioUsed in
--hp-text#0f172a#f8fafc18.1:1 / 17.6:1Primary text, labels
--hp-text-secondary#64748b#94a3b84.76:1 / 5.31:1Secondary text, descriptions, toggle labels
--hp-text-disabled#94a3b8#475569exempt (WCAG 1.4.3)Disabled state text
--hp-text-on-accent#ffffff#0c1a29Aliases --hp-accent-foreground for semantic clarity
--hp-text-error#dc2626#f871715.93:1 / 6.04:1hp-field-error messages

Border Radius

TokenValueUsed in
--hp-radius-sm4pxCheckboxes, tags, small close buttons
--hp-radius6pxButtons, inputs, popovers
--hp-radius-md8pxCards, dialogs triggers, toasts
--hp-radius-lg12pxModals (hp-dialog-content)
--hp-radius-full9999pxSwitch track, avatar, progress bar, pills

Override example — square/sharp corners (enterprise style):

css
:root {
  --hp-radius-sm: 2px;
  --hp-radius: 3px;
  --hp-radius-md: 4px;
  --hp-radius-lg: 6px;
}

Shadows

TokenValueUsed in
--hp-shadow-sm0 1px 2px 0 rgb(0 0 0 / 0.05)Switch thumb
--hp-shadow0 1px 3px … 0 1px 2px …Elevated buttons
--hp-shadow-md0 4px 6px … 0 2px 4px …Tooltips, popovers
--hp-shadow-lg0 10px 15px … 0 4px 6px …Dialogs, toasts

Dark mode adjusts shadow opacity upward (e.g., 0.4 instead of 0.1) automatically.


Backdrop

TokenLight defaultDark defaultUsed in
--hp-backdrop-bgrgb(0 0 0 / 0.5)rgb(0 0 0 / 0.65)hp-dialog-backdrop, alert dialog backdrop

Override to disable blur:

css
:root {
  --hp-backdrop-bg: rgb(0 0 0 / 0.3);
}

hp-dialog-backdrop {
  backdrop-filter: none;
}

Typography

TokenValueUsed in
--hp-font-size-xs0.75remToast description, field error, avatar fallback
--hp-font-size-sm0.875remDefault body text for all components
--hp-font-size-base1remInherited from document
--hp-font-size-lg1.125remDialog title
--hp-font-weight-normal400Default weight
--hp-font-weight-medium500Labels, buttons, triggers
--hp-font-weight-semibold600Dialog title, toast title, avatar initials

Spacing

TokenValueUsed in
--hp-space-10.25remVery tight gaps (toast title margin)
--hp-space-20.5remSmall padding (radio group gap, toggle padding)
--hp-space-30.75remMedium padding (accordion trigger, collapsible)
--hp-space-41remStandard padding (dialog, tab panels, toast)

Transitions

TokenValueUsed in
--hp-transition-fast100ms easeTooltip show/hide
--hp-transition150ms easeDefault interactive transitions
--hp-transition-slow200ms easeDialog and toast entrance

Opacity

TokenValueUsed in
--hp-opacity-disabled0.5All disabled interactive elements

Focus Outline (shared with base.css)

TokenDefaultDark defaultDescription
--hp-focus-outline-color#2563eb#38bdf8Focus ring color — override to match your brand accent
--hp-focus-outline-width2pxsameThickness of the focus ring

Per-Component Visual API

hp-button

Reads from:

--hp-text            → default text color
--hp-surface         → default background
--hp-border          → default border
--hp-bg-muted        → hover background
--hp-border-strong   → hover border
--hp-bg-subtle       → active background
--hp-accent          → pressed state background
--hp-accent-hover    → pressed state hover
--hp-accent-foreground → pressed state text
--hp-opacity-disabled → disabled opacity
--hp-focus-outline-color / --hp-focus-outline-width → focus ring
--hp-transition      → all state transitions
--hp-radius          → border radius
--hp-font-size-sm    → font size
--hp-font-weight-medium → font weight

Custom override example:

css
/* Danger button variant */
.btn-danger {
  background: var(--hp-text-error);
  color: white;
  border-color: var(--hp-text-error);
}
.btn-danger:hover:not([disabled]) {
  opacity: 0.9;
}

hp-switch

StateTokens used
Default (off)--hp-bg-muted (track bg), --hp-border, --hp-text-secondary (thumb color)
Hover (off)--hp-border-strong
Checked (on)--hp-accent (track), --hp-accent-foreground (thumb)
Checked hover--hp-accent-hover
Disabled--hp-opacity-disabled
Thumb shadow--hp-shadow-sm

hp-checkbox and hp-radio

StateTokens used
Default--hp-surface (bg), --hp-border-strong (border)
Hover--hp-accent (border)
Checked--hp-accent (bg + border), --hp-accent-foreground (checkmark/dot)
Disabled--hp-opacity-disabled

hp-toggle-group / hp-toggle

StateTokens used
Container--hp-border (outer border), --hp-surface (bg), --hp-radius
Item default--hp-text-secondary (text), --hp-border (separator)
Item hover--hp-bg-muted, --hp-text
Item pressed--hp-bg-muted, --hp-text
Disabled--hp-opacity-disabled

hp-tabs

StateTokens used
Tab list--hp-border (bottom border)
Tab default--hp-text-secondary (text)
Tab hover--hp-text, --hp-bg-muted (bg)
Tab selected--hp-accent (text + bottom border)
Tab disabled--hp-opacity-disabled
Panel--hp-text, --hp-space-4 (padding)

hp-accordion

StateTokens used
Container--hp-border, --hp-radius-md, --hp-surface
Item border--hp-border
Trigger default--hp-text
Trigger hover--hp-bg-muted
Trigger expanded--hp-accent (text color)
Content--hp-text-secondary

hp-collapsible

StateTokens used
Trigger--hp-text, hover: --hp-bg-muted
Content--hp-text-secondary, --hp-border (top border)

hp-dialog

ElementTokens used
Backdrop--hp-backdrop-bg
Content--hp-surface, --hp-border, --hp-radius-lg, --hp-shadow-lg, --hp-text
Title--hp-text, --hp-font-size-lg, --hp-font-weight-semibold
Close button--hp-text-secondary (default), --hp-text (hover), --hp-bg-muted (hover bg)
Animationshp-dialog-in/out, hp-backdrop-in/out (keyframes from base.css)

hp-popover

ElementTokens used
TriggerSame as hp-button
Content--hp-surface, --hp-border, --hp-radius-md, --hp-shadow-lg
Animationshp-overlay-in/out, hp-overlay-in-up/out-up (from base.css)

hp-tooltip

ElementTokens used
Content--hp-text (bg), --hp-surface (text), --hp-radius-sm, --hp-shadow-md
Arrowborder-top-color: var(--hp-text)
Animationshp-tooltip-in/out (from base.css)

hp-toast

ElementTokens used
Toast--hp-surface, --hp-border, --hp-radius-md, --hp-shadow-lg, --hp-text
Title--hp-text, --hp-font-weight-semibold
Description--hp-text-secondary, --hp-font-size-xs
Close button--hp-text-secondary, hover: --hp-bg-muted
Animationshp-toast-in/out (from base.css)

hp-field

ElementTokens used
Label--hp-text, --hp-font-size-sm, --hp-font-weight-medium
Description--hp-text-secondary, --hp-font-size-xs
Error--hp-text-error
Input/select/textarea--hp-text, --hp-surface, --hp-border-strong, --hp-radius, --hp-accent (focus border)

Override the error color for your brand:

css
:root {
  --hp-text-error: #e11d48; /* rose-600 */
}

hp-progress

ElementTokens used
Track--hp-bg-muted, --hp-radius-full
Indicator--hp-accent, --hp-radius-full, --hp-transition-slow

hp-separator

Token usedPurpose
--hp-borderColor of the separator line

hp-avatar

ElementTokens used
Container--hp-bg-muted, --hp-radius-full
Fallback text--hp-text-secondary, --hp-font-size-sm, --hp-font-weight-semibold

Dark Mode

Dark mode is automatic via @media (prefers-color-scheme: dark) in theme.css. All color tokens are overridden there.

To force light mode regardless of OS:

css
@media (prefers-color-scheme: dark) {
  :root {
    --hp-bg: #ffffff;
    --hp-surface: #ffffff;
    --hp-text: #0f172a;
    --hp-text-secondary: #64748b;
    --hp-border: #e2e8f0;
    --hp-border-strong: #64748b;
    --hp-accent: #0369a1;
    --hp-accent-foreground: #ffffff;
    --hp-backdrop-bg: rgb(0 0 0 / 0.5);
  }
}

To use a class-based dark mode (e.g., .dark on <html>):

css
/* 1. Neutralize the auto dark mode */
@media (prefers-color-scheme: dark) {
  :root {
    /* paste all light defaults here */
  }
}

/* 2. Apply dark tokens on your class */
.dark {
  --hp-bg: #0f172a;
  --hp-bg-subtle: #1e293b;
  --hp-bg-muted: #334155;
  --hp-surface: #1e293b;
  --hp-surface-raised: #293548;
  --hp-border: #334155;
  --hp-border-strong: #64748b;
  --hp-text: #f8fafc;
  --hp-text-secondary: #94a3b8;
  --hp-text-disabled: #475569;
  --hp-text-on-accent: #0c1a29;
  --hp-accent: #38bdf8;
  --hp-accent-hover: #7dd3fc;
  --hp-accent-active: #bae6fd;
  --hp-accent-foreground: #0c1a29;
  --hp-backdrop-bg: rgb(0 0 0 / 0.65);
  --hp-text-error: #f87171;
  --hp-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
  --hp-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.4), 0 1px 2px -1px rgb(0 0 0 / 0.4);
  --hp-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.4);
  --hp-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.4);
  --hp-focus-outline-color: #38bdf8;
}

Overriding in a specific context

Tokens cascade like any CSS custom property, so you can scope overrides to any selector:

css
/* Only inside this form */
.checkout-form {
  --hp-accent: #d97706; /* amber accent for this form */
  --hp-border-strong: #92400e;
  --hp-focus-outline-color: #d97706;
}

/* Only inside a dark sidebar */
.sidebar {
  --hp-surface: #1e293b;
  --hp-text: #f8fafc;
  --hp-border: #334155;
}

Specificity model

All @styles selectors use a single element selector (specificity 0,0,1). Any class in your project will always win:

css
/* specificity 0,0,1 — from @styles */
hp-button {
  background: var(--hp-surface);
}

/* specificity 0,1,0 — your class, always wins */
.btn-primary {
  background: #7c3aed;
}

/* specificity 0,0,1 but declared after — wins by cascade order */
/* Use this if you want to extend without adding a class */
hp-button:not(.custom) {
  border-radius: 0;
}

Playground override pattern (reference)

The playground app demonstrates a complete theme override for a dark dashboard. See apps/playground/src/style.css for a full working example of mapping --hp-* tokens to a custom dark palette.


Try it visually

Use the Theme Builder to edit every token with color pickers and sliders, see all components update in real time, and copy the generated CSS block directly into your project.

Lanzado bajo la MIT License.