Using CSS variables to make easily customisable components
26 Dec 2022. 1258 words.
Motivation
For my React hobby projects, I really loved using Chakra UI and Mantine for a long time because they are very intuitive to use. If you want to make a filled red button, you just write
and you’re done! Sadly, I eventually stopped using those UI libraries because of my (and NextJS compiler’s) concerns towards bloated bundle sizes, and the use of CSS-in-JS to achieve easy styling, which the React team now discourages us to use.
However, trying to reproduce this flexibility with plain CSS is hard. I first tried this while going through 30 Days of React tutorials, and it was a hot mess. I now 100% agree that Tailwind is not built for this, but SASS mixins weren’t particularly pleasant either.
I finally had a “Eureka” moment when I was reading at Svelte Docs on style props: I can use CSS variables to dynamically style the components! Of course, I am reinventing the wheel since there are many fascinating UI libraries that only use (S)CSS, such as PrimeReact and Blueprint, but I took this as an opportunity to know more about Svelte and modern CSS.
Now let’s have a look.
Svelte Components with Themes
We will build a component with pre-defined styles that can be toggled with two props: variant and color. For example, to make the following buttons,
we only need the following code:
App.svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>importButtonfrom"./Button.svelte";</script><div><Button> Gray default
</Button><Buttonvariant="outline"color="blue"> Blue outlined
</Button><Buttonvariant="subtle"color="red"> Red subtle
</Button><Buttonvariant="filled"color="green"> Green filled
</Button></div>
Note Because creating a colour system is a painful task, I will use Radix colors for the component. The library provides CSS variables for their colour scheme, which essentially looks like:
<script></script><!-- passing the event listeners and props --><buttonon:click{...$$restProps}><slot/></button><style>button{padding:0.6em1em;font-weight:600;border:1pxsolidtransparent;border-radius:4px;cursor:pointer;/* for buttons with icons */display:flex;gap:0.6em;align-items:center;}</style>
We will then add a “default” style to the button, that is, how the button should look like if no props were passed on. My default style is a grey button without border or background, just like the case of Material UI. Don’t forget to add :hover and :active selectors to make the button respond to user actions.
<script></script><!-- passing the event listeners and props --><buttonon:click{...$$restProps}><slot/></button><style>button{padding:0.6em1em;font-weight:600;border:1pxsolidtransparent;border-radius:4px;cursor:pointer;/* for buttons with icons */display:flex;gap:0.6em;align-items:center;/* colors */color:var(--gray11);background-color:transparent;border-color:transparent;}button:hover{color:var(--gray12);background-color:var(--gray4);}button:active{background-color:var(--gray5);}</style>
Adding variety with CSS variables
Now, it’s time to add different style and colour options to this component. As mentioned earlier, the component will have four different variant choices and a few colour options:
This looks like a painful job to code up the four variants, but it is not as tricky as it seems. Let’s first summarise the text, background and border colours for the variants in a table. Note the “odd” ones are in bold.
Default
Outline
Subtle
Filled
base
text
color11
color11
color11
white
background
transparent
transparent
color3
color9
border
transparent
color7
transparent
transparent
:hover
text
color12
color12
color12
white
background
color4
color4
color4
color10
border
transparent
color8
transparent
transparent
:active
background
color5
color5
color5
color11
Because the variants share a fair amount of settings, we can selectively define CSS variables and provide fallback values, instead of setting up every variable for every variant. For example, only outlined buttons have non-transparent borders, so we can do something like: