Design Systems with Storybook, v2

Class Variance Authority

Steve Kinney

Steve Kinney

Design Systems with Storybook, v2

Check out a free preview of the full Design Systems with Storybook, v2 course

The "Class Variance Authority" Lesson is part of the full, Design Systems with Storybook, v2 course featured in this preview video. Here's what you'd learn in this lesson:

Steve uses the Class Variance Authority library to add type-safety to the variant styles that can be applied to the button component. This abstracts the CSS styles within the button component and centralizes them in a single variant TypeScript file.


Transcript from the "Class Variance Authority" Lesson

>> And so there's a version of this cuz nobody wants to watch me type that many Tailwind classes. If you do, I have a course that you can watch some other time, if you wanna watch me type that many Tailwind classes. But if we look at this, this is what the button that we had in our little anthology design system kinda looks like.

So this is kinda taking that CSS module that we had and refactoring it into Tailwind. Again, Tailwind is not like some of the things that came before Bootstrap or whatever, where it's a theme. It comes with a default theme that you can toss in the garbage and use your own if you want.

What I like it for is stuff like now I don't need to write these, either pseudo selectors or media queries in the case of responsive design and dark mode. I can just basically use focus column, they'll give me a focus state. I can use disabled column, will give me a disabled state.

There's also some other helpers like, you have the situation where if someone hovers over the entire card, everything should change underneath it. And so there's a group selector appear. So if they're hovering over the pier, or they're hovering over the parent or something like that, you can cascade those things down.

Could you write those on regular CSS? Absolutely, do I want to? I don't, right? And then getting the ability if that tree shakes all the unused CSS is just useful. For us, we use it less as like, we use all the Tailwind thing and all that stuff. We use it more as like a utility knife for things might otherwise be a little more tedious.

So here we've got our variance, great, awesome, right? Well, this is the base styles that they all got, right? And then we can say, okay, we've got the primary, well, that's a color that we pulled in in the last section when we play so Tailwind theme. So instead of purple, it's primary.

And now, if I need to change primary to red, which we shouldn't do, or blue or green or what have you, I can switch literally the actual theme file and it'll all just work. And like I said, I could take that the step further, where we just call it interactive.

And I can say that it's like should be the primary button color and change where those variables point as well. That's kind of like up the levels of the Galaxy Brain, what you might choose to do. And I'll show you our code for that in a second as well.

Secondary button, okay, that one's white with the different borders and the different active colors, right? Destructive and then the default variant is before I had primary, I can choose to make it secondary, whatever. It doesn't really matter and we can put a default size and all of those things in there as well.

And then basically, now we can derive the type from our styling. So we add another variant, automatically, that gets added to that ButtonProps as well. So let's pull this in and take it for a spin. So there's two choices, you can include this in the file itself of our component or you can choose to put it in its own file.

I think before I did it, that one is its own file, but if you do wanna share it across code bases, right? Having it as just as a regular JavaScript or TypeScript file that you can just import anywhere is super useful. So let's do that pattern. And so you can call it variant.

If it's in the button directory, you can call it button-variant. It really depends on how much you wanna be able to command P, okay, or whatever editor you use to get into it. We'll call it button-variant.ts, and we will paste this in. And then we'll also, Call it ButtonVariants.

And so if we look at this type now, you can see that we only have the variant, we don't have the sizes in here, we can do that, too. But you can see that it is already very similar to what we kinda wrote by ourselves. But you can imagine, if you looked at all the button combinations I have, I'd be writing more and more types.

They might get out of sync, and then I'd have to match them up with the default params. And all that stuff that I was doing earlier, it's nice to just get that all for free. And again, this is basically just a JavaScript function. If your needs are slightly different and there's nothing, this doesn't have to be Tailwind, these are just class names.

Yes, those are then powered by a Tailwind. But if you have an existing style system, right, and you have a bunch of class names that you wanna use the same approach, it works there, too. It doesn't really matter in any way, shape, or form. So we've got that in place, let's go ahead and pull it into our button now.

So here, we can just say, import, do I have an export on export const variants. Sure, import now, variants and then type ButtonVariants. Nope, not from there, we wanna do it from the ./button-variants. And now, we'll move size in a second, but now what this can become is we can say, & ButtonVariants.

So it's basically, we'll get rid of the size momentarily. Our type is eventually just going to be, hey, all the default props that a regular HTML button takes plus whatever my design system supports. And then you never update this again, right? You kinda get all the types, and even storybook will catch all of these things as well.

And so then now in here, we'll kinda lose the sizes for a second. It'll be okay, we can get rid of all of this. And we can just say that this is variants. And then unfortunately, because we've followed the predominant nomenclature, the button-variant, eventually, we'll add size in here as well.

And it's gonna look like a primary, it'll get you the base classes. It will get you then all the other classes specific to the primary button. And if you pass in some garbage that doesn't work, the type checking in there will know, right? And you'll kinda get that helpful red squiggly line that is frustrating when you don't like it.

And super helpful when you just made a typo when you're live coding in front of all your friends, right? And so we can kind of flip back over to our storybook, that's Microsoft Storybook. Let's close, probably comes hard. It looks like we need to do a little bit of tweaking there, but we get the kind of like, it's because I think sizes is where the padding is.

So I need to bring that back in, but I kind of get the initial colors in place as well and kind of support for all of that. So let's say we wanted to add in the sizes, we can do that as well. I don't have them memorized, so we'll kinda take some lucky guesses from the number of times I've done this over and over and over again.

We can go in here and we can say, okay, we don't even need to have necessarily base styles for the sizes cuz we can have this default size. So we'll go add another one here, we'll call it size. Here's the nice part about some of this autocomplete stuff, is that when you've worked on the same repo, preparing for things over and over and over again, sometimes it's caught on to what you're gonna type.

We'll see if this is totally right, but that looks like literally what I typed a thousand times. This is for Tailwind, this is our x padding, our y padding, and these are based on RAM. So a p of 4 is 1 RAM, 2.5 I wanna say is 10 pixels, 1.5 is 6 pixels.

I hate that I know this. But in the actual Figma for the design system, you can actually see this as well. And the fonts that we saw in that Figma, they're all kind of mapped to these correct sizes as well. And then we can say that the default size is medium, just like we had before.

And what's super cool about this is if we look at, so we got rid of all our style sheet and everything along those lines, so we're down one file completely. If I look at the ButtonVariants, look, it now also just automatically updated to say, hey, this thing also supports an optional style prop.

All right, there's either small, medium, or large. And because it's an optional one, cuz we have a default value, null and undefined also work as well, right, cuz it will fall through to the falsie value. And now adding sizes, ready for it, sizes or size. How many times have you guys made the same mistake by pluralizing it?

And our button is effectively back with the entire font that we had before, and the sizing that matches to what I had in the Figma file as well. And what's kinda cool about this is that because everything in that file is based on the work that the product designer hash on my team worked on as well.

Is that their Figma variables are mapped to the same theme values that we have. So all we do is export a new version of those pasted in to the same theme file, and any changes they make, we get completely for free, right? And that kind of interrupt there because we aligned on a system that works and we have the tooling in place.

And what we'll see in a second, we'll set up some visual regression testing, not in a second, a few minutes. We'll set up some visual regression testing so we can actually then see like, did this change in the way we thought it changed, right? And you can start with just the button, but then you can have one of your alerts, things that have buttons in them.

Do they all change? You can kinda get this high level thing. And the visual regression testings are great for two reasons, right? One, if you didn't mean to change something, that's the value prop that says on the thing, it should catch that. But two, it's a great way of you may be intentionally changing the button, but maybe you forgot all the places the button might be, right?

It's one thing to go look at the button story, but as you kinda build up the larger pieces in your design system, it's great to be like, hey, this changed also. And you can just kinda go ahead and validate it, right? Especially for our website, we run it on every single PR, right?

We'll just generate all the visual diffs, right? A lot of times yeah, they're changing on purpose because what do you think we were doing? But we can actually see the major one, only things we changed actually changed. And two, that maybe we didn't need to change all the buttons, we didn't really know all the places the buttons were.

Like, this one button, it being bigger like we intended in a table header or something like that is actually not what we wanted. And it calls it out before our customers do, cuz like I said, you always have tests. It's just a big question of, are you doing it manually?

Are they automated? Are your customers filing a bug report? So cool, we've got that class variants, and that will work across everything. I have a few more examples in the code base if you just wanna see it in action. So here, we've got one for a badge, right, where we can have what the default badge looks like, primary.

And all of a sudden, first of all, with a little bit of multicursor, how hard it's to edit this file? Not that hard. If I needed to change hypothetically, if I was going to, what is it like? If there's gonna be something in a later chapter, make sure you mentioned it earlier.

Let's say hypothetically one of these badges isn't accessible in dark mode, hypothetically, right? And I needed to change the contrast of all these dark mode ones, right? This becomes incredibly easy to manage. If I had gone to the full galaxy brand with this, which we could choose to do, we'll take a look at it in action in a second.

Then you could literally kind of just go and find that one class that points to something as well. But even that, doing a large scale change of all of the different contrasts would be roughly the same amount of work. I'll show you a little bit about what it's talking about before, which is all where you can take this.

So we have a, I'll go back a file, we've got this colors.ts, which looks shockingly like the colors.ts that I showed you earlier, right, that we had. And yes, secretly, there are blues, and there are cyans, and there are greens, and there are all sorts of colors cuz I have statuses of a lot of things, and they all have to be different colors.

So I have more colors than I would like, but we have all these colors. But you can't use purple in our code base, right? This is just like the kinda list that then gets abstracted away. We have color primary, a CSS variable, so not Tailwind-specific. We then map it to also a Tailwind class as well.

But you could theoretically use --color-primary, and you will get the primary color are secondary. And you can see this is a little helper function I wrote that will go get slate 950, but it's the same idea. It's just pulling off that JavaScript object. I didn't like writing square brackets.

And this also has a TypeScript interface on it so that, if you've watched me type, I make typos, [LAUGH] right? And nothing worse than like, it's just undefined, you don't know why it's working. It's like some of those things with key values, if you miss it, it doesn't blow up, it just doesn't work.

So this will actually give me a red squiggly line, but it's just getting the key and value of those colors. But then throughout, we've got you know this primary color text, we've got what it means to be a surface, which is kinda any card table. And then what we do is I have other values that we just switch the values of those same CSS variables.

So we'll use the default Tailwind stuff with dark:/. But you can theoretically just reference these variables and then swap them out. And so I can add six different modes, and it's really just like I swap in a different key value store. And what's cool about this approach is if it should be the same color across both, we start with the base CSS variables, and I only override the ones that change on the theme, right?

But then at end of the day, these just map to Tailwind classes, right, or I could just use the var syntax and CSS variables. The reason that we chose to map into Tailwind classes is that in Teleasense, where I do BG and I see all of them and the colors.

It's an ergonomics thing, not a thing that you need to do. And if you don't use Tailwind, you can just literally use the same approach as the CSS variables. There's nothing special or Tailwind-specific. I just like auto complete a lot and I'm willing to do ridiculous things to have that ergonomics, right?

And cuz then all we do at the end of the day is I add that root selector to the CSS file with all my variables, right? You can imagine what the CSS looks like in this case. And then if it's got that, we had to put the body class on dark at this point.

And then we have these additional classes, surface primary, surface secondary, so nobody references a color ever, right? And if I need to change when these are someone comes into this file and tweaks them, right? And yes, it took us a while to then replace all the colors and use these and figure out what they should be.

But now adding another theme or tweaking anything, I just tweak one file. And then what we'll see when we get to the testament storybook, and then I have an automated set of things, then verify that I did not mess things up along the way.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now