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
Configure Tailwind to use CSS variables instead of hardcoded colors
Define light mode colors from your Subframe theme as CSS variables
Define dark mode color overrides under an html.dark selector
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.
Modify your tailwind.config.js to use CSS variables. Add // @subframe/sync-disable at the top to prevent Subframe from overwriting your changes.
// @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 . Modify your theme.css to use CSS variables and add the dark mode custom variant: @theme {
/* Replace hardcoded colors with CSS variables */
--color-brand-50: var(--brand-50);
--color-brand-100: var(--brand-100);
/* ... continue for all colors */
/* Keep fonts, shadows, borders, spacing as-is */
}
/* Enable dark mode */
@custom-variant dark (&:where(.dark, .dark *));
Step 2: Create CSS variables file
Create a variables.css file that defines CSS variables for both light and dark modes:
/* 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):
@import "./variables.css" ;
@import "tailwindcss" ;
Or import directly in your entry point:
Step 4: Toggle dark mode
Add or remove the dark class on the <html> element to switch themes.
Next.js with next-themes
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
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 )
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
Respect system preferences
Use the user’s system preference as the default: < ThemeProvider attribute = "class" defaultTheme = "system" >
Prevent flash of unstyled content
Set the theme before React hydrates to prevent flashing: < script dangerouslySetInnerHTML = { {
__html: `
(function() {
const theme = localStorage.getItem('theme') || 'light';
document.documentElement.classList.add(theme);
})()
`
} } />