Scoped surface
Different hue, different radius, same markup.
Every design decision in the framework is a CSS variable or a Sass function. Nothing is hardcoded at a call site.
| Variable | Default | Purpose |
|---|---|---|
--color | rebeccapurple | Base hue. Everything derives from this. |
--accent-color | complement of --color | Links |
--error-color | maroon | Invalid inputs, small.error |
--fallback-color | white | Used by .contrast |
--border-radius | 0.25rem | Corners on every surface |
--border-width | 0.1625rem | Borders and rules |
--padding | 1rem | Base spacing unit |
--box-shadow | four-layer shadow | Articles and dialogs |
--icon-width | 1rem | Size of input icons |
--icon-position | 0.75rem | Inset of input icons |
--surface-lc-1…-9 | derived | Background stops, light to dark |
--text-lc-1…-9 | derived | Foreground stops, high to low contrast |
--primary-surface-lc | stop 6 | Primary button background |
--primary-text-lc | stop 1 | Primary button text |
--grey-surface-lc-* | derived | Chroma-free equivalents, used by disabled states |
--primary-color is declared on :root but is not read by any rule.
Primary buttons use --primary-surface-lc against the ambient --color.
Variables cascade, so overriding one on a subtree is the whole customization story. No build step is involved in any of this.
Different hue, different radius, same markup.
<article class="card" style="--color: seagreen; --border-radius: 1rem">
<h3>Scoped surface</h3>
<p>Different hue, different radius, same markup.</p>
<button>Themed button</button>
</article>
Without a data-color-scheme attribute, the scheme follows
prefers-color-scheme. Set the attribute to pin it, on <html> or on
any subtree.
Forced light
Forced dark
<div class="flex flip">
<div data-color-scheme="light" class="scheme-pane">
<p><strong>Forced light</strong></p>
<button>Button</button>
</div>
<div data-color-scheme="dark" class="scheme-pane">
<p><strong>Forced dark</strong></p>
<button>Button</button>
</div>
</div>
The nine stops live in $lcArray in vars.scss, as
(lightness, chroma) pairs from lightest to darkest. $dimLcArray is the
greyscale counterpart used by disabled states.
$lcArray: (
(99%, 0.018),
(90.5%, 0.045),
(80.5%, 0.125),
(69.5%, 0.17),
(58%, 0.185),
(46%, 0.17),
(34%, 0.135),
(24.5%, 0.095),
(17%, 0.055)
);
If you change these, check contrast in both schemes. Light mode resolves
text-lc-N on surface-lc-M to stops (M, 10 - N); dark mode
resolves the same pairing to stops (10 - M, N). Those are different swatches, so
passing in one scheme does not imply passing in the other.
| Signature | Returns |
|---|---|
cur-color($lc) | The ambient --color at stop $lc |
get-color($lc, $color) | An explicit color at stop $lc |
get-grey($lc, $color) | The same, with chroma forced to zero |
get-border-color() | Shorthand for cur-color(var(--surface-lc-4)) |
oklch-from-vars($color, $lightness) | Lightness override, chroma and hue preserved |
@include apply-greyscale() | Swaps the live scale for the grey one on this element and its subtree |
@include lightSchemeVars() | Emits the full light-mode variable block |
@include darkSchemeVars() | Emits the full dark-mode variable block |
@use "@sparkstone/css/src/vars.scss" as *;
.alert {
--color: var(--error-color);
background-color: cur-color(var(--surface-lc-2));
color: cur-color(var(--text-lc-1));
border: var(--border-width) solid get-border-color();
border-radius: var(--border-radius);
padding: var(--padding);
}
That is the pattern for your own components: rebind --color if you want a different
hue, then pull surfaces and text from the same stops the framework uses. You inherit the contrast
guarantees for free.
Three icons ship as base64 SVG data URLs, redefined per scheme so the stroke flips:
--icon-chevron for select, --icon-calendar for date,
--icon-clock for time. Override any of them with your own data URL.
select {
--icon-chevron: url("data:image/svg+xml;base64,…");
}