Skip to content
Le blog de Pascal Andy - L'homme et les relations technologiques

shadcn Theme System

The blog uses shadcn/ui compatible themes, so color schemes from tweakcn.com and other sources drop in as they are.

Overview

The theme system uses CSS custom properties (variables) with the modern oklch() color format for perceptually uniform colors. Each theme provides both light and dark mode variants, which the light/dark toggle switches between.

Key Features

Configuration

Theme Structure

Themes are defined in src/config.ts: THEMES holds every theme, and ACTIVE_THEME names the one the site uses. The example below activates caffeine:

import type { ThemeConfig } from "./types";

export const THEMES: Record<string, ThemeConfig> = {
  caffeine: {
    name: "Caffeine",
    light: {
      background: "oklch(0.9821 0 0)",
      foreground: "oklch(0.2435 0 0)",
      primary: "oklch(0.4341 0.0392 41.9938)",
      // ... other colors
    },
    dark: {
      background: "oklch(0.1776 0 0)",
      foreground: "oklch(0.9491 0 0)",
      primary: "oklch(0.9247 0.0524 66.1732)",
      // ... other colors
    },
  },
};

export const ACTIVE_THEME: keyof typeof THEMES = "caffeine";

Switching Themes

To switch themes, simply change the ACTIVE_THEME value:

// Change from "caffeine" to your desired theme
export const ACTIVE_THEME: keyof typeof THEMES = "yourThemeName";

Then rebuild the site:

just build

Adding New Themes

From tweakcn.com

  1. Browse themes at tweakcn.com

  2. Get the theme JSON:

    curl https://tweakcn.com/r/themes/THEME_NAME.json
  3. Extract the colors from cssVars.light and cssVars.dark

  4. Add to src/config.ts:

    export const THEMES: Record<string, ThemeConfig> = {
      caffeine: {
        /* existing */
      },
    
      newTheme: {
        name: "New Theme",
        light: {
          background: "oklch(...)",
          foreground: "oklch(...)",
          // ... paste light colors
        },
        dark: {
          background: "oklch(...)",
          foreground: "oklch(...)",
          // ... paste dark colors
        },
      },
    };
  5. Activate the theme:

    export const ACTIVE_THEME: keyof typeof THEMES = "newTheme";

Using the Parse Utility

src/utils/parseTheme.ts converts tweakcn JSON to the config format:

import { parseTweakcnTheme, formatThemeAsTS } from "@/utils/parseTheme";

// Fetch and parse a theme
const response = await fetch("https://tweakcn.com/r/themes/THEME_NAME.json");
const json = await response.json();
const config = parseTweakcnTheme(json);

// Format as TypeScript for copy-pasting
const tsCode = formatThemeAsTS("themeName", config);
console.log(tsCode);

Available CSS Variables

The theme system provides these semantic color variables:

VariablePurpose
--backgroundPage background
--foregroundPrimary text color
--cardCard/surface background
--card-foregroundCard text color
--popoverPopover/dropdown background
--popover-foregroundPopover text color
--primaryPrimary brand/action color
--primary-foregroundText on primary color
--secondarySecondary/subtle actions
--secondary-foregroundText on secondary
--mutedMuted/disabled backgrounds
--muted-foregroundMuted text
--accentAccent/highlight color
--accent-foregroundText on accent
--destructiveError/danger color
--destructive-foregroundText on destructive
--borderBorder color
--inputInput field borders
--ringFocus ring color

Using in Components

In Astro components, use Tailwind utilities:

<!-- Using Tailwind utilities -->
<div class="bg-background text-foreground">
  <a class="text-primary hover:text-primary/80">Link</a>
  <button class="bg-primary text-primary-foreground">Button</button>
</div>

Or use CSS variables directly:

.custom-element {
  background-color: var(--background);
  color: var(--foreground);
  border: 1px solid var(--border);
}

Backward Compatibility

Components inherited from AstroPaper use text-accent; src/styles/global.css maps it to --primary:

Theme Types

The TypeScript types are defined in src/types.ts:

export type ThemeColors = {
  background: string;
  foreground: string;
  card: string;
  "card-foreground": string;
  popover: string;
  "popover-foreground": string;
  primary: string;
  "primary-foreground": string;
  secondary: string;
  "secondary-foreground": string;
  muted: string;
  "muted-foreground": string;
  accent: string;
  "accent-foreground": string;
  destructive: string;
  "destructive-foreground": string;
  border: string;
  input: string;
  ring: string;
};

export type ThemeConfig = {
  name: string;
  light: ThemeColors;
  dark: ThemeColors;
};

How It Works

  1. Build time: Layout.astro reads the active theme from config.ts
  2. CSS injection: Theme colors are injected as CSS variables in the <head>
  3. Tailwind mapping: global.css maps CSS variables to Tailwind color utilities
  4. Runtime toggle: The light/dark toggle switches the data-theme attribute
  5. OG images: src/utils/getOgColors.ts converts the active theme’s colors for the generated OG images; their cache key includes the theme, so switching themes regenerates them

This approach provides:

Example: Creating a Custom Theme

Here’s how to create your own theme:

// In src/config.ts
export const THEMES: Record<string, ThemeConfig> = {
  // ... existing themes

  ocean: {
    name: "Ocean",
    light: {
      background: "oklch(0.98 0.01 220)",
      foreground: "oklch(0.20 0.02 220)",
      card: "oklch(0.99 0.005 220)",
      "card-foreground": "oklch(0.20 0.02 220)",
      popover: "oklch(0.99 0.005 220)",
      "popover-foreground": "oklch(0.20 0.02 220)",
      primary: "oklch(0.50 0.15 220)",
      "primary-foreground": "oklch(1.00 0 0)",
      secondary: "oklch(0.90 0.05 220)",
      "secondary-foreground": "oklch(0.30 0.05 220)",
      muted: "oklch(0.95 0.01 220)",
      "muted-foreground": "oklch(0.50 0.02 220)",
      accent: "oklch(0.92 0.02 220)",
      "accent-foreground": "oklch(0.20 0.02 220)",
      destructive: "oklch(0.60 0.20 25)",
      "destructive-foreground": "oklch(1.00 0 0)",
      border: "oklch(0.88 0.02 220)",
      input: "oklch(0.88 0.02 220)",
      ring: "oklch(0.50 0.15 220)",
    },
    dark: {
      background: "oklch(0.18 0.02 220)",
      foreground: "oklch(0.95 0.01 220)",
      // ... dark mode colors
    },
  },
};

Resources