Skip to main content
Vibestrap intentionally ships light-mode only. The reason: every component change becomes a two-mode tax, the marketing site reads better as a single confident theme, and most of the premium scaffolds we benchmark against (Stripe, Loops, Resend, Clerk, tuwa.ai) don’t ship dark mode either. That said, dark mode is a real preference for many developers. If your end-product needs it, here’s the five-step recipe to put it back.

Step 1 — Reinstall next-themes

pnpm add next-themes

Step 2 — Wrap with ThemeProvider

Edit src/components/providers.tsx:
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from 'next-themes';
import { useState } from 'react';
import { Toaster } from 'sonner';

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: { staleTime: 60 * 1000, refetchOnWindowFocus: false },
        },
      })
  );
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
      <QueryClientProvider client={queryClient}>
        {children}
        <Toaster position="top-center" richColors />
      </QueryClientProvider>
    </ThemeProvider>
  );
}
Note: drop the theme="light" prop from <Toaster> — sonner will follow the system theme automatically through next-themes.

Step 3 — Re-add .dark block in globals.css

Add the variant directive at the top of src/app/globals.css (right after the @plugin line):
@custom-variant dark (&:is(.dark *));
And add a .dark { ... } block right after :root { ... } defining the inverse palette. For the editorial cream-mono baseline, that means something like:
.dark {
  --background: oklch(0.16 0.01 85);
  --foreground: oklch(0.96 0.012 85);
  --card: oklch(0.20 0.012 85);
  --card-foreground: oklch(0.96 0.012 85);
  --popover: oklch(0.20 0.012 85);
  --popover-foreground: oklch(0.96 0.012 85);
  --primary: oklch(0.96 0.012 85);
  --primary-foreground: oklch(0.16 0.01 85);
  --secondary: oklch(0.26 0.012 85);
  --secondary-foreground: oklch(0.96 0.012 85);
  --muted: oklch(0.26 0.012 85);
  --muted-foreground: oklch(0.7 0.005 80);
  --accent: oklch(0.26 0.012 85);
  --accent-foreground: oklch(0.96 0.012 85);
  --border: oklch(1 0 0 / 10%);
  --input: oklch(1 0 0 / 14%);
  --ring: oklch(0.55 0.2 250);
}
Tune values to your skin — these mirror the cream-mono editorial defaults.

Step 4 — Restore the toggle component

Recreate src/components/layout/theme-toggle.tsx:
'use client';

import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';

export function ThemeToggle() {
  const { setTheme, resolvedTheme } = useTheme();
  const next = resolvedTheme === 'dark' ? 'light' : 'dark';
  return (
    <Button
      variant="ghost"
      size="icon"
      aria-label="Toggle theme"
      onClick={() => setTheme(next)}
    >
      <Sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
    </Button>
  );
}
Then mount it in src/components/layout/header.tsx:
import { ThemeToggle } from './theme-toggle';
// …
<div className="flex items-center gap-1">
  <LocaleSwitcher />
  <ThemeToggle />
  <UserNav />
</div>

Step 5 — Restore dark: Tailwind variants where they matter

The places in Vibestrap that used dark-variant compensations were:
  • Markdown prose pages — add dark:prose-invert back to the <div className="prose ..."> wrappers under src/app/[locale]/(marketing)/{terms,privacy,refund,license,about,changelog,blog/[slug]}/page.tsx.
  • Status colors in src/components/ai/*text-emerald-600 etc. benefit from dark:text-emerald-400 for readable contrast on dark.
  • src/components/ui/alert.tsx — destructive variant readability.
  • src/components/turnstile-field.tsx — pass resolvedTheme to the Turnstile widget so its embedded UI matches.
Search for the dark:* usages you need with:
grep -rn "text-emerald-600\|text-amber-600\|prose " src/
…and add a paired dark:* class alongside.

Verification

After the five steps, run the gates:
pnpm typecheck && pnpm lint && pnpm check:i18n && pnpm build
Toggle the theme in the browser and confirm:
  • Marketing pages render cleanly in both themes.
  • Status / accent colors are readable in dark.
  • Sonner toasts pick up the active theme.
  • Turnstile widget colors match.

Why we don’t ship it ourselves

Inherited maintenance: every component change becomes “does this work in dark too?”. Most buyers ship to mainstream consumer audiences who don’t toggle dark mode on marketing sites. Editorial / cream-mono aesthetics — Vibestrap’s default skin — are light-first by design. If you go through the recipe above and discover gaps in our documentation, please send us a note — we’ll improve this page rather than re-add dark mode to the default branch.