Supasheet

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>
  );
}
'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