Skip to content

Theme System

The template supports full theme switching — brand color, fonts, dark/light mode, and layout variants — all config-driven with zero runtime JavaScript.


Layout.astro reads src/config/theme.ts at build time and generates an inline <style> tag injected into <head>:

@font-face {
font-family: "Inter Variable";
src: url("/fonts/inter-variable.woff2") format("woff2");
}
@font-face {
font-family: "Plus Jakarta Sans Variable";
src: url("/fonts/plus-jakarta-sans-variable.woff2") format("woff2");
}
:root {
--color-brand-50: #edfff6;
--color-brand-100: #c8ffea;
/* ... all 11 stops ... */
--font-sans: "Inter Variable", system-ui, sans-serif;
--font-display: "Plus Jakarta Sans Variable", "Inter Variable", sans-serif;
}

This inline <style> appears after the <link rel="stylesheet"> in the HTML. In the CSS cascade, same-origin author stylesheets are ordered by source order — so the inline block always wins over the compiled Tailwind sheet.

src/styles/global.css declares @theme { --color-brand-*: <defaults>; } which Tailwind compiles into utility classes (bg-brand-500, text-brand-400, etc.) that reference those CSS custom properties. The inline :root override swaps the actual values — all utilities pick up the new palette automatically.

Result: build-time theming with zero runtime JS.


Change the one import in src/config/theme.ts:

// Change this line to switch the entire site theme:
import { darkGreen } from "../themes/dark-green"; // jujualpha default
// import { darkBlue } from "../themes/dark-blue";
// import { darkPurple } from "../themes/dark-purple";
// import { lightNeutral } from "../themes/light-neutral";
export const theme: Theme = darkGreen;
Preset Mode Accent Cards
dark-green dark Green (jujualpha) bordered
dark-blue dark Blue bordered
dark-purple dark Purple elevated
light-neutral light Gray flat

Copy src/themes/dark-green.ts and replace the brand color scale:

import type { Theme } from "./types";
export const myTheme: Theme = {
mode: "dark",
colors: {
brand: {
50: "#fff7ed",
100: "#ffedd5",
200: "#fed7aa",
300: "#fdba74",
400: "#fb923c",
500: "#f97316",
600: "#ea6c0c",
700: "#c2540a",
800: "#9a3c06",
900: "#7c2d12",
950: "#431407",
},
},
fonts: {
sans: { family: "Inter Variable", src: "/fonts/inter-variable.woff2" },
display: { family: "Plus Jakarta Sans Variable", src: "/fonts/plus-jakarta-sans-variable.woff2" },
},
layout: {
hero: "centered",
nav: "fixed",
cards: "bordered",
},
};

Use uicolors.app to generate a full 50–950 scale from a single hex value.

For dark themes: 50 should be near-white, 950 near-black. 500 is typically the primary action color.

Then update the import in src/config/theme.ts:

import { myTheme } from "../themes/my-theme";
export const theme: Theme = myTheme;

If the client has a different typeface:

  1. Get the .woff2 variable font file (Google Fonts → Download family → Variable)
  2. Copy to public/fonts/
  3. Update the theme file:
fonts: {
sans: { family: "Nunito Variable", src: "/fonts/nunito-variable.woff2" },
display: { family: "Sora Variable", src: "/fonts/sora-variable.woff2" },
},
  1. Update the defaults in src/styles/global.css so Tailwind’s font-sans and font-display utilities have the right fallback:
@theme {
--font-sans: "Nunito Variable", system-ui, sans-serif;
--font-display: "Sora Variable", "Nunito Variable", sans-serif;
}

theme.mode sets "dark" or "light". Layout.astro adds the value as a class on <html>:

<html class:list={[theme.mode, "scroll-smooth"]}>

Body background and text colour switch based on this class:

<body class:list={[
isDark ? "bg-gray-950 text-gray-100" : "bg-white text-gray-900",
"font-sans antialiased"
]}>

Mode is set at build time per client. There is no dark/light toggle by default — add one later if the client requests it.


Value Layout
"centered" Text and CTAs centered, single column
"split" Text left, image/graphic right (two columns)
"fullscreen" Full-bleed background, overlay text
Value Behaviour
"fixed" Stays at top of viewport at all times
"sticky" Scrolls away, then sticks when scrolling back up
"static" Flows with page, no sticky behaviour
Value Style
"bordered" border border-white/10, no shadow
"elevated" shadow-lg shadow-black/20, no border
"flat" Neither border nor shadow

Components that render cards read theme.layout.cards and apply the correct class automatically.