Skip to content

Extending a theme ​

@astryx-vue/themes ships Neutral, Butter, Chocolate, Gothic, Matcha, Stone, and Y2K. Each one is a CSS entry such as butter.css plus the stylesheets in that theme's directory. There is no runtime theme registry.

Override variables ​

Change the look by overriding CSS variables. Put the override in the astryx.overrides layer so it beats the token layer without rewriting structural styles:

css
@layer astryx.overrides {
  :root {
    --astryx-color-text-primary: light-dark(#1a1a1a, #f5f5f5);
    --astryx-radius-element: 0.5rem;
  }
}

Load that sheet after the theme CSS, for example @astryx-vue/themes/neutral.css.

Every theme selects [data-astryx-theme='…']. Neutral also selects :root, so importing neutral.css applies it globally. The other themes stay on the attribute.

When one component has to diverge, override the variable from that theme's component file, for example --astryx-button-primary-background in packages/themes/neutral/button.css. Use the names in that file.

Do not keep a separate dark palette. Colors are already light-dark(). Switching is described in Dark mode.

Build a theme in Vue ​

To change one subtree, wrap it in Theme and pass a defineTheme() object. color, typography, radius, and motion generate a token set from a few numbers. The seven shipped themes stay handwritten CSS.

ts
import { defineTheme } from '@astryx-vue/core/Theme'

const brand = defineTheme({
  name: 'brand',
  // One hex, or a light seed and a dark seed.
  color: { accent: ['#0064E0', '#48CAE4'] },
  typography: { scale: { base: 14, ratio: 1.2 } },
  // Values only this theme family uses. Names stay as written.
  localTokens: { '--brand-ink': ['#0A2540', '#E6F0FF'] },
  components: {
    'button': { 'variant:primary': { borderRadius: '999px' } },
    'card-header': { base: { color: 'var(--brand-ink)' } },
  },
  // Larger body text on touch screens narrower than the lg breakpoint.
  adaptations: {
    rules: [{
      when: { pointer: 'coarse', width: { below: 'lg' } },
      value: { typography: { scale: { base: 16, ratio: 1.1736 } } },
    }],
  },
})

Overriding theme tokens ​

Theme writes its tokens into a <style> that sits inside the wrapper element. The rule selector is :scope.astryx-theme, which weighs as much as two classes, and that <style> comes later in the document than your stylesheets. So a plain rule on the wrapper loses when it sits inside @layer astryx.overrides and names fewer than three classes. Pick one of these:

  • Put the override in CSS that is not inside any @layer. Unlayered styles beat every layered style, so this always wins. It is the simplest choice.
  • Inside @layer astryx.overrides, stack at least three classes on the wrapper (.wrapper.wrapper.wrapper), or wrap the Theme in your own element with an id and select through it (#brand .wrapper). Theme does not put an id on its wrapper, so #id alone does not match it.

The same holds for adapted tokens, with or without a matching condition. A value passed in the wrapper's own style attribute also wins.

Theme names must be unique. Two Theme components with the same name share one set of rules. If their values differ, the one that renders later wins for both. Give different themes different names.

Theme targets ​

Keys under components are theme targets, named <component-kebab>-<part>: button, card-header, radio-indicator, progress-bar-fill. Each target is a stable astryx-* class on one element. Style keys under a target match the attributes that element reflects: variant:primary matches [data-variant="primary"], a state such as checked matches [data-checked="checked"], and + joins two keys.

Run-together and camelCase names are not targets. defineTheme() throws for buttonIcon or Card, and a run-together name such as progressbar matches nothing. Upstream removed these spellings in 0.7.0; this package follows. The old names and their replacements:

RemovedUse
base-tabletable
checkboxcheckbox-indicator
codeblock, codeblock-copy-button, codeblock-header, codeblock-titlecode-block, code-block-copy-button, code-block-header, code-block-title
date-input-clear-icon, date-range-input-clear-icon, multi-selector-clear-icon, selector-clear-iconinput-clear-icon
hovercardhover-card
naviconnav-icon
popover-surfacepopover
progressbar, progressbar-fill, progressbar-mark, progressbar-trackprogress-bar, progress-bar-fill, progress-bar-mark, progress-bar-track
radio, radio-dotradio-indicator, radio-indicator-dot
statusdotstatus-dot
textareatext-area

Components also do not render bare value classes such as .primary or .sm. A selector like .astryx-button.primary in your own CSS becomes .astryx-button[data-variant="primary"].

A :hover style applies to the enabled control only. Disabled and aria-disabled elements skip it. For card, dialog, bottom-sheet, dropdown-menu, context-menu, and segmented-control, a themed borderRadius or padding also reaches the inner parts that follow it, such as a card's header corners. On chat a themed borderRadius and padding land on the composer, and on progress-bar-mark a themed width and height land on the tick itself.

Local tokens ​

localTokens holds values only one theme family uses, keyed by their full custom-property name. The name is kept exactly as written; no prefix is added. A shared token name such as --color-accent is refused here, because components already read it. A theme that extends this one inherits the local tokens and may change their values. A reference loop among them is an error.

Adaptations ​

adaptations.rules is an ordered list of { when, value }. when can hold a width range by named breakpoint (sm 640, md 768, lg 1024, xl 1280, 2xl 1536 CSS px; change them with widthBreakpoints), a pointer of coarse or fine, a contrast preference, and a reduced-motion preference. Every field in one when must match. value can set the same things a theme does. A partial type scale, radius, or motion is completed from the theme's own config. Rules apply after the theme, in order, so a later matching rule wins. They apply through CSS only: useTheme().token() keeps returning the theme's root values. An adapted token sits after the root tokens with the same selector, so while its condition matches it wins by cascade order; no !important is involved. To override an adapted token, follow the rules in Overriding theme tokens.

extends ​

extends starts from everything another theme resolved to: tokens, local tokens, component styles, onDark and onLight, adaptations, icons, and indicators. The new theme's own values win. Inherited adaptation rules are worked out again against the new theme's scale.

Bad values ​

A token or style value that would end its CSS declaration early, such as red; } body { display: none, is dropped with a console warning. The rest of the theme still applies. Valid CSS that only looks unusual, such as a ; inside a quoted string or a data URL, is kept.

Tailwind ​

This package does not ship a Tailwind bridge. Styles are plain CSS variables and @layer, so a Tailwind or UnoCSS project can read them directly, for example bg-[var(--astryx-color-background-surface)], or map them in its own theme config.