Skip to content

Beginner Astro Blog with Tailwind & Cloudflare

Part 2: Design System & Typography

Use Tailwind CSS v4, CSS variables, and fontsource to give your Astro blog a clean design, readable typography, and light/dark themes.

In this part, you’ll build a proper design system using Tailwind v4’s @theme directive. You’ll define colors and typography once, get auto-generated utility classes, and implement dark mode without writing dark: prefixes everywhere.

In Part 1, you:

  • Created an Astro v5 project
  • Added Tailwind CSS v4
  • Wired in Cloudflare Wrangler scripts

Now we’ll focus entirely on design and readability.


Why Use Tailwind v4’s @theme Directive?

It’s tempting to sprinkle arbitrary Tailwind classes like text-blue-500 and bg-gray-100 everywhere.

That works for demos, but quickly becomes messy:

  • Colors feel inconsistent across pages
  • Dark mode requires dark: prefix on every single class
  • Changing your brand color means finding and replacing dozens of classes

Tailwind v4’s @theme solves this:

  • Define design tokens once (colors, fonts, sizes)
  • Get auto-generated utility classes (text-accent, bg-bg-primary)
  • Dark mode works automatically by overriding theme variables
  • Change a color in one place, updates everywhere

We’ll also use fontsource to self-host fonts (no external CDN dependencies).

The result: consistent design with Tailwind’s ergonomics.


Step 1 - Install Fonts with fontsource

We’ll use:

  • Outfit (for headings - geometric, modern look)
  • Inter (for body text - highly readable)
  • Fira Code (for code - programmer favorite with ligatures)

Install the font packages:

npm install @fontsource-variable/outfit @fontsource/inter @fontsource/fira-code

Import Fonts in Your Global CSS

Open src/styles/global.css (or create it). At the top, add:

@import '@fontsource-variable/outfit/wght.css';
@import '@fontsource/inter/400.css';
@import '@fontsource/inter/600.css';
@import '@fontsource/fira-code/400.css';
@import '@fontsource/fira-code/500.css';

These imports:

  • Load Outfit Variable for flexible heading weights
  • Load Inter at 400 and 600 weights for body text
  • Load Fira Code for code blocks with ligature support

We’ll hook them into CSS variables next.

For more details on using fonts in Astro projects:


Step 2 - Define Your Design System with Tailwind v4’s @theme

Tailwind v4 introduces the @theme directive, which lets you define design tokens that automatically generate utility classes.

This approach uses Tailwind’s official theming system. Learn more:

At the top of global.css, add your Tailwind import and theme definition:

@import 'tailwindcss';

@theme {
  /* Color tokens - these automatically generate utilities like bg-bg-primary, text-text-primary, etc. */
  --color-bg-primary: #f8fafc; /* slate-50 - cool, clean backgrounds */
  --color-bg-secondary: #f1f5f9; /* slate-100 */
  --color-bg-tertiary: #e2e8f0; /* slate-200 */
  --color-text-primary: #0f172a; /* slate-900 - strong contrast */
  --color-text-secondary: #334155; /* slate-700 */
  --color-text-tertiary: #64748b; /* slate-500 */
  --color-border: #cbd5e1; /* slate-300 */
  --color-border-subtle: #e2e8f0; /* slate-200 */
  --color-accent: #3b82f6; /* blue-500 - vibrant blue for actions */
  --color-accent-hover: #2563eb; /* blue-600 */
  --color-accent-subtle: #dbeafe; /* blue-100 */

  /* Typography scale - generates text-xs, text-sm, etc. */
  --text-xs: 0.75rem; /* 12px - standard Tailwind xs */
  --text-sm: 0.875rem; /* 14px - standard Tailwind sm */
  --text-base: 1rem; /* 16px - standard web base */
  --text-lg: 1.125rem; /* 18px */
  --text-xl: 1.25rem; /* 20px */
  --text-2xl: 1.5rem; /* 24px */
  --text-3xl: 1.875rem; /* 30px */
  --text-4xl: 2.25rem; /* 36px */

  /* Font families - generates font-heading, font-body, font-mono */
  --font-heading: 'Outfit Variable', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  --font-body: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono:
    'Fira Code', ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
}

How @theme Works:

  • --color-text-primary → generates text-text-primary, bg-text-primary, border-text-primary
  • --color-bg-primary → generates bg-bg-primary, etc.
  • --text-xs → generates text-xs utility class
  • --font-heading → generates font-heading utility class

You get the power of CSS variables with automatic Tailwind utility generation. No manual mapping needed!

You can also add regular CSS variables outside the @theme block for values you don’t need as utilities:

:root {
  /* Non-utility variables for direct CSS usage */
  --leading-tight: 1.25;
  --leading-normal: 1.5;
  --leading-relaxed: 1.625;
  --header-height: 3.5rem;
  --toc-width: 14rem;
}

Step 3 - Add Dark Mode Tokens

With Tailwind v4’s @theme, dark mode works by overriding theme variables inside a .dark class. The beauty is that all your utility classes automatically work with both themes.

For more on Tailwind’s dark mode approach:

Add this after your @theme block in global.css:

.dark {
  /* Override theme variables for dark mode */
  --color-bg-primary: #0f172a; /* slate-900 - deep, rich dark */
  --color-bg-secondary: #1e293b; /* slate-800 */
  --color-bg-tertiary: #334155; /* slate-700 */
  --color-text-primary: #f8fafc; /* slate-50 - high contrast */
  --color-text-secondary: #cbd5e1; /* slate-300 */
  --color-text-tertiary: #94a3b8; /* slate-400 */
  --color-border: #475569; /* slate-600 */
  --color-border-subtle: #334155; /* slate-700 */
  --color-accent: #60a5fa; /* blue-400 - brighter for dark mode */
  --color-accent-hover: #93c5fd; /* blue-300 */
  --color-accent-subtle: #1e3a5f; /* dark blue tint */
}

How Dark Mode Works:

  1. Light mode colors are defined in @theme
  2. Dark mode colors override them in .dark class
  3. A JavaScript toggle adds/removes .dark on <html>
  4. All utilities like bg-bg-primary automatically switch, no dark: prefix needed!

This is simpler than traditional Tailwind dark mode because the theme variables handle everything.


Step 4 - Base Global Styles

Add base styles that apply your design tokens to HTML elements:

* {
  box-sizing: border-box;
}

html {
  font-family: var(--font-body);
  background-color: var(--color-bg-primary);
  color: var(--color-text-primary);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

body {
  margin: 0;
  padding: 0;
  font-size: var(--text-base);
  line-height: var(--leading-normal);
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--font-heading);
  font-weight: 600;
  line-height: var(--leading-tight);
  margin: 0;
  color: var(--color-text-primary);
}

h1 {
  font-size: var(--text-4xl);
  font-weight: 700;
}

h2 {
  font-size: var(--text-3xl);
}

h3 {
  font-size: var(--text-2xl);
}

p {
  margin: 0;
  line-height: var(--leading-relaxed);
}

a {
  color: var(--color-accent);
  text-decoration: none;
  transition: color 200ms ease;
}

a:hover {
  color: var(--color-accent-hover);
}

code {
  font-family: var(--font-mono);
  font-size: 0.875em;
}

These base styles ensure consistent typography before you add any Tailwind utilities.


Step 5 - Using Your Theme in Components

Now that you’ve defined your design system with @theme, you can use generated utility classes throughout your site:

<!-- Background and text colors -->
<main class="min-h-screen bg-bg-primary text-text-primary">
  <div class="border border-border bg-bg-secondary p-4">
    <p class="text-text-secondary">Secondary text</p>
  </div>
</main>

<!-- Accent colors for links and highlights -->
<a class="text-accent transition-colors hover:text-accent-hover"> Read more </a>

<!-- Typography utilities -->
<h1 class="font-heading text-4xl">Large Heading</h1>
<p class="font-body text-base">Body text at 15px</p>
<code class="font-mono text-sm">Code snippet</code>

Key Benefits:

  • Clean class names: bg-bg-primary instead of bg-(--color-bg-primary)
  • Dark mode automatic: No dark: prefix needed, theme switches handle it
  • Type-safe: Tailwind IntelliSense recognizes all your custom utilities
  • Maintainable: Change colors in one place, updates everywhere

Let’s create a base layout that every page can reuse.

This layout uses Astro’s component slot pattern:

In src/layouts/BaseLayout.astro:

---
import '../styles/global.css'

const { title = 'My Astro Blog' } = Astro.props
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{title}</title>
  </head>
  <body class="flex min-h-screen flex-col bg-bg-primary text-text-primary">
    <header class="h-14 border-b border-border bg-bg-secondary">
      <div class="mx-auto flex h-full max-w-7xl items-center justify-between px-4">
        <a href="/" class="text-sm font-semibold tracking-tight">My Astro Blog</a>
        <!-- Theme toggle will go here later -->
      </div>
    </header>

    <main class="flex-1 pt-6">
      <div class="mx-auto max-w-5xl px-4 py-8">
        <slot />
      </div>
    </main>

    <footer class="border-t border-border bg-bg-secondary">
      <div class="mx-auto max-w-7xl px-4 py-6 text-xs text-text-secondary">
        © {new Date().getFullYear()} My Astro Blog. All rights reserved.
      </div>
    </footer>
  </body>
</html>

Key Layout Patterns:

  • body is a flex container with min-h-screen to ensure full viewport height
  • main uses flex-1 so content fills space and pushes footer to bottom
  • Shared max-w-* wrappers keep content centered and readable
  • Theme utilities like bg-bg-primary and text-text-primary work automatically

Using the Layout:

---
import BaseLayout from '../layouts/BaseLayout.astro'
---

<BaseLayout title="Home">
  <h1 class="mb-4 text-4xl">Welcome</h1>
  <p class="text-base">This is my new Astro blog with a custom design system.</p>
</BaseLayout>

Step 7 - Planning for a Theme Toggle

We won’t build the full theme toggle component yet, but let’s think about how it will work:

  • A small script runs early and:
    • Checks localStorage for a saved theme
    • Falls back to prefers-color-scheme: dark
    • Adds or removes the .dark class on <html>
  • A button in the header toggles between light and dark

Because your colors are all defined with CSS variables, switching the .dark class will update the entire site instantly.

We’ll wire this up when we build interactive components later.


Recap and What’s Next

You’ve built a solid design foundation:

  • ✅ Installed fonts with fontsource (Outfit + Inter + Fira Code)
  • ✅ Defined a design system using Tailwind v4’s @theme directive
  • ✅ Set up automatic dark mode with CSS variable overrides
  • ✅ Created base global styles for typography and elements
  • ✅ Built a flexible layout with header, content area, and footer

What Makes This Approach Strong:

  • Single source of truth: All colors and typography defined in @theme
  • Auto-generated utilities: --color-accenttext-accent, bg-accent, etc.
  • No dark mode duplication: Theme variables switch automatically, no dark: prefix
  • Maintainable: Change a color once, updates across entire site

In Part 3, we’ll build out the content layer:

  • Setting up Astro content collections for blog posts
  • Creating specialized layouts for posts and pages
  • Adding astro-icon for clean, tree-shakeable icons
  • Building a table of contents component
  • Implementing search and tag filtering

Your blog will transform from a styled shell into a functional content site.