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
@themedirective. You’ll define colors and typography once, get auto-generated utility classes, and implement dark mode without writingdark: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:
- Astro Fonts Guide: https://docs.astro.build/en/guides/fonts/
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:
- Tailwind Theme Configuration: https://tailwindcss.com/docs/theme
- Tailwind Color Reference: https://tailwindcss.com/docs/colors
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→ generatestext-text-primary,bg-text-primary,border-text-primary--color-bg-primary→ generatesbg-bg-primary, etc.--text-xs→ generatestext-xsutility class--font-heading→ generatesfont-headingutility 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:
- Tailwind Dark Mode: https://tailwindcss.com/docs/dark-mode
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:
- Light mode colors are defined in
@theme - Dark mode colors override them in
.darkclass - A JavaScript toggle adds/removes
.darkon<html> - All utilities like
bg-bg-primaryautomatically switch, nodark: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-primaryinstead ofbg-(--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
Step 6 - Build a Simple Layout with Header and Footer
Let’s create a base layout that every page can reuse.
This layout uses Astro’s component slot pattern:
- Astro Components Guide: https://docs.astro.build/en/basics/astro-components/
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:
bodyis a flex container withmin-h-screento ensure full viewport heightmainusesflex-1so content fills space and pushes footer to bottom- Shared
max-w-*wrappers keep content centered and readable - Theme utilities like
bg-bg-primaryandtext-text-primarywork 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
localStoragefor a saved theme - Falls back to
prefers-color-scheme: dark - Adds or removes the
.darkclass on<html>
- Checks
- 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
@themedirective - ✅ 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-accent→text-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.