Theme specification
@skirbi/pinta
This specification describes the current Pinta theme contract, checked against
@skirbi/pinta’s lib/themes/pinta.css on 2026-09-15. The stylesheet contains
the detailed token and component recipe reference.
The namespace migration and removal of general-purpose utilities happened in
v0.0.12. Examples using pinta-theme, semtic-theme, generic tone/variant
attributes, or numbered spacing utilities describe the old design.
Scope
Pinta provides CSS tokens, color derivations, typography, accessibility defaults, and component recipes. It requires no JavaScript for styling. Applications own their layout refinements and state management.
The extension model is ordinary CSS: choose meaningful classes or attribute selectors, consume theme tokens, and override recipes through cascade layers. There is no general-purpose utility attribute API.
Namespace and activation
| Contract | Name |
|---|---|
| Theme activation and optional name | skirbi-theme |
| Theme mode | `skirbi-theme-mode=“light |
| Page background transform | `skirbi-bg-transform=“none |
| Theme custom properties | --skirbi-* |
| Structural and component element names | semtic-* |
[skirbi-theme] enables the defaults on a theme root and recipes within its
subtree. An empty attribute is valid. A named theme is a selector hook for
CSS supplied by the theme author, not an automatic theme loader.
<html skirbi-theme="client">
To create a different context within a page, establish another theme root:
<section skirbi-theme="client" skirbi-theme-mode="dark">
<semtic-panel>Dark surface</semtic-panel>
</section>
Mode and transform selectors require skirbi-theme on the same element.
Tokens inherit, but a nested theme root declares Pinta’s defaults again.
Descendant selectors from outer themes still participate in the cascade;
a theme root is not style isolation.
Cascade contract
@layer skirbi-base, skirbi-theme, skirbi-transform, skirbi-components,
skirbi-app;
| Layer | Responsibility |
|---|---|
skirbi-base |
Structural, typography, and accessibility defaults |
skirbi-theme |
Palette, source tokens, default knobs, theme overrides |
skirbi-transform |
Environment selection, derivations, final tokens |
skirbi-components |
Component recipes |
skirbi-app |
Application overrides |
Load Pinta before theme and application styles. For normal declarations,
later layers take precedence over earlier layers. Specificity and source
order resolve competing declarations within a layer. Unlayered normal rules
outrank layered normal rules. !important reverses layer priority, so the
layer order is not a promise that every application declaration wins.
Color families
All four families, primary, secondary, tertiary, and quaternary, expose:
| Suffix | Meaning |
|---|---|
main |
Source color |
soft |
By default, 30% main mixed with white in sRGB |
strong |
By default, 30% main mixed with black in sRGB |
contrast |
Foreground for that family |
For example, the primary family uses --skirbi-primary-main,
--skirbi-primary-soft, --skirbi-primary-strong, and
--skirbi-primary-contrast. All values can be overridden.
--skirbi-on-dark defaults to white and --skirbi-on-light to #0f172a.
Primary, secondary, and quaternary contrast default to on-dark; tertiary
contrast defaults to on-light. Pinta does not calculate whether a changed
palette has sufficient contrast. Theme authors must choose and check these
foregrounds.
The --skirbi-<family>-light and --skirbi-<family>-dark names are deprecated
aliases for soft and strong. New themes should use the current names.
There is no generic --tone-* or --brand-* contract.
Status colors
| Token | Default |
|---|---|
--skirbi-info-base |
#2f6fed |
--skirbi-success-base |
#0f766e |
--skirbi-warn-base |
#d97706 |
--skirbi-error-base |
#b91c1c |
These serve semantic states independently of the brand palette. Changing them requires checking the components and states that consume them.
Background derivation and final tokens
--skirbi-page-bg-source and --skirbi-surface-bg-source supply source colors.
In light mode they default to primary and tertiary main, respectively.
The transform layer mixes source colors with --skirbi-env-base, which is
white in light mode and black in dark mode.
| Attribute value | Page source percentage |
|---|---|
skirbi-bg-transform="none" |
100% |
skirbi-bg-transform="wash" |
--skirbi-wash-perc, default 8% |
skirbi-bg-transform="reverse-wash" |
--skirbi-reverse-wash-perc, default 85% |
Without the attribute, --skirbi-bg-transform-perc defaults to the wash
percentage. The --skirbi-bg-transform property is a marker; the implemented
switch uses attribute selectors. Changing that property alone has no effect
on the percentage.
Surface backgrounds mix their source using --skirbi-surface-lift, borders
use --skirbi-border-lift, and muted text uses --skirbi-muted-lift. Their
light-mode defaults are 8%, 12%, and 40%. Elevated surfaces add ten
percentage points to the surface lift.
Each final token uses an explicit override if supplied, otherwise its derived value:
| Explicit override | Token consumed by recipes |
|---|---|
--skirbi-page-bg |
--skirbi-page-bg-final |
--skirbi-surface-bg |
--skirbi-surface-bg-final |
--skirbi-surface-bg-elevated |
--skirbi-surface-bg-elevated-final |
--skirbi-surface-border |
--skirbi-surface-border-final |
--skirbi-text-muted |
--skirbi-text-muted-final |
Foregrounds use --skirbi-page-fg and --skirbi-surface-fg directly.
Set source and override tokens on the theme root where derivations are
computed. Application CSS should consume final tokens instead of depending
on the intermediate *-transformed properties.
Mode behavior
Light is the default. [skirbi-theme][skirbi-theme-mode="dark"] switches the
environment to black, the surface source to secondary main, and the page and
surface foregrounds to primary and secondary contrast. Surface and border
lift become 12% and 18%.
The skirbi-theme-mode attribute is Pinta’s light/dark switch. Changing it
updates the recipes within that theme root. Choosing an initial mode from
prefers-color-scheme and persisting a user’s choice are integration concerns.
@layer skirbi-theme {
[skirbi-theme="client"] {
--skirbi-primary-main: #355c7d;
--skirbi-secondary-main: #00c9b5;
--skirbi-secondary-contrast: var(--skirbi-on-light);
}
[skirbi-theme="client"][skirbi-theme-mode="dark"] {
--skirbi-surface-bg-source: #243642;
--skirbi-surface-fg: #f8fafc;
}
}
Typography, focus, and motion
Typography uses --skirbi-font-sans, --skirbi-font-mono,
--skirbi-font-scale, --skirbi-line-height, --skirbi-h1-scale through
--skirbi-h4-scale, --skirbi-heading-line-height,
--skirbi-heading-tracking, and --skirbi-prose-gap.
There is no font-profile attribute in the current stylesheet.
Focus recipes consume --skirbi-focus-ring-size and
--skirbi-focus-ring-color, with fallbacks of 2px and primary main.
Motion knobs include --skirbi-transition-duration,
--skirbi-transform-duration, and --skirbi-transition-ease.
Component contracts
Recipes belong to specific elements and roles. They are not a universal
theme-tone / theme-variant system.
Buttons and action links support lead, support, extra, antagonist,
and herald. Their tokens use --skirbi-button-<role>-*; shared knobs include
--skirbi-button-radius and --skirbi-button-font-family.
Form controls use --skirbi-field-height for input/select height and derive
checkbox/radio size as 2 / 3.5 of it. --skirbi-choice-size overrides that
size. Textarea height is independent. Error recipes respond to a nonempty
semtic-error message and native :user-invalid states.
For a complete list of each component’s selectors and knobs, consult its
recipe in lib/themes/pinta.css.
Layout recipes
The layout tags retain the semtic- namespace. Their grid/flex behavior comes
from CSS, without a JavaScript component registration.
| Element | Defaults and knobs |
|---|---|
semtic-grid |
Grid, one column, --skirbi-columns, --skirbi-grid-gap |
semtic-flex |
Row flexbox, --skirbi-flex-gap, --skirbi-direction, --skirbi-justify, --skirbi-align, --skirbi-wrap |
semtic-stack |
Column flexbox, --skirbi-stack-gap |
All three gaps default to 1rem. semtic-flex still maps component-specific
semtic-direction, semtic-justify, semtic-align, and semtic-wrap="wrap"
attributes. There is no numbered semtic-gap mapping or responsive
semtic-columns-* API.
Pinta also provides named layouts:
semtic-grid[diptych]: content first in the DOM, then aside; aside appears on the left at wider widths. Knobs:--skirbi-diptych-asideand--skirbi-diptych-gap.semtic-grid[triptych]: content first, then start and end regions. Knobs:--skirbi-triptych-content,--skirbi-triptych-aside,--skirbi-triptych-gap, and--skirbi-triptych-edge.
The shared layout thresholds are 48rem and 69rem. Diptych stacks at
48rem and below. Triptych has three columns above 69rem, two columns
between the thresholds, and stacks at 48rem and below. Media queries use
literal lengths. The corresponding --skirbi-breakpoint-mobile and
--skirbi-breakpoint-tablet properties document those values; changing the
properties does not rewrite the media queries.
Page-specific spacing, columns, responsiveness, borders, shadows, and
interactions belong in normal CSS, usually in skirbi-app.