React tool to instantly change your site's colours
Ellis
Posted on July 23, 2024
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.
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 */
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);
}
Design page - Colours tab
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 */
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;
ColourShift component
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;
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;
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;
Demo
Click here ➤ Design page ➤ Colours tab
Posted on July 23, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.