HSLA Colors System in React with Styled Components & TypeScript

July 13, 2022

4 min read

HSLA Colors System in React with Styled Components & TypeScript
Watch on YouTube

What is the best approach for handling colors in React app? Let's find out.

We'll use the HSLA color format that has four components. Hue is the color itself. It's an angle on the color wheel that ranges from 0 to 360, where 120 would be green and 240 blue. Saturation is a percentage where 100% makes color fully saturated, and 0% will make the color look gray. Lightness is also a percentage value, where 100% will make any color white and 0% - black. The final component is alpha. When it's 0, the color is transparent, and one - is fully opaque.

HSLA gives us an intuitive way to change colors. Do you want to make a color a bit lighter? You can increase the L component by 10%. How would you do that with RGB or HEX? It wouldn’t be that easy or elegant.

Here we have the HSLA class. The constructor takes four parameters, where alpha is one by default.

export const hslaKeys = ["h", "s", "l", "a"] as const

export type ColorModifiers = Partial<
  Record<(typeof hslaKeys)[number], (parameter: number) => number>
>

const enforceRange = (value: number, min: number, max: number) =>
  Math.max(min, Math.min(max, value))

export class HSLA {
  private _h = 0
  get h(): number {
    return this._h
  }
  set h(newH: number) {
    this._h = enforceRange(newH, 0, 360)
  }

  private _l = 0
  get l(): number {
    return this._l
  }
  set l(newL: number) {
    this._l = enforceRange(newL, 0, 100)
  }

  private _s = 0
  get s(): number {
    return this._s
  }
  set s(newS: number) {
    this._s = enforceRange(newS, 0, 100)
  }

  private _a = 0
  get a(): number {
    return this._a
  }
  set a(newA: number) {
    this._a = enforceRange(newA, 0, 100)
  }

  constructor(h: number, s: number, l: number, a = 1) {
    this.h = h
    this.s = s
    this.l = l
    this.a = a
  }

  toCssValue() {
    return `hsla(${this.h},${this.s}%,${this.l}%,${this.a})`
  }

  getVariant(modifiers: ColorModifiers) {
    const [h, s, l, a] = hslaKeys.map((key) => {
      const value = this[key]
      const modifier = modifiers[key]

      return modifier ? modifier(value) : value
    })

    return new HSLA(h, s, l, a)
  }
}

To convert it to CSS value, we have a method that takes each component and transforms them into a CSS HSLA function.

To get a new variant of color, we have the getVariant method. It receives an object with modifiers, where the modifier is a function that receive an old component value and returns a new one.

To keep components inside of a range, we use setters. If we try to make saturation 120% percent, the S setter we'll convert it 100%, by leveraging the enforceRange function.

My app has just a few base colors, and I use the HSLA class to define all of them. Here we have a handful of variables, like primary lightness, or background saturation, that we share across quite a few colors.

import { HSLA } from "./HSLA"

const primaryHue = 210
const primarySecondHue = 41
const primaryThirdHue = 6
const primarySaturation = 92
const primaryLightness = 52
const lightnessIncrease = 5

export const backgroundHue = 214
const backgroundSaturation = 42
const backgroundLightness = 10

const transparentSaturation = 60
const transparentLightness = 88

export const regularTextAlpha = 0.9

export const colors = {
  primary: new HSLA(primaryHue, primarySaturation, primaryLightness),
  attention: new HSLA(primarySecondHue, primarySaturation, primaryLightness),
  alert: new HSLA(primaryThirdHue, primarySaturation, primaryLightness),
  success: new HSLA(130, primarySaturation, primaryLightness),

  background: new HSLA(
    backgroundHue,
    backgroundSaturation,
    backgroundLightness + lightnessIncrease
  ),
  backgroundDark: new HSLA(
    backgroundHue,
    backgroundSaturation,
    backgroundLightness,
    1
  ),
  backgroundLight: new HSLA(
    backgroundHue,
    backgroundSaturation,
    backgroundLightness + lightnessIncrease * 2
  ),

  text: new HSLA(
    backgroundHue,
    transparentSaturation,
    transparentLightness,
    regularTextAlpha
  ),

  backgroundDimGlass: new HSLA(
    backgroundHue,
    transparentSaturation,
    transparentLightness,
    0.5
  ),
  backgroundGlass: new HSLA(
    backgroundHue,
    transparentSaturation,
    transparentLightness,
    0.15
  ),

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

  white: new HSLA(0, 0, 256, 1),
} as const

export type Colors = typeof colors

export type ColorName = keyof typeof colors

To include those colors in styled-components Theme types, we can create a type declaration file and extend the default theme.

import "styled-components"

import { Colors } from "ui/colors"

declare module "styled-components" {
  export interface DefaultTheme {
    colors: Colors
  }
}

Then we can create a theme object, pass it to the ThemeProvider, and use colors in styled-components.

import { DefaultTheme, ThemeProvider } from "styled-components"
import { colors } from "ui/colors"

export const theme: DefaultTheme = { colors } as const

export const App = () => <ThemeProvider theme={theme}>...</ThemeProvider>

Here's an example of the badge component that makes background by creating an almost transparent variant and converting it to a CSS value.

background: ${({ theme }) => theme.colors.success.getVariant({ a: () => a * 0.2 }).toCssValue()};