Skip to main content
Subframe doesn’t currently support both light & dark mode per theme, but you can implement it in your codebase using CSS variables. This guide shows you how to set up dark mode using CSS variables and Tailwind’s dark mode support.

Prerequisites

Before setting up dark mode, ensure you have:

How it works

  1. Configure Tailwind to use CSS variables instead of hardcoded colors
  2. Define light mode colors from your Subframe theme as CSS variables
  3. Define dark mode color overrides under an html.dark selector
  4. Toggle between modes by adding/removing the dark class on the HTML element
This keeps your Subframe workflow unchanged while enabling dark mode in your application.

Step 1: Configure Tailwind for CSS variables

Modify your tailwind.config.js to use CSS variables.
Add // @subframe/sync-disable at the top to prevent Subframe from overwriting your changes.
tailwind.config.js
// @subframe/sync-disable
module.exports = {
  darkMode: "selector", // Enable class-based dark mode
  theme: {
    extend: {
      colors: {
        brand: {
          50: "var(--brand-50)",
          100: "var(--brand-100)",
          // ... rest of your brand colors as CSS variables
        },
        neutral: {
          0: "var(--neutral-0)",
          50: "var(--neutral-50)",
          // ... rest of your neutral colors
        },
        // Continue for error, warning, success, etc.
        "brand-primary": "var(--brand-primary)",
        "default-font": "var(--default-font)",
        "subtext-color": "var(--subtext-color)",
        "neutral-border": "var(--neutral-border)",
        white: "var(--white)",
        "default-background": "var(--default-background)",
      },
    },
  },
}
Find your theme configuration in the Theme tab.

Step 2: Create CSS variables file

Create a variables.css file that defines CSS variables for both light and dark modes:
variables.css

/* Light mode */

html {
  --brand-50: rgb(250, 250, 250);
  --brand-100: rgb(245, 245, 245);
  /* ... rest of brand colors */

  --neutral-0: rgb(255, 255, 255);
  --neutral-50: rgb(247, 247, 247);
  /* ... rest of neutral colors */

  /* Semantic colors */
  --brand-primary: rgb(26, 26, 26);
  --default-font: rgb(41, 41, 41);
  --subtext-color: rgb(117, 117, 117);
  --neutral-border: rgb(240, 240, 240);
  --white: rgb(255, 255, 255);
  --default-background: rgb(252, 252, 252);
}

/* Dark mode */

html.dark {
  --brand-50: rgb(23, 23, 23);
  --brand-100: rgb(38, 38, 38);
  /* ... inverted brand colors (light becomes dark) */

  --neutral-0: rgb(10, 10, 10);
  --neutral-50: rgb(23, 23, 23);
  /* ... inverted neutral colors */

  /* Dark semantic colors */
  --brand-primary: rgb(212, 212, 212);
  --default-font: rgb(250, 250, 250);
  --subtext-color: rgb(163, 163, 163);
  --neutral-border: rgb(64, 64, 64);
  --white: rgb(10, 10, 10);
  --default-background: rgb(10, 10, 10);
}
Dark mode colors typically invert the scale: light mode’s --brand-50 (lightest) becomes dark mode’s --brand-900 (darkest), and vice versa.

Step 3: Import variables globally

Import your variables.css in your global CSS file (typically index.css, globals.css, or App.css depending on framework):
globals.css
@import "./variables.css";
@import "tailwindcss";
Or import directly in your entry point:
main.tsx
import "./variables.css"

Step 4: Toggle dark mode

Add or remove the dark class on the <html> element to switch themes.

Next.js with next-themes

npm install next-themes
app/layout.tsx
import { ThemeProvider } from 'next-themes'

export default function RootLayout({ children }) {
  return (
    <html suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="class" defaultTheme="system">
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

React with context

ThemeProvider.tsx
import { createContext, useContext, useEffect, useState } from 'react'

const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} })

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  useEffect(() => {
    const root = window.document.documentElement
    root.classList.remove('light', 'dark')
    root.classList.add(theme)
  }, [theme])

  const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light')

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

export const useTheme = () => useContext(ThemeContext)

Theme toggle button

import { useTheme } from 'next-themes'

export function ThemeToggle() {
  const { theme, setTheme } = useTheme()

  return (
    <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      {theme === 'dark' ? '☀️' : '🌙'}
    </button>
  )
}

Best practices

Always test your application in both light and dark modes. Check for:
  • Sufficient contrast ratios (use browser DevTools)
  • Readability of all text
  • Visibility of borders and dividers
  • Proper styling of interactive states
Use the user’s system preference as the default:
<ThemeProvider attribute="class" defaultTheme="system">
Set the theme before React hydrates to prevent flashing:
_document.tsx
<script dangerouslySetInnerHTML={{
  __html: `
    (function() {
      const theme = localStorage.getItem('theme') || 'light';
      document.documentElement.classList.add(theme);
    })()
  `
}} />