DEV Community

Cover image for React tool to instantly change your site's colours
Ellis
Ellis

Posted on • Updated on

React tool to instantly change your site's colours

The goal

I wanted to see how simple, easy and effective it would be to create an internal page for a website where the designer could use a couple of sliders to change the site colours and immediately see what the entire website and all the component would look like.

  • Internal: we probably don't need the page to be secret, but we don't need to advertise it to the user via any menus or links.
  • Effective: it should give the designer an effortless, immediate, good view of what the site and components would look like.

 

Techniques used

  • React, typescript, styled-components,
  • and obviously: html, css and javascript.

 

OKLCH color space

After comparing various CSS colour spaces available, I chose OKLCH to be ideal for this purpose, because:

  • OKLCH is a colour space (or a colour function) which has been normalised such that: once we have selected a fixed value for lightness "L" and colour intensity "C" (aka chroma), we can change the colour hue "H" of an object freely, and the colour will keep looking good with this constant L and C. This exactly fits our use case here.
  • So for each colour we first choose a fixed L and C value to our liking, then we play with the H value using a slider.

More info about OKLCH:

In sRGB (left), a straight line is drawn between the two colors. In OKLCH (right), an arc is drawn along the hues to reach the second color.
xxx

 

OKLCH parameters

  • Lightness L: 0% for black, 100% for white.
  • Colour intensity (or chroma) C: 0 for gray, no max, typically around 0.1 or less than 0.5.
  • Colour hue (the colour itself) H: 0 to 360 (360 = 0).
  • (A fourth parameter alpha provides opacity, which we won't be using here.)

Example:

color: oklch(70% 0.3 150) /* bright green */
Enter fullscreen mode Exit fullscreen mode

 

The global index.css file for the site

(Don't worry too much about the colour names, you could choose better ones. :)

:root {
  /* OKLCH HUE VALUES: */
  --color1: 144; /* green */
  --color2: 90; /* brown */

  --color-primary: oklch(65% 0.1 var(--color1));
  --color-primary-lighter: oklch(70% 0.1 var(--color1));
  --color-disabled: oklch(70% 0.05 var(--color1));

  --color-border: oklch(76% 0.1 var(--color2));
  --color-border-darker: oklch(64% 0.17 var(--color2));

  --color-border-gray: #aaa;
  --color-border-light: #ccc;

  --color-whitish: #f0f0f0;
  --color-blackish: #444444;
}

button {
  background-color: var(--color-primary);
  border: 1px solid var(--color-primary);
}
Enter fullscreen mode Exit fullscreen mode

 

Design page - Colours tab

Image description

 

Sections on this tab:

  • Colours: four boxes to showcase text each colour (with white/black fg/bg).
  • Hue selection: two sliders, one for each main colour hue.
  • Sample components: a small number of components to view on this page with the new colours.

When we change the colours, we can browser through the entire website to immediately see the new look.

When we are happy with the new values, we can go and update the colour hue values in index.css:

  /* OKLCH HUE VALUES: */
  --color1: 144; /* green */
  --color2: 90; /* brown */
Enter fullscreen mode Exit fullscreen mode

 

ColoursTab component

import styled from "styled-components";

import {
  ColourShift,
  ComponentCardButton,
  ComponentCardPaging,
  PageTabContent,
  SectionTitle,
} from "../components";

const Section = styled.div`
  display: flex;
  flex-direction: column;
  gap: 10px;
`;

// ------------------------------------
type BoxProps = {
  c?: string;
  fg: string;
  bg: string;
};

const Box = ({ c, fg, bg }: BoxProps) => {
  return (
    <p
      style={{
        width: "130px",
        padding: "6px 6px 9px",
        backgroundColor: `var(--color-${bg})`,
        color: `var(--color-${fg})`,
        overflowWrap: "anywhere",
        borderRadius: "4px",
      }}
    >
      {c}
    </p>
  );
};

// ------------------------------------
type ColourProps = {
  colour: string;
};

const Colour = ({ colour }: ColourProps) => {
  return (
    <div
      style={{
        display: "flex",
        gap: "10px",
        justifyContent: "space-between",
        //
      }}
    >
      <Box c={colour} fg="whitish" bg={colour} />
      <Box c={colour} fg="blackish" bg={colour} />
      <Box c={colour} fg={colour} bg="whitish" />
      <Box c={colour} fg={colour} bg="blackish" />
    </div>
  );
};

// ------------------------------------
const ColoursTab = () => {
  return (
    <PageTabContent>
      <Section>
        <SectionTitle>Colours</SectionTitle>
        <p>
          <b>global variable: color1</b>
        </p>
        <Colour colour="primary" />
        <Colour colour="primary-lighter" />
        <Colour colour="disabled" />
        <br />

        <p>
          <b>global variable: color2</b>
        </p>
        <Colour colour="border" />
        <Colour colour="border-darker" />
        <br />

        <p>
          <b>misc</b>
        </p>
        <Colour colour="border-gray" />
        <Colour colour="border-light" />
        <Colour colour="blackish" />
        <Colour colour="whitish" />
      </Section>

      <Section>
        <SectionTitle>Hue selection (oklch, 0..359)</SectionTitle>
        <ColourShift name="color1" />
        <ColourShift name="color2" />
      </Section>

      <Section>
        <SectionTitle>Sample components</SectionTitle>
        <ComponentCardButton isRow />
        <ComponentCardPaging />
      </Section>
    </PageTabContent>
  );
};

export default ColoursTab;
Enter fullscreen mode Exit fullscreen mode

 

ColourShift component

Component:
Image description

The code:

import { useEffect, useState } from "react";
import styled from "styled-components";

import { useCssGlobalVariables } from "../hooks";
import GradientBar from "./GradientBar";

const Container = styled.div`
  border: 1px solid var(--color-border-gray);
  border-radius: 8px;
  box-shadow: var(--box-shadow);
`;

const InputContainer = styled.div`
  padding: 1px 10px;
`;

type InputProps = {
  $colour: string;
};

const Input = styled.input<InputProps>`
  width: 100%;
  accent-color: ${({ $colour }) => $colour};
`;

// ------------------------------------
type Props = {
  name: string;
};

const ColourShift = ({ name: name }: Props) => {
  const {
    getCssGlobalVariable,
    setCssGlobalVariable,
  } = //
    useCssGlobalVariables(`--${name}`);

  const [hue, setHue] = useState("");

  // ---------------------------------
  useEffect(() => {
    const h = getCssGlobalVariable();
    setHue(h);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const onChange = (h: string) => {
    setCssGlobalVariable(h);
    setHue(h);
  };

  // ------------------------------------
  return (
    <Container data-testid="ColourShift">
      <GradientBar title={name} hue={hue} />

      <InputContainer>
        <Input
          type="range"
          $colour={`oklch(70% 0.1 ${hue})`}
          min="0"
          max="359"
          value={hue}
          onChange={(e) => onChange(e.target.value)}
        />
      </InputContainer>
    </Container>
  );
};

export default ColourShift;
Enter fullscreen mode Exit fullscreen mode

 

GradientBar component

This is a sub-component of the ColourShift, showing the colour scale on the upper half of the ColourShift.

Note that the gradient needs an additional middle value between 0 and 359 to work properly.

import styled from "styled-components";

const Container = styled.div`
  background: linear-gradient(
    to right in oklch,
    oklch(65% 0.1 0),
    oklch(65% 0.1 179),
    oklch(65% 0.1 359)
  );
  width: 100%;
  padding: 3px 10px 6px;
  display: flex;
  justify-content: space-between;
  border-top-left-radius: 6px;
  border-top-right-radius: 6px;

  * {
    color: var(--color-whitish);
  }
`;

const TitleAndHue = styled.div`
  display: flex;
  gap: 6px;
`;

const Hue = styled.p`
  width: "30px";
`;

// ------------------------------------
type Props = {
  title: string;
  hue: string;
};

const GradientBar = ({ title, hue }: Props) => {
  return (
    <Container data-testid="GradientBar">
      <p>0</p>

      <TitleAndHue>
        <p>{title}:</p>
        <Hue>{hue}</Hue>
      </TitleAndHue>

      <p>359</p>
    </Container>
  );
};

export default GradientBar;
Enter fullscreen mode Exit fullscreen mode

 

useCssGlobalVariables custom hook

This custom hook encapsulates the javascript required for reading and updating a CSS global variable.

const useCssGlobalVariables = (name: string) => {
  const getCssGlobalVariable = () => {
    const root = document.querySelector(":root");
    if (!root) return "";

    const style = getComputedStyle(root);
    return style.getPropertyValue(name);
  };

  // ------------------------------------
  const setCssGlobalVariable = (value: string) => {
    const root = document.querySelector(":root");
    if (!root) return null;

    (root as HTMLElement).style.setProperty(name, value);
  };

  // ------------------------------------
  return { getCssGlobalVariable, setCssGlobalVariable };
};

export default useCssGlobalVariables;
Enter fullscreen mode Exit fullscreen mode

 

Demo

Click here ➤ Design page ➤ Colours tab

Top comments (0)