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 Rule: Colour Marks Hydration
Section titled “The Rule: Colour Marks Hydration”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.
The Seven Direction Tokens
Section titled “The Seven Direction Tokens”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.
How the themes are applied
Section titled “How the themes are applied”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.
Which Surfaces the Aliases Paint
Section titled “Which Surfaces the Aliases Paint”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.astroeverywhere it renders, including the six homepage feature cards and the hero specimen- the promoted-tier specimens in
FeatureCards.astro Featured.astroDashboardCardand anything else composed on the @fpkit/acssCard
body [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.
The Three Type Roles
Section titled “The Three Type Roles”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 |
- Display covers every heading level.
h1throughh6all render at weight 600, which is the single weight the self-hosted woff2 ships - so the full range costs no additional font request. - Casing is not part of the role.
text-transform: capitalizestays onh1-h3, which are titles.h4-h6are used for UI labels - the contact form renders its error summary as anh6- and capitalizing a sentence would produce “Please Correct The Following Errors”. - Tracking is negative on display.
-0.015emonh1-h6, tightened to-0.02emonh1. - The display stack falls back through the system sans, so a failed font request degrades to the body face rather than to a serif.
- Body is declared on
body, nothtml. @fpkit/acss setsfont-familyonhtml, which is why one face used to render everywhere. Declaring the body role onbodybeats that by inheritance and leaves the headings free. - Mono is opt-in for anything that is not code. Code semantics get it by element; everything else opts in through a data attribute, so an eyebrow is a deliberate choice at the call site rather than a selector guessing at class names.
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.
Using the mono role
Section titled “Using the mono role”<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.
Overriding the Tokens as a Consumer
Section titled “Overriding the Tokens as a Consumer”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:
- Load your sheet after the kit’s stylesheet. Every override below has the same specificity as the kit’s own declaration, so source order decides the winner.
- Override all three scopes. The kit sets the tokens in
:root, in@media (prefers-color-scheme: dark) :root, and in:root[data-theme='dark']and:root[data-theme='light']. Redefining only:rootleaves 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;}What an override must not break
Section titled “What an override must not break”--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.
Verification
Section titled “Verification”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-backgroundand--header-backgroundeach keep at least onevar()consumer in the compiled CSS, and the dark block sets at least one painted property rather than variables alone
Related Files
Section titled “Related Files”| 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 |