Skip to main content
Subframe does not currently have a built-in way to theme a project for both light and dark mode, but you can implement dark mode in your codebase after exporting your components and theme. 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:

Configure Tailwind to use CSS variables

  • Tailwind v3
  • Tailwind v4
To enable dark mode, you need to modify your tailwind.config.js file to use CSS variables instead of hardcoded color values. This allows you to swap colors dynamically based on the theme.
  1. Add darkMode: "selector" to enable class-based dark mode switching
  2. Replace all color values with CSS variable references using the var() function
Add the @subframe/sync-disable comment at the top to prevent Subframe from overwriting your changes when syncing.
tailwind.config.js
// @subframe/sync-disable
module.exports = {
  darkMode: "selector",
  theme: {
    extend: {
      colors: {
        brand: {
          50: "var(--brand-50)",
          100: "var(--brand-100)",
          200: "var(--brand-200)",
          300: "var(--brand-300)",
          400: "var(--brand-400)",
          500: "var(--brand-500)",
          600: "var(--brand-600)",
          700: "var(--brand-700)",
          800: "var(--brand-800)",
          900: "var(--brand-900)",
        },
        neutral: {
          0: "var(--neutral-0)",
          50: "var(--neutral-50)",
          100: "var(--neutral-100)",
          200: "var(--neutral-200)",
          300: "var(--neutral-300)",
          400: "var(--neutral-400)",
          500: "var(--neutral-500)",
          600: "var(--neutral-600)",
          700: "var(--neutral-700)",
          800: "var(--neutral-800)",
          900: "var(--neutral-900)",
          950: "var(--neutral-950)",
        },
        error: {
          50: "var(--error-50)",
          100: "var(--error-100)",
          200: "var(--error-200)",
          300: "var(--error-300)",
          400: "var(--error-400)",
          500: "var(--error-500)",
          600: "var(--error-600)",
          700: "var(--error-700)",
          800: "var(--error-800)",
          900: "var(--error-900)",
        },
        warning: {
          50: "var(--warning-50)",
          100: "var(--warning-100)",
          200: "var(--warning-200)",
          300: "var(--warning-300)",
          400: "var(--warning-400)",
          500: "var(--warning-500)",
          600: "var(--warning-600)",
          700: "var(--warning-700)",
          800: "var(--warning-800)",
          900: "var(--warning-900)",
        },
        success: {
          50: "var(--success-50)",
          100: "var(--success-100)",
          200: "var(--success-200)",
          300: "var(--success-300)",
          400: "var(--success-400)",
          500: "var(--success-500)",
          600: "var(--success-600)",
          700: "var(--success-700)",
          800: "var(--success-800)",
          900: "var(--success-900)",
        },
        "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)",
      },
      fontSize: {
        caption: [
          "12px",
          {
            lineHeight: "16px",
            fontWeight: "400",
            letterSpacing: "0em",
          },
        ],
        "caption-bold": [
          "12px",
          {
            lineHeight: "16px",
            fontWeight: "500",
            letterSpacing: "0em",
          },
        ],
        body: [
          "14px",
          {
            lineHeight: "20px",
            fontWeight: "400",
            letterSpacing: "0em",
          },
        ],
        "body-bold": [
          "14px",
          {
            lineHeight: "20px",
            fontWeight: "500",
            letterSpacing: "0em",
          },
        ],
        "heading-3": [
          "16px",
          {
            lineHeight: "20px",
            fontWeight: "600",
            letterSpacing: "0em",
          },
        ],
        "heading-2": [
          "20px",
          {
            lineHeight: "24px",
            fontWeight: "600",
            letterSpacing: "0em",
          },
        ],
        "heading-1": [
          "30px",
          {
            lineHeight: "36px",
            fontWeight: "500",
            letterSpacing: "0em",
          },
        ],
        "monospace-body": [
          "14px",
          {
            lineHeight: "20px",
            fontWeight: "400",
            letterSpacing: "0em",
          },
        ],
      },
      fontFamily: {
        caption: "Inter",
        "caption-bold": "Inter",
        body: "Inter",
        "body-bold": "Inter",
        "heading-3": "Inter",
        "heading-2": "Inter",
        "heading-1": "Inter",
        "monospace-body": "monospace",
      },
      boxShadow: {
        sm: "0px 1px 2px 0px rgba(0, 0, 0, 0.05)",
        default: "0px 1px 2px 0px rgba(0, 0, 0, 0.05)",
        md: "0px 4px 16px -2px rgba(0, 0, 0, 0.08), 0px 2px 4px -1px rgba(0, 0, 0, 0.08)",
        lg: "0px 12px 32px -4px rgba(0, 0, 0, 0.08), 0px 4px 8px -2px rgba(0, 0, 0, 0.08)",
        overlay:
          "0px 12px 32px -4px rgba(0, 0, 0, 0.08), 0px 4px 8px -2px rgba(0, 0, 0, 0.08)",
      },
      borderRadius: {
        sm: "2px",
        md: "4px",
        DEFAULT: "4px",
        lg: "8px",
        full: "9999px",
      },
      container: {
        padding: {
          DEFAULT: "16px",
          sm: "calc((100vw + 16px - 640px) / 2)",
          md: "calc((100vw + 16px - 768px) / 2)",
          lg: "calc((100vw + 16px - 1024px) / 2)",
          xl: "calc((100vw + 16px - 1280px) / 2)",
          "2xl": "calc((100vw + 16px - 1536px) / 2)",
        },
      },
      spacing: {
        112: "28rem",
        144: "36rem",
        192: "48rem",
        256: "64rem",
        320: "80rem",
      },
      screens: {
        mobile: {
          max: "767px",
        },
      },
    },
  },
}
Replace the color values in this example with the actual colors from your Subframe theme. You can find your theme configuration in the Theme tab.

Create a CSS variables file

Create a new file called variables.css that defines the CSS variables for both light and dark modes. The variables under html define the default (light) theme, while variables under html.dark define the dark theme.
variables.css
html {
  /* brand */
  --brand-50: rgb(250, 250, 250);
  --brand-100: rgb(245, 245, 245);
  --brand-200: rgb(229, 229, 229);
  --brand-300: rgb(212, 212, 212);
  --brand-400: rgb(163, 163, 163);
  --brand-500: rgb(115, 115, 115);
  --brand-600: rgb(82, 82, 82);
  --brand-700: rgb(64, 64, 64);
  --brand-800: rgb(38, 38, 38);
  --brand-900: rgb(23, 23, 23);

  /* neutral */
  --neutral-0: rgb(255, 255, 255);
  --neutral-50: rgb(247, 247, 247);
  --neutral-100: rgb(240, 240, 240);
  --neutral-200: rgb(235, 235, 235);
  --neutral-300: rgb(230, 230, 230);
  --neutral-400: rgb(166, 166, 166);
  --neutral-500: rgb(117, 117, 117);
  --neutral-600: rgb(87, 87, 87);
  --neutral-700: rgb(66, 66, 66);
  --neutral-800: rgb(41, 41, 41);
  --neutral-900: rgb(26, 26, 26);
  --neutral-950: rgb(10, 10, 10);

  /* error */
  --error-50: rgb(254, 242, 242);
  --error-100: rgb(254, 226, 226);
  --error-200: rgb(254, 202, 202);
  --error-300: rgb(252, 165, 165);
  --error-400: rgb(248, 113, 113);
  --error-500: rgb(239, 68, 68);
  --error-600: rgb(220, 38, 38);
  --error-700: rgb(185, 28, 28);
  --error-800: rgb(153, 27, 27);
  --error-900: rgb(127, 29, 29);

  /* warning */
  --warning-50: rgb(255, 251, 235);
  --warning-100: rgb(254, 243, 199);
  --warning-200: rgb(253, 230, 138);
  --warning-300: rgb(252, 211, 77);
  --warning-400: rgb(251, 191, 36);
  --warning-500: rgb(245, 158, 11);
  --warning-600: rgb(217, 119, 6);
  --warning-700: rgb(180, 83, 9);
  --warning-800: rgb(146, 64, 14);
  --warning-900: rgb(120, 53, 15);

  /* success */
  --success-50: rgb(240, 253, 244);
  --success-100: rgb(220, 252, 231);
  --success-200: rgb(187, 247, 208);
  --success-300: rgb(134, 239, 172);
  --success-400: rgb(74, 222, 128);
  --success-500: rgb(34, 197, 94);
  --success-600: rgb(22, 163, 74);
  --success-700: rgb(21, 128, 61);
  --success-800: rgb(22, 101, 52);
  --success-900: rgb(20, 83, 45);

  /* Additional 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);
}

html.dark {
  /* brand */
  --brand-50: rgb(23, 23, 23);
  --brand-100: rgb(38, 38, 38);
  --brand-200: rgb(64, 64, 64);
  --brand-300: rgb(38, 38, 38);
  --brand-400: rgb(115, 115, 115);
  --brand-500: rgb(163, 163, 163);
  --brand-600: rgb(212, 212, 212);
  --brand-700: rgb(229, 229, 229);
  --brand-800: rgb(245, 245, 245);
  --brand-900: rgb(250, 250, 250);

  /* neutral */
  --neutral-0: rgb(10, 10, 10);
  --neutral-50: rgb(23, 23, 23);
  --neutral-100: rgb(38, 38, 38);
  --neutral-200: rgb(64, 64, 64);
  --neutral-300: rgb(82, 82, 82);
  --neutral-400: rgb(115, 115, 115);
  --neutral-500: rgb(163, 163, 163);
  --neutral-600: rgb(212, 212, 212);
  --neutral-700: rgb(229, 229, 229);
  --neutral-800: rgb(245, 245, 245);
  --neutral-900: rgb(250, 250, 250);
  --neutral-950: rgb(255, 255, 255);

  /* error */
  --error-50: rgb(60, 24, 26);
  --error-100: rgb(72, 26, 29);
  --error-200: rgb(84, 27, 31);
  --error-300: rgb(103, 30, 34);
  --error-400: rgb(130, 32, 37);
  --error-500: rgb(170, 36, 41);
  --error-600: rgb(229, 72, 77);
  --error-700: rgb(242, 85, 90);
  --error-800: rgb(255, 99, 105);
  --error-900: rgb(254, 236, 238);

  /* warning */
  --warning-50: rgb(57, 26, 3);
  --warning-100: rgb(68, 31, 4);
  --warning-200: rgb(79, 35, 5);
  --warning-300: rgb(95, 42, 6);
  --warning-400: rgb(118, 50, 5);
  --warning-500: rgb(148, 62, 0);
  --warning-600: rgb(247, 104, 8);
  --warning-700: rgb(255, 128, 43);
  --warning-800: rgb(255, 139, 62);
  --warning-900: rgb(254, 234, 221);

  /* success */
  --success-50: rgb(19, 40, 25);
  --success-100: rgb(22, 48, 29);
  --success-200: rgb(25, 57, 33);
  --success-300: rgb(29, 68, 39);
  --success-400: rgb(36, 85, 48);
  --success-500: rgb(47, 110, 59);
  --success-600: rgb(70, 167, 88);
  --success-700: rgb(85, 180, 103);
  --success-800: rgb(99, 193, 116);
  --success-900: rgb(229, 251, 235);

  /* Additional 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);
}
Customize these color values to match your brand’s light and dark themes. The dark mode colors typically invert the scale (e.g., --brand-50 in light mode becomes closer to --brand-900 in dark mode).

Import the CSS variables globally

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

Toggle dark mode

To switch between light and dark mode, add or remove the dark class on the <html> element. You can implement this with a button or toggle.
I