Theming
Customize the look and feel of Supasheet
Overview
Supasheet uses Tailwind CSS 4 and CSS variables for theming, making it easy to customize colors, typography, and overall appearance.
Theme Configuration
Environment Variables
NEXT_PUBLIC_DEFAULT_THEME_MODE=system # light | dark | system
NEXT_PUBLIC_THEME_COLOR=#3b82f6 # Light mode primary color
NEXT_PUBLIC_THEME_COLOR_DARK=#2563eb # Dark mode primary color
Color System
CSS Variables
Colors are defined in /app/global.css
:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
}
Customizing Colors
/* Custom brand colors */
:root {
--brand-primary: 210 100% 50%;
--brand-secondary: 280 100% 50%;
--brand-accent: 150 100% 45%;
}
.dark {
--brand-primary: 210 100% 60%;
--brand-secondary: 280 100% 60%;
--brand-accent: 150 100% 55%;
}
Using Theme Colors
In Tailwind Classes
<div className="bg-primary text-primary-foreground">
Primary background
</div>
<div className="bg-secondary text-secondary-foreground">
Secondary background
</div>
<button className="bg-destructive text-destructive-foreground">
Delete
</button>
Custom Color Utilities
// Using brand colors
<div className="bg-[hsl(var(--brand-primary))]">
Brand primary
</div>
Theme Switcher
Basic Theme Toggle
'use client';
import { useTheme } from 'next-themes';
import { Moon, Sun } from 'lucide-react';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<Button
variant="outline"
size="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
);
}
Dropdown Theme Selector
'use client';
import { useTheme } from 'next-themes';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import { Moon, Sun, Monitor } from 'lucide-react';
export function ThemeSelector() {
const { theme, setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
{theme === 'light' && <Sun className="w-4 h-4" />}
{theme === 'dark' && <Moon className="w-4 h-4" />}
{theme === 'system' && <Monitor className="w-4 h-4" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setTheme('light')}>
<Sun className="w-4 h-4 mr-2" /> Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<Moon className="w-4 h-4 mr-2" /> Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<Monitor className="w-4 h-4 mr-2" /> System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Typography
Font Configuration
// app/layout.tsx
import { Inter, JetBrains_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
variable: '--font-sans',
});
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
<body>{children}</body>
</html>
);
}
Tailwind Configuration
// tailwind.config.js
module.exports = {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],
mono: ['var(--font-mono)', 'monospace'],
},
},
},
};
Custom Themes
Create Theme Preset
/* global.css */
[data-theme='ocean'] {
--background: 210 100% 97%;
--foreground: 210 100% 10%;
--primary: 200 100% 45%;
--primary-foreground: 0 0% 100%;
--secondary: 190 100% 40%;
--accent: 170 100% 35%;
--muted: 210 40% 96%;
--border: 210 40% 90%;
}
[data-theme='forest'] {
--background: 120 20% 97%;
--foreground: 120 20% 10%;
--primary: 140 60% 40%;
--primary-foreground: 0 0% 100%;
--secondary: 100 60% 35%;
--accent: 80 70% 45%;
--muted: 120 20% 96%;
--border: 120 20% 90%;
}
Theme Switcher with Presets
'use client';
import { useState } from 'react';
const themes = ['default', 'ocean', 'forest', 'sunset'];
export function ThemePresetSelector() {
const [selectedTheme, setSelectedTheme] = useState('default');
function applyTheme(theme: string) {
document.documentElement.setAttribute('data-theme', theme);
setSelectedTheme(theme);
}
return (
<div className="flex gap-2">
{themes.map((theme) => (
<button
key={theme}
onClick={() => applyTheme(theme)}
className={`px-4 py-2 rounded ${
selectedTheme === theme ? 'bg-primary text-primary-foreground' : 'bg-secondary'
}`}
>
{theme}
</button>
))}
</div>
);
}
Component Styling
Using Variants
import { cn } from '@/lib/utils';
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
sm: 'h-9 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-11 px-8',
},
},
defaultVariants: {
variant: 'default',
size: 'md',
},
}
);
export function CustomButton({
variant,
size,
className,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
}
Spacing and Layout
Custom Spacing
// tailwind.config.js
module.exports = {
theme: {
extend: {
spacing: {
'18': '4.5rem',
'88': '22rem',
'128': '32rem',
},
},
},
};
Container Sizes
.container {
@apply mx-auto px-4 sm:px-6 lg:px-8;
max-width: 1280px;
}
.container-sm {
@apply mx-auto px-4;
max-width: 640px;
}
.container-lg {
@apply mx-auto px-4;
max-width: 1536px;
}
Animations
Custom Animations
// tailwind.config.js
module.exports = {
theme: {
extend: {
keyframes: {
'fade-in': {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
'slide-up': {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
},
animation: {
'fade-in': 'fade-in 0.3s ease-out',
'slide-up': 'slide-up 0.3s ease-out',
},
},
},
};
Usage
<div className="animate-fade-in">
Fades in on mount
</div>
<div className="animate-slide-up">
Slides up on mount
</div>
Responsive Design
Breakpoints
// tailwind.config.js
module.exports = {
theme: {
screens: {
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
},
},
};
Responsive Utilities
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Responsive grid */}
</div>
<div className="text-sm md:text-base lg:text-lg">
{/* Responsive text */}
</div>
<div className="p-4 md:p-6 lg:p-8">
{/* Responsive padding */}
</div>
Dark Mode Images
Conditional Images
import Image from 'next/image';
export function Logo() {
return (
<>
<Image
src="/logo-light.png"
alt="Logo"
className="block dark:hidden"
width={200}
height={50}
/>
<Image
src="/logo-dark.png"
alt="Logo"
className="hidden dark:block"
width={200}
height={50}
/>
</>
);
}
Custom Shadcn Components
Override Component Styles
// components/ui/custom-button.tsx
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
export function BrandButton({ className, ...props }: React.ComponentProps<typeof Button>) {
return (
<Button
className={cn(
'bg-[hsl(var(--brand-primary))] hover:bg-[hsl(var(--brand-primary))]/90',
className
)}
{...props}
/>
);
}
Best Practices
1. Use CSS Variables
/* Good ✅ */
:root {
--spacing-unit: 0.25rem;
--border-radius: 0.5rem;
}
.card {
padding: calc(var(--spacing-unit) * 4);
border-radius: var(--border-radius);
}
2. Consistent Naming
/* Good ✅ */
--primary
--primary-foreground
--secondary
--secondary-foreground
/* Bad ❌ */
--blue
--text-on-blue
--green
--green-text
3. Theme Scoping
/* Good ✅ - Scoped to data attribute */
[data-theme='custom'] {
--primary: 200 100% 50%;
}
/* Bad ❌ - Global override */
.custom-theme {
background: blue;
}
Next Steps
- Custom Components - Build themed components
- Internationalization - Multi-language support
- CRUD Operations - Themed auto-generated UI