Ink for what is static
Headings, body copy, captions and card surfaces use --ink, --ink-soft, --paper and
--paper-sunk. If a visitor cannot operate it, it does not get the accent.
The astro-basics visual direction is seven colour tokens, three type roles, and one structural rule that ties colour to interactivity. These tokens are the public styling contract for projects that import this package, so the names and their meanings are stable and every one of them is overridable from a consumer stylesheet.
The direction is one idea specific to Astro rather than to websites in general.
Astro ships static HTML and withholds JavaScript. The palette says the same thing the framework does: static content
renders in ink on paper, and the accent, --island, appears only where something is interactive - links, buttons,
focus rings, the interactive-specimen badge. Nothing decorative carries it.
Ink for what is static
Headings, body copy, captions and card surfaces use --ink, --ink-soft, --paper and
--paper-sunk. If a visitor cannot operate it, it does not get the accent.
Island for what responds
Links, buttons, focus rings and the specimen badge use --island and --island-bg. The accent
is a signal that something will react.
Defined in src/styles/_design-tokens.scss and consumed in src/styles/_base.scss.
| Token | Light | Dark | Role |
|---|---|---|---|
--ink |
#101418 |
#e8eaed |
Primary text; the default color on body |
--ink-soft |
#5a6472 |
#99a2ad |
Secondary text: decks, eyebrows, labels, captions, inline code |
--paper |
#fcfcfd |
#0d1014 |
Page background |
--paper-sunk |
#f1f3f5 |
#151a20 |
Recessed surfaces: the header band, card backgrounds |
--island |
#0b6070 |
#6bb9c9 |
The accent. Interactive only |
--island-bg |
#e6f2f6 |
#102a33 |
Accent wash behind interactive affordances and ::selection |
--rule |
#dde1e6 |
#262c33 |
Hairlines and dividers |
The accent is a deep petrol — a low-chroma blue-green. It is deliberately neither Tailwind sky-500 nor the
violet/indigo family that generated palettes converge on, so the page does not read as a framework default. Low chroma
is the point: the accent sits beside ink as a second voice rather than shouting over it, and it darkens cleanly under
the color-mix hover states instead of going muddy. Every pair clears WCAG 2.1 Level AA. Measured against the running
page rather than estimated: in light, ink on paper is 18.04:1, ink-soft on paper 5.85:1, island on paper 7.01:1, and the
lowest pair of all — ink-soft on paper-sunk — 5.39:1; in dark, island on paper is 8.54:1 and the lowest pair — again
ink-soft on paper-sunk — is 6.77:1. e2e/home-accessibility.spec.ts asserts the 4.5:1 AA floor against the tokens the
page actually resolves, so no repoint can drop a pair below AA without failing the build. The exact figures above are
not themselves asserted — they record the current budget, and re-measuring is part of changing a token.
Both themes are SCSS mixins applied through three scopes:
:root { @include direction-light;}
@media (prefers-color-scheme: dark) { :root { @include direction-dark; }
@include direction-dark-surfaces;}
:root[data-theme='dark'] { @include direction-dark; @include direction-dark-surfaces;}
:root[data-theme='light'] { @include direction-light;}:root[data-theme] carries specificity (0,2,0) against the media query’s (0,1,0), so an explicit toggle beats the OS
preference regardless of source order. Starlight already stamps that attribute on these docs routes.
Two component aliases sat in the token file with zero consumers, which is why every painted surface inherited @fpkit/acss defaults. Both now point at direction tokens and both have real painted consumers.
Aliased to var(--paper-sunk) and painted by body [data-card], plus the same selector inside the dark scopes.
[data-card] is the attribute @fpkit/acss puts on its Card root, so the alias reaches:
Card.astro everywhere it renders, including the six homepage feature cards and the hero specimenFeatureCards.astroFeatured.astroDashboardCard and anything else composed on the @fpkit/acss Cardbody [data-card] { background-color: var(--card-background);}The selector is scoped through body on purpose. The vendor rule is (0,1,0); body [data-card] is (0,1,1) and
wins on specificity rather than on whichever stylesheet the bundler emits last. It also leaves the card’s own
header strip alone.
Aliased to var(--paper-sunk) and painted by body > header, which covers the shared Header.astro band on every
route and the HomeHero.astro band on the homepage.
body > header { background-color: var(--header-background);}The child combinator is load-bearing twice over: (0,0,2) beats the vendor’s (0,0,1), and it cannot reach a nested
header inside a card.
A third header token, --header-deck-font-size, sizes the deck paragraph under the shared header heading. It is a
fluid clamp() on a shallower slope than the heading’s own clamp, so the two separate as the viewport grows
instead of converging.
One typeface used to render across every heading, paragraph, link and button, so elements differed only by size. Three roles is the smallest set that reads as deliberate.
| Role | Token | Applies to | Treatment |
|---|---|---|---|
| Display | --font-family-display |
h1 through h6 |
Inter 600, negative tracking |
| Body | --font-family-sans |
body, p, li, everything inheriting from body |
System sans stack |
| Mono | --font-family-mono |
code, kbd, samp, [data-ui="eyebrow"], [data-ui="label"] |
Uppercase, 0.12em tracking, --ink-soft |
h1 through h6 all render at weight 600, which is the single weight the
self-hosted woff2 ships - so the full range costs no additional font request.text-transform: capitalize stays on h1-h3, which are titles. h4-h6 are
used for UI labels - the contact form renders its error summary as an h6 - and capitalizing a sentence would
produce “Please Correct The Following Errors”.-0.015em on h1-h6, tightened to -0.02em on h1.body, not html. @fpkit/acss sets font-family on html, which is why one face used to
render everywhere. Declaring the body role on body beats that by inheritance and leaves the headings free.The display face is self-hosted: a latin-subset, single-weight inter-600.woff2 in public/fonts/, declared
with font-display: swap and preloaded from src/layouts/Base.astro. No CDN and exactly one font request.
<p data-ui="eyebrow">30+ components / zero client JS by default</p><figcaption data-ui="label">Rendered</figcaption>Both attributes resolve to the mono family, --ink-soft, 0.75rem, 0.12em tracking and uppercase.
Projects importing this package get the components, and the components read these custom properties at runtime. Nothing is compiled in, so a consumer rebrands the kit by redefining properties - no fork, no SCSS build, no component edits.
Two rules make an override stick:
:root, in @media (prefers-color-scheme: dark) :root,
and in :root[data-theme='dark'] and :root[data-theme='light']. Redefining only :root leaves the dark path on
the kit’s values./* your-app/src/styles/brand.css - imported after the kit's index.css */
:root,:root[data-theme='light'] { --ink: #12100e; --ink-soft: #6b6257; --paper: #fffdf9; --paper-sunk: #f5f0e6; --island: #b4531a; --island-bg: #fdeee2; --rule: #e4dbcb;}
@media (prefers-color-scheme: dark) { :root { --ink: #f4efe7; --ink-soft: #a89a88; --paper: #14110d; --paper-sunk: #1d1913; --island: #ff9a5c; --island-bg: #2a1a10; --rule: #2e2820; }}
:root[data-theme='dark'] { --ink: #f4efe7; --ink-soft: #a89a88; --paper: #14110d; --paper-sunk: #1d1913; --island: #ff9a5c; --island-bg: #2a1a10; --rule: #2e2820;}You do not need to re-declare the painted rules. body, body > header, body [data-card] and ::selection all
resolve through var(), so redefining the variables repaints every surface that consumes them.
:root { /* Keep a fallback chain. The kit's @font-face rule and the Base.astro preload still reference the bundled Inter file. */ --font-family-display: 'Söhne Breit', ui-sans-serif, system-ui, sans-serif; --font-family-sans: 'Söhne', ui-sans-serif, system-ui, sans-serif; --font-family-mono: 'Berkeley Mono', ui-monospace, monospace;}The alias layer is a second, narrower override surface. Re-point an alias when a surface should diverge from the direction rather than follow it.
:root { --card-background: var(--paper); /* flush cards instead of recessed */ --header-background: transparent; /* no header band at all */ --header-deck-font-size: clamp(1rem, 0.8vw + 0.85rem, 1.375rem); --card-radius: 0;}--island is the only token bound to a structural rule. If you re-point it, keep it on interactive surfaces only.
Applying your accent to a heading, a decorative border or a static card background reintroduces exactly the ambiguity
the direction removes - and it fails e2e/homepage-design-direction.spec.ts if you run this project’s suite.
Measured against the running dev server in both themes:
| Surface | Light | Dark |
|---|---|---|
body background |
rgb(252,252,253) |
rgb(13,16,20) |
body colour |
rgb(16,20,24) |
rgb(232,234,237) |
body > header (--header-background) |
rgb(241,243,245) |
rgb(21,26,32) |
[data-card] (--card-background) |
rgb(241,243,245) |
rgb(21,26,32) |
Link colour (--island) |
rgb(11,96,112) |
rgb(107,185,201) |
code colour (--ink-soft) |
rgb(90,100,114) |
rgb(153,162,173) |
Automated coverage:
e2e/homepage-design-direction.spec.ts - three distinct font families resolve, the display-to-deck ratio clears 3.0,
both aliases have at least one consumer, body background differs between light and dark, no non-interactive element
carries the accent, and horizontal overflow is 0 at 320, 390, 768 and 1280tests/integration/design-tokens.test.ts - --card-background and --header-background each keep at least one
var() consumer in the compiled CSS, and the dark block sets at least one painted property rather than variables
alone| File | Purpose |
|---|---|
src/styles/_design-tokens.scss |
Token definitions, both themes, the font face |
src/styles/_base.scss |
The painted consumers and the type roles |
src/components/astro/HomeHero.astro |
Homepage hero and the live component specimen |
src/styles/DESIGN-TOKENS-README.md |
The wider token layer this direction extends |
project-docs/03-features/design-direction.md |
The same contract in the project docs |