Dark and Light Theme for React App

September 12, 2022

5 min read

Dark and Light Theme for React App
Watch on YouTube

I took both dark and light color themes from Notion and applied them to my React app. The goal was to create a universal color palette format to make colors easy when starting a new project. Let's see how it went.

Here's my productivity app after stealing Notion colors. It has a different style to Notion, but the colors still fit Increaser, and I would assume this color palette would work for other types of apps too.

export type ThemeName = "dark" | "light"
import { DefaultTheme } from "styled-components"
import { HSLA } from "ui/colors/HSLA"
import { generatePaleteCollorGetter } from "ui/colors/palette"

const primaryLightness = 52

const backgroundHue = 0
const backgroundSaturation = 0

const backgroundLightness = 10

export const regularTextAlpha = 0.9

export const darkTheme: DefaultTheme = {
  name: "dark",
  colors: {
    primary: new HSLA(197, 71, 52),
    primaryHover: new HSLA(195, 94, 41),

    attention: new HSLA(4, 69, 62),
    attentionHover: new HSLA(4, 58, 55),

    alert: new HSLA(0, 79, 63),
    success: new HSLA(130, 56, primaryLightness),

    foreground: new HSLA(
      backgroundHue,
      backgroundSaturation,
      backgroundLightness + 3
    ),
    background: new HSLA(
      backgroundHue,
      backgroundSaturation,
      backgroundLightness
    ),
    text: new HSLA(0, 0, 100, 0.81),
    textSupporting: new HSLA(0, 0, 61),
    textSupporting2: new HSLA(0, 0, 100, 0.44),
    textSupporting3: new HSLA(0, 0, 100, 0.28),

    backgroundGlass: new HSLA(0, 0, 100, 0.06),
    backgroundGlass2: new HSLA(0, 0, 100, 0.13),

    overlay: new HSLA(backgroundHue, backgroundSaturation, 1, 0.8),

    outlinedHover: new HSLA(0, 0, 16),

    white: new HSLA(0, 0, 100),

    getPaletteColor: generatePaleteCollorGetter(56, 52),
  },
  shadows: {
    small:
      "rgb(15 15 15 / 20%) 0px 0px 0px 1px, rgb(15 15 15 / 20%) 0px 2px 4px",
    mediud:
      "rgb(15 15 15 / 10%) 0px 0px 0px 1px, rgb(15 15 15 / 20%) 0px 3px 6px, rgb(15 15 15 / 40%) 0px 9px 24px;",
  },
}
import { DefaultTheme } from "styled-components"
import { HSLA } from "ui/colors/HSLA"
import { generatePaleteCollorGetter } from "ui/colors/palette"

const primaryLightness = 52

export const regularTextAlpha = 0.9

export const lightTheme: DefaultTheme = {
  name: "light",
  colors: {
    primary: new HSLA(197, 71, 52),
    primaryHover: new HSLA(195, 94, 41),

    attention: new HSLA(4, 69, 62),
    attentionHover: new HSLA(4, 58, 55),

    alert: new HSLA(0, 79, 63),
    success: new HSLA(130, 56, primaryLightness),

    foreground: new HSLA(60, 11, 98),
    background: new HSLA(0, 0, 100),

    text: new HSLA(60, 6, 20),
    textSupporting: new HSLA(45, 19, 8, 0.6),
    textSupporting2: new HSLA(45, 8, 20, 0.65),
    textSupporting3: new HSLA(45, 8, 81),

    backgroundGlass: new HSLA(45, 8, 20, 0.08),
    backgroundGlass2: new HSLA(45, 8, 20, 0.16),

    overlay: new HSLA(0, 0, 0, 0.4),

    outlinedHover: new HSLA(0, 0, 88),

    white: new HSLA(0, 0, 100),

    getPaletteColor: generatePaleteCollorGetter(52, 48),
  },
  shadows: {
    small:
      "rgb(15 15 15 / 10%) 0px 0px 0px 1px, rgb(15 15 15 / 10%) 0px 2px 4px",
    mediud:
      "rgb(15 15 15 / 5%) 0px 0px 0px 1px, rgb(15 15 15 / 10%) 0px 3px 6px, rgb(15 15 15 / 20%) 0px 9px 24px",
  },
}

The theme structure has a name - it could be either dark or light. There could be a need for more shadows, but for now, let's have only two - a small one for cards and a medium for popover menus. Both themes have the same shadows, except the dark theme is more opaque. To make the theme universal, we use functional names like primary or secondary instead of purple or yellow. The only exception is the white color.

First, we have primary and attention colors with hover. Primary is the color we use for interactive elements, while attention is more of a call to action, for example, on the landing page. After that, we have a color for success and failure that are green and red. Background and foreground colors fill most of the app space. For example, here, we use the background color for the main page content and foreground for the sidebar.

Then we have four types of text. Every other color gets less prominent. Here we use the text color for the title of the page, the supporting color for inactive navigation items, the second supporting color for the title of the card, and the third supporting color for placeholders in inputs.

Two background colors will not be enough, especially for a dark theme. We also use two glass colors, where the second one is more opaque. Here we use the first color for interactive elements hovers and the second one for the radio group at the top. Then we have an overlay color to focus the user on the modal and the outlineHover for elements like these buttons.

I didn't take palette colors from Notion and instead used a function that generates a color from an index. I have a video in the description explaining this approach.

Usually, components don't need to know the theme name, except in a few situations like this card, which doesn't need to use foreground in the light theme.

import React from "react"
import styled, { css } from "styled-components"
import { defaultBorderRadiusCSS } from "ui/borderRadius"
import { getCSSUnit } from "ui/utils/getCSSUnit"

interface Props {
  width?: React.CSSProperties["width"]
  padding?: React.CSSProperties["padding"]
}

export const Card = styled.div<Props>`
  background: ${({ theme }) =>
    theme.name === "light"
      ? theme.colors.background.toCssValue()
      : theme.colors.backgroundGlass.toCssValue()};
  box-shadow: ${({ theme }) => theme.shadows.small};
  ${defaultBorderRadiusCSS};
  ${({ width }) =>
    width &&
    css`
      width: ${getCSSUnit(width)};
    `}
  ${({ padding }) =>
    css`
      padding: ${getCSSUnit(padding ?? 20)};
    `}
`