Theme specification

@skirbi/pinta

5 min

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
<html skirbi-theme="client">

To create a different context within a page, establish another theme root:

html
<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

css
@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.

css
@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-aside and --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.