Skip to content
AstroCraft Docs
On this theme

Colors & Theming

8-BitQuest uses Tailwind CSS v4, which means configuration lives in CSS rather than a JavaScript file. There is no tailwind.config.js anywhere in the project. Colour is organised in three layers across two files, and understanding them is the difference between rebranding in a few edits and hunting through components.

The three layers

Layer 1 — the brand palette ramps. In src/styles/tailwind-theme.css, an @theme block defines four ramps as direct hex, each stepped -50 through -950:

@theme {
  --color-primary-500: #41a6f6;   /* the retro action/heading blue */
  --color-secondary-200: #ffb2ba; /* the retro pink accent */
  --color-success-400: #63de86;   /* the retro action green */
  --color-base-500: #3f4358;      /* the single cool navy ramp both themes are cut from */
  /* … each ramp full -50…-950 */
}

These are real values — text-primary-500, bg-base-900, text-success-400 — that do not respond to the theme. 8-BitQuest deliberately dropped the Tailwind-scale aliasing (--color-primary: var(--color-violet-500)) that the skeleton shipped with; the ramps are hand-tuned hex from the Figma frame, so repointing them is the whole rebrand.

Layer 2 — semantic runtime variables. In src/styles/global.css, a @layer base block declares the semantic names twice, once for :root (light) and once for .dark. These are what flip:

:root {
  --background: #d6e3f4;                  /* clean light retro-blue desktop */
  --foreground: var(--color-base-900);    /* navy ink */
  --card: #ffffff;                        /* crisp white window panel */
  --primary: var(--color-primary-700);    /* action blue, deepened for the light ground */
  --border: #6a769a;
  --radius: 0;                            /* sharp pixel corners — no rounding */
  --pixel-shadow: #6a769a;
}

.dark {
  --background: var(--color-base-900);    /* #101222 navy page */
  --foreground: var(--color-base-100);    /* light ink */
  --card: var(--color-base-800);          /* navy panel */
  --primary: var(--color-primary-500);    /* #41a6f6 action blue */
  --border: #000000;                      /* pixel borders stay pure black */
  --pixel-shadow: #000000;                /* the signature hard black shadow */
}

Layer 3 — the bridge. Back in tailwind-theme.css, an @theme inline block turns those runtime variables into utilities:

@theme inline {
  --color-background: var(--background);
  --color-primary: var(--primary);
  --color-border: var(--border);
}

The inline keyword is required. Without it Tailwind resolves the variable to its value at build time and the utility stops responding to the theme class. So text-foreground resolves through --color-foreground to --foreground to whichever of :root or .dark is active. That chain is the whole system.

The pixel signature

Two tokens carry the retro look, and both are theme-aware in a way worth understanding.

Sharp corners. --radius is 0 in both themes, so every rounded-* utility resolves to a square corner. There is no rounding anywhere on the site by design; the seven derived --radius-* steps all calc() off that zero.

The hard offset shadow. --shadow-pixel is 4px 4px 0 0 with zero blur — the retro “sticker” depth on every panel, card, button and badge, with a larger --shadow-pixel-lg (8px 8px) for the hero. Its colour is itself a token, --pixel-shadow: pure black in dark mode, and a soft slate-navy in light mode — the same colour as --border, so the border and its shadow read as one gentler mark on the light ground. That signature surface is packaged as the ui/pixel-panel primitive (bg-card + a 4px black frame + the pixel shadow), which the cards and article layouts build on.

The complete token set

The semantic layer is a full set of colour pairs plus shape:

  • Surfacesbackground, foreground, card, card-foreground, muted, muted-foreground, accent, accent-foreground
  • Brandprimary, primary-foreground, secondary, secondary-foreground
  • Statusinfo, success, warning, error, each with a -foreground partner
  • Chromeborder, input, outline, and --pixel-shadow
  • Shape--radius (0) and its seven derived steps

Note that success exists both as a semantic token (which flips per theme) and as a full hex ramp (--color-success-*, fixed). That is not an accident: the pixel action buttons need a green that reads identically in both themes, so they reach the fixed ramp rather than the flipping semantic token.

The pixel button

The retro 8-bit button is a shared CSS class, .pixel-btn in global.css, not a primitive — a flat face with an inset depth shadow and a notched black pixel border drawn by two pseudo-elements. Its colour is parameterised by four CSS variables that default to the theme pink, with two modifier classes:

  • .pixel-btn--blue repoints them to the action blue (the “READ BLOG” / “EMAIL ME” CTAs).
  • .pixel-btn--green repoints them to the fixed success green (the contact “SEND MESSAGE” button).

Because the modifiers point at the fixed --color-* ramps rather than the semantic tokens, all three faces read the same in both themes. The ThemeToggle primitive re-skins .pixel-btn into a square icon button, which is why it is the one primitive that composes a bare CSS class rather than the button recipe.

The rule for markup

Never write a raw Tailwind colour in a component. bg-violet-700 and text-zinc-300 bypass the whole system: they will not flip in dark mode and they will not follow a rebrand. Use bg-primary, text-foreground, border-border, bg-muted, or a palette alias like text-base-700 dark:text-base-300 paired so the two themes cannot drift.

Decorative artwork follows the same rule through currentColor: the 404 illustration sets text-primary on its root <svg> and fills its shapes with fill="currentColor", so it inherits the primary token and flips for free with no per-colour CSS.

Dark mode

Declared once, class-based:

@variant dark (&:where(.dark, .dark *));

Paired with html { @apply bg-background text-foreground scheme-light dark:scheme-dark; }, so native UI — scrollbars, form controls — matches the theme too. Light mode is not a cream “paper” theme; it is a cool inversion of the dark navy — a light retro-blue desktop with crisp white panels — with the neutral surfaces tuned as literals (the shared navy ramp leans violet at its light end, so light mode gets its own cleaner blue) while the accents stay tokenised.

The pre-paint script

The theme class is set by an inline script in BaseHead before the browser paints, which is what stops the flash of the wrong theme. A saved visitor choice — written by the ThemeToggle primitive to localStorage["colorTheme"] — wins; otherwise it follows the device prefers-color-scheme, and while the visitor has not pinned a choice it still updates live on OS colour-scheme changes. It re-runs on astro:after-swap so a view transition cannot lose the theme. Keep it inline — moving it into a bundled <script> reintroduces the flash.

Rebranding

Because the ramps are direct hex, a rebrand is repointing them in src/styles/tailwind-theme.css:

  • Change the brand colour — repoint the --color-primary-* steps. Everything follows: the semantic --primary in both themes, every bg-primary* utility, the .main-text-gradient class, the focus ring, and the token-driven illustration.
  • Change the accent or action colours — repoint --color-secondary-* (the pink) or --color-success-* (the green). The pixel-button modifiers read these ramps directly.
  • Change the neutral feel — repoint the --color-base-* ramp. That drives backgrounds, cards, muted surfaces, borders and inputs in both themes.
  • Retune the light/dark mapping without touching the ramps — edit the two :root / .dark blocks in global.css. Keep both in step; that symmetry is the reason layer 2 exists.
  • Soften the pixel look — raise --radius above 0 for rounded corners, or reduce the --shadow-pixel offset. Both are single edits that cascade everywhere.

Verifying

Run pnpm dev and open /examples/ui. It renders all 39 primitives in every variant, with a theme toggle in its header. A missing token shows up instantly as an un-themed element, and it is far faster than clicking through real pages. Check both themes — a token that resolves correctly in dark and wrongly in light is the most common way a palette edit goes half-finished.

NEXT STEPTypography