Theming ​

The kit's look comes from three kinds of CSS variables:

  • Palette — a primary color, four status colors, a neutral gray-* scale, the page background and text, and fonts. They are Tailwind theme tokens: an app overrides them in its own @theme.
  • Color roles — what components actually paint with, named by purpose: main and muted text, surfaces, lines, focus. Each reads the palette by default, and an app can set any of them in @theme too.
  • w-* variables — sizes and radii of fields, buttons and list headers. An app sets them on body, directly or with the matching utilities (w-input-h-9, w-button-rounded-xl).

All are plain CSS variables at runtime, so this site can swap them live: the whole site follows the playground below, including the header and every component page.

  • The theme in use is at the top, with what to do with it. Save as… keeps it under a name in this browser; changes to a saved theme are saved into it as you make them, and the palette button in the header switches to it from any page. Copy link shares it: the link opens this page with the theme applied, and adds it to the other person's themes when it is a saved one. Copy CSS gives the stylesheet for an app.
  • Presets are starting points, and My themes lists the saved ones. Random picks a primary color, status colors (negative, positive, warning, info) in the same style and weight, the neutral scale, size and radius that suit them, and a dark mode background that fits the style — lifted for Soft, almost black for Neon, whose shadows also glow in its primary — each at a lightness that passes the kit's contrast checks in both modes. A fill with black text can be bright, like warning, and goes brightest in Neon. A yellow or lime primary always is one, and the status colors turn bright with it. Bright fills stand out from a white page less than darker fills do, so their text and the focus ring are darkened to stay readable.
  • Ask an AI assistant describes a look in words: it opens Claude or ChatGPT with a prompt that explains the tokens, or copies it for another assistant. The reply gives the theme as JSON and as a link; paste either one, or the whole reply, into the field below the buttons.
  • The fields below adjust each token of the theme in use.

Preview ​

edible
pet-safe

Model: [ 2, 3 ]

Password, two-factor authentication and active sessions.

Tabs keep the height of the tallest one opened so far, so the page below doesn't jump.

Current tab: security

0%

In an app ​

Copy CSS gives what goes in the app's stylesheet, after the kit's base:

css
@import "tailwindcss";
@import "eco-vue-js/tailwind-base/base.css";

@theme {
  --color-primary: oklch(54% 0.15 250);
  --color-gray-50: oklch(98.4% 0.003 247.858);
  /* … the rest of the neutral scale */
}

body {
  --w-input-height: 2.25rem;
  --w-input-rounded: 0.5rem;
  /* … */
}

Only changed colors are listed, but every w-* variable is: this site sets its own sizes, and an app without them gets the kit's defaults instead (in the shape table).

Light and dark mode ​

A color named X is its value in light mode and X-dark its value in dark mode — not a darker shade. The status colors and primary default their -dark to the light value, which works when the color is a mid-tone: dark enough for white text, light enough to stand out on a dark page. Set -dark when one value can't do both, as the compact preset does with a near-black primary that turns light gray in dark mode.

Swapping the neutral scale in the playground also sets the dark page background, default-dark, to the scale's gray-900, so both modes take its tint. In an app, set default-dark alongside the scale: the kit keeps it fixed, since some apps use it and gray-900 as two different surfaces.

Tones ​

The palette gives one color per tone, the fill. Everything else a tone needs is derived from it in the current mode:

  • Text and borders on the page — the fill's lightness clamped to stay readable: at most 53% in light mode, at least 72% in dark.
  • Text on the fill — black or white, whichever reads better by APCA, so a saturated purple or blue keeps white text where WCAG 2 would pick black.
  • Soft background — 12% of the fill over the surface, as in info cards.

So a light brand color still gives readable links on white, and a yellow fill gets black text, without extra tokens. To pin one of them, set --tone-text-{name}, --tone-line-{name}, --tone-on-{name} or --tone-soft-{name} in an @theme inline block — for example --tone-on-warning: var(--color-black-default).

Roles ​

RoleClassesDefault, light / darkPaints
text-accenttext-accentblack-default / defaultMain text
text-descriptiontext-descriptiongray-500 / gray-400Muted text
text-subtletext-subtlegray-400 / gray-600Placeholders, disabled text, chart axes
surfacebg-surfacedefault / default-darkPage and cards
surface-subtlebg-surface-subtlegray-50 / gray-850Barely raised areas: striped rows, empty chart cells, panels inside a card
surface-mutedbg-surface-mutedgray-100 / gray-800Secondary fills, hovered rows, tooltips
surface-insetbg-surface-insetgray-200 / gray-700Fills inside controls, chips
surface-raisedbg-surface-raised, surface-raiseddefault / default-dark lightened by 4%Dropdowns and popovers — in dark mode a shadow hardly shows, so they are lighter than the page instead
overlaybg-overlaydefault 40% / default-dark 60%Translucent bars over scrolling content
backdropbg-backdropprimary-light 40% / primary-darkest 40%Behind modals, bottom sheets and the mobile nav
trackbg-trackgray-300 / gray-700Unfilled tracks of sliders and progress bars, toggles and checkboxes when off
track-strongbg-track-stronggray-400 / gray-500The stronger track: neutral progress bars, slider and tab indicator parts
lineborder-linegray-300 / gray-700Field borders and dividers
line-subtleborder-line-subtlegray-200 / gray-700Card and section borders
line-raisedborder-line-raised, outline-line-raisedgray-300 / gray-700Edge of dropdowns, popovers and tooltips
shadowshadow-md, drop-shadow-mdblack 15% / black 50%Shadow under dropdowns, modals, tooltips and toasts — a primary mix makes it a glow
focusborder-focus, outline-focusprimary / primary-darkFocused field border and ring
gradient-start, gradient-middle, gradient-endfrom-gradient-start via-gradient-middle to-gradient-endprimary, turned 30° and 60° towards pink / the same from primary-darkThe brand gradient

A theme sets a role with --role-{name} and --role-{name}-dark in @theme:

css
@theme {
  --role-line: var(--color-gray-400);
  --role-surface-muted-dark: oklch(25% 0.02 250);
}

The shadow's color is a role too, and --w-shadow-lift sets how far it falls: 0 centers it, so a theme can turn every shadow-md into a glow:

css
@theme {
  --role-shadow-dark: color-mix(in oklab, var(--color-primary-dark) 45%, transparent);
}

body {
  --w-shadow-lift: 0rem;
}

On a dark page a glow shows about as bright as its alpha times its color's lightness, so a yellow or cyan glows much brighter than a purple at the same share. Dividing the alpha by the lightness evens that out — this matches 45% at lightness 0.6 for every hue:

css
@theme {
  --role-shadow-dark: oklch(from var(--color-primary-dark) l c h / min(0.675, calc(0.27 / l)));
}

Data palette ​

Categories — chart series, plant kinds, syntax colors — take the kit's data hues: data-red, data-orange, data-amber, data-green, data-teal, data-cyan, data-blue, data-violet, data-fuchsia, data-pink and data-gray. They are mid-tones like the status colors, so each works as a tone in both modes without -dark values (the Preview above shows them as fills and as soft chips).

An app names its categories after them, so a theme that retunes the palette retunes the categories too:

css
@theme {
  --color-kind-fern: var(--color-data-green);
  --color-kind-succulent: var(--color-data-teal);
}
html
<span class="tone-kind-fern text-tone">Fern</span>
<span class="tone-kind-succulent surface-soft text-tone">Succulent</span>

In app code ​

Components use the same classes, and app code should too: a color written as a role or a tone follows the theme and the mode by itself, with no dark: pair.

html
<!-- muted text and a divider -->
<p class="text-description border-b border-line-subtle">3 items</p>

<!-- tone text on the page -->
<p class="tone-negative text-tone">Required</p>

<!-- filled: plain text and descriptions inside become readable on the fill -->
<div class="tone-primary surface-fill">
  Plan <span class="text-description">renews monthly</span>
</div>

<!-- soft: tinted background, normal text, a tone icon -->
<div class="tone-info surface-soft">
  <IconInfo class="text-tone" />
  <p class="text-accent">Title</p>
</div>
ClassEffect
tone-{name}Sets the tone for the element and its children: primary, negative, positive, warning, info, any theme color (tone-kind-fern) or an arbitrary one (tone-[#0f766e])
text-toneThe tone's readable shade, for text and icons on the page
border-tone-lineThe tone's shade for borders and outlines on the page — nearer the fill than text-tone, as lines need less contrast than text
bg-tone-fill, border-tone-fill, text-tone-onThe fill, and the text that goes on it
bg-tone-softThe tone's soft background
surface-fillFill background; text-accent, text-description, text-subtle and lines inside switch to the text-on-fill color
surface-softSoft background; roles inside stay as they are
surface-mutedMuted background; a soft tone set inside it mixes over it
surface-raisedDropdown or popover background; a soft tone and bg-surface inside it follow it
surfacePage background and text, and resets roles a scope outside changed
light, darkForces a mode for the element and its children, as tooltips do with dark

Roles take opacity like any color: bg-surface-muted/50, outline-focus/20.

Migrating dark: pairs ​

The kit ships a codemod that rewrites light/dark palette pairs in an app's class strings to roles and tones:

sh
npx eco-vue-color-roles                      # report what it would change in src/
npx eco-vue-color-roles --write src          # apply
npx eco-vue-color-roles --exact --write src  # only replacements that render the same colors

Without --exact it also moves near-duplicate grays to the role the kit uses and tone text to the readable shade derived from the fill. It ends with the pairs it found no role for, most frequent first. A frequent one usually means the app's surface or line differs from the kit's default. Set it as a role in the app's theme, then run again with --theme, so those pairs map too:

css
@theme {
  --role-surface-dark: var(--color-gray-900);
}
sh
npx eco-vue-color-roles --theme src/assets/styles/index.css --write src

A link to any page of this site can carry a theme in a theme query parameter. Opening it applies the theme, keeps it in the browser like one picked here, and drops the parameter from the address. Copy link above makes one for this page.

The parameter is a preset id, or a URL-encoded JSON object of tokens, with an optional preset the tokens override:

https://rsmple.github.io/eco-vue-js/guide/theming?theme=ocean
https://rsmple.github.io/eco-vue-js/components/select?theme={"preset":"compact","color-primary":"#e11d48","neutral":"stone"}

Encode the JSON with encodeURIComponent when building a link in code; browsers accept it unencoded when typed. Keys are the variable names without --, plus neutral, which takes a scale name: zinc, slate, gray, neutral or stone. An optional name saves the theme among the opener's themes under that name, numbered when it is taken; opening a theme that is saved already switches to it instead of saving a copy. Unknown keys are ignored, and so are values with ;, {, }, <, >, \, url( or @import. Colors take any CSS color syntax, including var(--color-gray-400); sizes any CSS length. Radii double as side padding, so a pill shape is half the height (1.125rem for 2.25rem), not 9999px.

To make a theme from a description — "a warm, rounded theme for a finance dashboard" — pick a neutral scale and a primary, adjust status colors only where they clash with it, and link to the page the theme should be seen on, or to this one to keep editing it. Mid-tone fills (OKLCH lightness 50–58%) work in both modes without -dark values.

Presets ​

IdLook
defaultThe kit's violet on zinc, as this site ships
oceanBlue primary, cyan info, slate neutrals
forestGreen primary, lime positive, stone neutrals, pill-shaped fields and buttons
sunsetOrange primary, rose negative, stone neutrals
compactMonochrome: near-black primary that turns light in dark mode, no tint, 2rem fields and buttons, small radii

Palette ​

KeyDefaultControls
color-primaryoklch(62% 0.17 286.8)Fills, links, focus and selection
color-primary-darkprimaryThe same in dark mode
color-primary-lightprimary 10% over defaultList headers and the backdrop in light mode
color-primary-darkestprimary-dark 20% over default-darkThe same in dark mode
neutralzincThe gray-* scale: lines, fills, muted text
color-default#ffffffPage and card background in light mode
color-default-darkoklch(21% 0.006 285.885), zinc's 900Page and card background in dark mode
color-black-default#333333Main text in light mode
color-negativeoklch(64% 0.2 24.1)Errors and destructive actions
color-positiveoklch(69% 0.148 150)Success
color-warningoklch(85% 0.16 85)Warnings; black text on the fill
color-infooklch(63% 0.16 262.6)Information
color-*-darkthe light valueEach status color in dark mode
color-data-*Tailwind's hues at mid lightness: white text on all but orange and amber; gray is the scale's 500Data palette
role-*, role-*-darksee RolesOne role, per mode
font-sansMontSerrat, system-ui, sans-serifText font. Only MontSerrat is loaded on this site; other families must be installed on the viewer's machine

Shape ​

KeySite valueKit defaultControls
w-input-height2.25rem2.75remInputs and selects; the options inside them follow
w-input-rounded0.5rem0.75remRadius of inputs and selects, and the side padding of their options
w-input-gap0.125rem0.25remSpace between a field's border and the options in it
w-button-height2.25rem2.75remButtons
w-button-rounded0.75rem1remRadius of buttons, and their side padding
w-list-header-height2.25rem2remList headers
w-list-header-rounded0.75rem1remRadius of list headers
w-checkbox-size0.75rem1remCheckboxes and radios
w-shadow-lift0.5rem0.5remHow far shadow-md falls under dropdowns, modals and tooltips; 0rem centers it into a glow

The kit has more w-* variables for single components, such as w-list-padding and w-modal-wrapper-rounded; the playground covers the ones that shape the whole app.