Drakk3 Portfolio uses Tailwind CSS v4 with a custom theme system that bridges compatibility with shadcn/ui components. This guide explains how the dual-layer theme configuration works.
Architecture Overview
The theme system uses two layers:
@theme directive - Tailwind v4’s native configuration in src/styles/global.css
:root variables - HSL value bridge for shadcn/ui compatibility
Tailwind v4 is configured via the Vite plugin (@tailwindcss/vite), not PostCSS. The tailwind.config.mjs exists alongside for shadcn/ui compatibility, but @theme in global.css is the source of truth.
The @theme Directive
Tailwind v4 introduces the @theme directive for defining design tokens directly in CSS. All color, spacing, and animation tokens are defined here.
Color Tokens
Colors use the --color-* prefix and include full HSL values:
@theme {
/* Colors */
--color-background: hsl(240 5% 4%);
--color-foreground: hsl(0 0% 94%);
--color-card: hsl(240 4% 10%);
--color-card-foreground: hsl(0 0% 94%);
--color-primary: hsl(0 0% 100%);
--color-primary-foreground: hsl(0 0% 0%);
--color-secondary: hsl(240 4% 16%);
--color-secondary-foreground:hsl(0 0% 94%);
--color-muted: hsl(240 4% 16%);
--color-muted-foreground: hsl(240 5% 65%);
--color-accent: hsl(240 4% 16%);
--color-accent-foreground: hsl(0 0% 94%);
--color-destructive: hsl(0 72% 51%);
--color-destructive-foreground: hsl(0 0% 100%);
--color-border: hsl(240 4% 16%);
--color-input: hsl(240 4% 16%);
--color-ring: hsl(0 0% 100%);
/* Border radius */
--radius-sm: calc(0 .5rem - 4px);
--radius-md: calc(0 .5rem - 2px);
--radius-lg: 0.5rem;
/* Animations */
--animate-marquee: marquee 30s linear infinite;
--animate-fade-up: fade-up 0 .5s ease both;
--animate-spin: spin 0 .7s linear infinite;
}
Usage in Tailwind Classes
These tokens are automatically available as Tailwind utilities:
// Backgrounds
< div className = "bg-background" />
< div className = "bg-card" />
< div className = "bg-primary" />
// Text colors
< p className = "text-foreground" />
< p className = "text-muted-foreground" />
// Borders
< div className = "border-border" />
< div className = "ring-ring" />
The shadcn HSL Bridge
shadcn/ui components expect HSL values without the hsl() wrapper. The :root layer provides this compatibility:
:root {
--background : 240 5 % 4 % ;
--foreground : 0 0 % 94 % ;
--card : 240 4 % 10 % ;
--card-foreground : 0 0 % 94 % ;
--primary : 0 0 % 100 % ;
--primary-foreground : 0 0 % 0 % ;
--secondary : 240 4 % 16 % ;
--secondary-foreground : 0 0 % 94 % ;
--muted : 240 4 % 16 % ;
--muted-foreground : 240 5 % 65 % ;
--accent : 240 4 % 16 % ;
--accent-foreground : 0 0 % 94 % ;
--destructive : 0 72 % 51 % ;
--destructive-foreground : 0 0 % 100 % ;
--border : 240 4 % 16 % ;
--input : 240 4 % 16 % ;
--ring : 0 0 % 100 % ;
--radius : 0.5 rem ;
}
Why Two Layers?
@theme (Tailwind v4)
Native Tailwind v4 tokens
Uses --color-* prefix
Includes full hsl() wrapper
Used by Tailwind utilities
:root (shadcn)
Compatibility with shadcn/ui
No prefix, just --name
HSL values only (no wrapper)
Used by hsl(var(--name)) syntax
shadcn Component Usage
shadcn components reference colors using hsl(var(--name)):
src/components/ui/card.tsx
const Card = React . forwardRef < HTMLDivElement , React . HTMLAttributes < HTMLDivElement >>(
({ className , ... props }, ref ) => (
< div
ref = { ref }
className = { cn (
'rounded-lg border border-border bg-card text-card-foreground' ,
className
) }
{ ... props }
/>
)
);
The tailwind.config.mjs extends these variables:
theme : {
extend : {
colors : {
background : 'hsl(var(--background))' ,
foreground : 'hsl(var(--foreground))' ,
card : {
DEFAULT : 'hsl(var(--card))' ,
foreground : 'hsl(var(--card-foreground))' ,
},
// ... more colors
},
},
},
Dark-Only Design
This portfolio uses a dark-only design with no light mode toggle. All colors are optimized for dark backgrounds.
There is no light mode color scheme. The darkMode: 'class' in tailwind.config.mjs exists for shadcn compatibility, but the theme is intentionally dark-only.
Color Philosophy
Background : Near-black (hsl(240 5% 4%))
Foreground : Off-white (hsl(0 0% 94%))
Primary : Pure white (hsl(0 0% 100%))
Muted : Zinc-based grays for subtle elements
Accent : Minimal, matching secondary for consistency
Customizing the Theme
Changing Colors
To change colors, update both layers in src/styles/global.css:
Update @theme
Modify the --color-* variables with full hsl() values: @theme {
--color-primary: hsl(220 90% 56%); /* custom blue */
}
Update :root
Update the corresponding :root variable with HSL values only: :root {
--primary : 220 90 % 56 % ; /* custom blue, no hsl() */
}
Test in browser
Changes apply immediately in dev mode. Check both Tailwind utilities and shadcn components.
Adding New Colors
Follow the same pattern for custom colors:
@theme {
--color-success: hsl(142 76% 36%);
--color-warning: hsl(38 92% 50%);
}
:root {
--success : 142 76 % 36 % ;
--warning : 38 92 % 50 % ;
}
Then extend tailwind.config.mjs:
theme : {
extend : {
colors : {
success : 'hsl(var(--success))' ,
warning : 'hsl(var(--warning))' ,
},
},
},
Border Radius System
Border radius tokens are defined in @theme:
@theme {
--radius-sm: calc(0 .5rem - 4px); /* 4px */
--radius-md: calc(0 .5rem - 2px); /* 6px */
--radius-lg: 0.5rem; /* 8px */
}
The :root layer provides a single value for shadcn:
:root {
--radius : 0.5 rem ;
}
Usage in tailwind.config.mjs:
borderRadius : {
lg : 'var(--radius)' ,
md : 'calc(var(--radius) - 2px)' ,
sm : 'calc(var(--radius) - 4px)' ,
},
Best Practices
Use Tailwind utilities Prefer bg-background over custom CSS when possible
Keep layers in sync Always update both @theme and :root when changing colors
Test shadcn components Verify changes work with both custom and shadcn components
Use semantic names Follow the naming pattern: background, foreground, primary, etc.
Related Resources
Color Tokens Complete reference of all color tokens and their HSL values
Animations Animation system and keyframe definitions