HyperSaaS
FrontendUI Components

Theming

Color system, dark mode, and CSS custom properties.

Color System

HyperSaaS uses CSS custom properties with the oklch color space, following shadcn/ui's theming conventions. The primary color is sky blue (hue ~228-232).

Light Theme

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --primary: oklch(0.588 0.155 230);
  --primary-foreground: oklch(0.985 0.002 230);
  --secondary: oklch(0.97 0.001 286.375);
  --accent: oklch(0.97 0.001 286.375);
  --muted: oklch(0.97 0.001 286.375);
  --destructive: oklch(0.577 0.245 27.325);
  --ring: oklch(0.588 0.155 230);
  --sidebar-primary: oklch(0.588 0.155 230);
}

Dark Theme

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --primary: oklch(0.746 0.138 232);
  --primary-foreground: oklch(0.208 0.042 231);
  --ring: oklch(0.746 0.138 232);
  --sidebar-primary: oklch(0.746 0.138 232);
}

Color Tokens

TokenPurpose
--background / --foregroundPage background and text
--primary / --primary-foregroundPrimary buttons, links, accents
--secondary / --secondary-foregroundSecondary elements
--muted / --muted-foregroundSubtle backgrounds, disabled text
--accent / --accent-foregroundHover states, highlights
--destructive / --destructive-foregroundError states, delete buttons
--card / --card-foregroundCard backgrounds
--popover / --popover-foregroundDropdown/popover backgrounds
--borderBorder color
--inputInput border color
--ringFocus ring color
--sidebar-*Sidebar-specific colors
--chart-1 through --chart-5Chart colors

Dark Mode

Dark mode is handled by next-themes:

// components/theme-provider.tsx
import { ThemeProvider as NextThemesProvider } from "next-themes";

export function ThemeProvider({ children }) {
  return (
    <NextThemesProvider
      attribute="class"
      defaultTheme="system"
      enableSystem
      disableTransitionOnChange
    >
      {children}
    </NextThemesProvider>
  );
}

The attribute="class" strategy adds/removes the dark class on <html>, which activates the dark CSS variables.

Theme Toggle

import { useTheme } from "next-themes";

function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <Button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      {theme === "dark" ? <SunIcon /> : <MoonIcon />}
    </Button>
  );
}

Customizing Colors

To change the primary color, update the CSS variables in globals.css. The oklch color space makes it easy to adjust:

  • Hue (~228-232): Shift to change the color family
  • Chroma (~0.138-0.155): Increase for more saturated, decrease for muted
  • Lightness (~0.588 light / ~0.746 dark): Adjust for contrast

Example — switching to violet:

:root {
  --primary: oklch(0.588 0.155 280);  /* Changed hue from 230 to 280 */
}
.dark {
  --primary: oklch(0.746 0.138 282);
}

Fonts

The app uses Geist font family:

// app/layout.tsx
import { Geist, Geist_Mono } from "next/font/google";

const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });

Applied via CSS variables:

body {
  font-family: var(--font-geist-sans), sans-serif;
}
code {
  font-family: var(--font-geist-mono), monospace;
}

Tailwind CSS 4

The project uses Tailwind CSS v4 with the new @plugin directive:

/* globals.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";

Components use Tailwind utility classes that reference the CSS custom properties:

<button className="bg-primary text-primary-foreground hover:bg-primary/90">
  Click me
</button>

On this page