Recently, I have been using shadcn/ui a lot, and one thing I always felt was missing is the ability to programmatically access the theme and colors. Most of the time, we don’t need it, but there are cases where having this support is useful.
For example, I was using a data grid from MUI and wanted to match its colors and theme to the shadcn/ui theme.
So, I created a hook that makes shadcn/ui colors accessible programmatically. Let’s understand it step by step.
1const getColor = (variable: string) =>2 getComputedStyle(document.documentElement)3 .getPropertyValue(`--${variable}`)4 .trim();
I noticed that all Tailwind and ShadCN colors, when used, are added to the :root element of the HTML page. This means we can access these variables using the global getComputedStyle function by passing document.documentElement, which represents the :root element.
These variables are stored as properties in the computed styles, so the getColor function takes a color variable name and returns the color associated with it.
1type ThemeColors = {2primary: string;3secondary: string;4};
Now, let’s define all the variables that we want to access. I have added two for the demo, but you can add all the variables that you need.
1export const useThemeColors = (): ThemeColors => {2const [colors, setColors] = useState<ThemeColors>({3primary: getColor("primary"),4secondary: getColor("secondary"),5});67return colors;8};
After that, I defined a hook called useThemeColors, which has a state for keeping the theme colors. But you might be thinking, why do we need a state? We could just return the colors directly.
Consider the case when the user toggles the theme — you want your colors to update dynamically based on the new theme. To achieve this, we will use MutationObserver.
1useEffect(() => {2const observer = new MutationObserver(() => {3 setColors({4 primary: getColor("primary"),5 secondary: getColor("secondary"),6 });7});89observer.observe(document.documentElement, {10 attributes: true,11 attributeFilter: ["class"],12});1314return () => observer.disconnect();15}, []);
So, I defined a MutationObserver inside a useEffect. This Web API helps detect changes in the target HTML element within a web page. Here, we are using it to detect style changes.
Of course, we need to disconnect it when the component unmounts, and the final code is:
1import { useState, useEffect } from "react";23type ThemeColors = {4primary: string;5secondary: string;6background: string;7};89const getColor = (variable: string) =>10getComputedStyle(document.documentElement)11.getPropertyValue(`--${variable}`)12.trim();1314export const useThemeColors = (): ThemeColors => {15const [colors, setColors] = useState<ThemeColors>({16primary: getColor("primary"),17secondary: getColor("secondary"),18background: getColor("background"),19});2021useEffect(() => {22const observer = new MutationObserver(() => {23 setColors({24 primary: getColor("primary"),25 secondary: getColor("secondary"),26 background: getColor("background"),27 });28});2930observer.observe(document.documentElement, {31 attributes: true,32 attributeFilter: ["class"],33});3435return () => observer.disconnect();36}, []);3738return colors;39};
Bonus: The colors returned by shadcn/ui are in HSL format. If you want to convert them to RGB, you can use the following function:
1import space from "color-space";23const hslToRgb = (hslColor: string) => {4// Removes all the % symbols and splits the string on the space.5const hslArray = hslColor.replace(/%/g, "").split(" ");67// Converts the HSL array to RGB8const rgbString = space.hsl.rgb(hslArray).join(",");910return `rgb(${rgbString})`;11};
With this custom hook, you can now easily access and use ShadCN/UI theme colors dynamically in your React projects. Whether you’re integrating with third-party components like MUI or simply need programmatic access to your theme, this approach ensures your UI stays consistent.
I hope you found this helpful! If you have any questions or improvements, feel free to share your thoughts. Happy coding