Using Button Variants0:00
Let's talk about Multistyle components as in components with multiple style variants in Chat and UI. Since we already have a Button component generated, we're going to take that one for a spin. So in the welcome page I will go and replace that input from the previous lesson. And let's add a little flex container so that flexGap four items center. And here we are going to bring our Button component from our UI directory.
And here we are going to bring our Button component from our UI directory and just call it button. Okay, here's our default button and now let's go check out some of the variations for it. So I'll duplicate that one and this one will have a variant of, and how awesome is that? Autocomplete thanks to TypeScript. Uh, we will have a secondary button,
Autocomplete thanks to TypeScript. Uh, we will have a secondary button, let's call it secondary. Duplicate this and let's try another. Let's go with outline and let's do one more. We don't have to do all of them. And this one is going to be the ghost one. Okay, so now we have a range of different looking buttons, uh, which is super cool. And let's also have a couple
uh, which is super cool. And let's also have a couple of different variations for the size. So next to the first one, actually before it I'll have a size attribute and we'll go with large and also have a small one. I think it was small. Was it? Yep. SM after the default button. Alright, so now we have a good representation
Rebuilding Button from Scratch1:25
SM after the default button. Alright, so now we have a good representation of the capabilities of our button component. Now if we look back at the code for this button component, uh, at first site you might have the impression that there is a whole lot of code happening just for a simple button. So what we're going to do is recreate it from scratch naively. So we're going to create a new component,
from scratch naively. So we're going to create a new component, a simple button component, and we're going to go through the journey of adding multiple style variants to it. Okay? So in my components/UI directory, next to the ChatUI components, I will create a new MyButton.tsx file and here will export a function called MyButton. And for now, let's just return a button.
Call my Button. And for now, let's just return a Button and it's going to text for text the children pass to it. So let's receive here children, which is React, that ReactNode, and I'll just import the ReactNode type directly like so. Okay, so we have that new MyButton component. So I will duplicate this whole section and let's add a empty dash-4 here. And here we are going to, instead of these Buttons have
and let's add a empty dash four here. And here we are going to, instead of these buttons have myButton, which should be available. And we'll call it myButton. Now it's completely unstar at this point. So with tailwind preflight it should just look like text. And yep, here it is, our glorious button components. Alright, so let's give it some style. So it looks like an actual button. So here I will go, class name equals
So it looks like an actual button. So here I will go, class name equals and let's go bg, let's make a little bit different. So indigo-600 text-white text-sm font-medium. And we'll add some padding with px-5 py-2. And finally some rounded corners with rounded hmm sm. All right, let's go take a look. And yeah, that's a pretty decent looking button. Let's just add some hover and active styles.
And yeah, that's a pretty decent looking button. Let's just add some hover and active styles. So on hover we'll go bg-indigo-500, so it's a little bit brighter. And on active bg-indigo-700. So it feels like it's pressed into the page. Alright, hover active. Nice. Alright, so this is a base button and we're going to go and add multiple style variants next. But before we do that, let's see what happens when we try
Supporting Native Props3:41
and add multiple style variants next. But before we do that, let's see what happens when we try to pass an attribute to the button where we consume it. So back in my welcome page. And you know what, I'll collapse the shazi and UI button div here. And let's say that we want to make the button disabled. So I will pass the disabled attribute and right away you see that red squiggly, which is not a good sign.
and right away you see that red squiggly, which is not a good sign. Back in the myButton definition, I will add some stars for the disabled status. So let's go bg-slate-300 and text-slate-500. Yes and that should be enough. And actually we'll have two buttons, the default button and then the disabled ones just so we can compare them. Alright, let's go take a look and it's not working,
and then the disabled ones just so we can compare them. Alright, let's go take a look And it's not working, the button is not disabled. I can interact with it and that's what the squiggly was trying to tell us. So let's take a look at what's happening. If I hover over disabled, it's going to tell me that the property disabled is not existing on the type that we've set to our button. Our button, my button only accepts children.
that we've set to our button. Our button, MyButton only accepts children as props and nothing else. So when you try to pass disabled to this button, it just flies right over the button and out it goes. So what we need to do is open up the amount of props that the component can receive and uh, I'll define the type here MyButtonProps so we can see a little clearer. So we'll have the children, which is ReactNode.
so we can see a little clearer. So we'll have the children, which is ReactNode. And so now we can replace that with our myButton props. This is exactly the same as before, but now we are going to add all the possible HTML attributes for the button component. So, and yes I could use this, but I like to use the componentProps type and then pass the HTMLAttributes type in there. So now the button can receive children and any other props,
and then pass the HTML elements type in there. So now the button can receive children and any other props, but I need to receive all these other ...props here. This represents everything else but children and we need to spread them on the button components here. And so now the disabled attribute should be received in these spread and then add it to the button. And indeed it's now working. Nice. So looking at this code should remind you of something that we've looked at in the previous lesson in this series.
Merging className Safely5:51
So looking at this code should remind you of something that we've looked at in the previous lesson in this series. Let's go and add a className attribute to the first button and go uppercase to make the button uppercase. Now once again, I'll need you to close your eyes and visualize what this button will look like. Whoa, is this what you had in mind? Remember as we pass the className attribute to the Button component here, that className attribute is received in the props.
to the Button component here, that className attribute is received in the props and spread onto the Button component after the button's own className. And therefore it's going to override it. And so all of these classes here will be wiped out and replaced with just uppercase. And so you know the drill because you've seen it in multiple components in the CN function lessons.
because you've seen it in multiple components in the CN function lessons. We need to also receive a class name optionally set to a string and then accept it here we can default it to an empty string. And the last thing we need to do is merge this class name with the other class name and resolve conflict. And so we can use the CN function for this and we're going to pass all the base classes as the first string
and we're going to pass all the base classes as the first string and then pass our class name attributes and looks like I have not imported cn. So let's do that now. So this time this button should look exactly like the expected default button with the indigo background, the padding and all, but also have uppercase text and yep, that's working correctly. Alright, now we're in a nice place
Adding Size and Look Variants7:20
and yep, that's working correctly. Alright, now we're in a nice place with our default styles for the button. So it's time to go and add multiple variants. Let's start with the size variant. Okay, so we'll want to add three different sizes to our button. And so I'm going to expand the props for this button with a size variant. Again, let's make it optional.
with a size variant. Again, let's make it optional and we'll have three values like copilot suggests, so large or medium or small. And the reason I'm making it optional here is I will have a default value and we'll set that to medium as default. So you can see that size is squiggled because it's not used anywhere. So what we wanna do next is go and before the final class name here,
So what we wanna do next is go and before the final class name here, maybe add some conditional logic based on the size. So size equals large and in that case maybe we'll bump the padding to px-6 and py-3. And if the size is medium, we will have yes these values and so we should to be clean, remove them here. But because we have tailwind-merge, it actually doesn't matter.
But because we have tailwind-merge, it actually doesn't matter. But let's still keep our code clean. And then if the size is small, copilot knows exactly what to do. Thank you very much. This should now take care of our size variance. So we have the base glasses and then the size modifiers and then whatever the consumer passes to it will always override any of that.
and then whatever the consumer passes to it will always override any of that. So okay, I'll delete the disabled button because we know it works. And remove the class name here. And what we'll do is have a large, medium, and small buttons. And so for the myButton, I should be able to go size yep and have the other complete suggestions. Wonderful. For large, the medium does not need one.
and have the other complete suggestions. Wonderful. For large, the medium does not need one because medium is the default. And then we can go small for the small one. And let's go take a look. Hey, hey, hey, that works really well. Okay, so now let's go about having some variants. Uh, let's call them look maybe different looks for the button. Alright, back to the type definition.
different looks for the button. Alright, back to the type definition. We go and we are going to add a look properly optional again. And so we'll have primary, which is the button we have now and then secondary. And then yeah, let's go with outline. All right, I think that's enough for learning purposes. And so once again we are going to receive the look and we will default it to primary.
And so once again we are going to receive the look and we will default it to primary. One more time. We need to actually use this. So if the look is primary, we are going to have bg-indigo-600 and text-white, but also hover:bg-indigo and active:bg-indigo. So let's cut these classes from up there and add them in our primary look and also want to remove the bg-indigo and the text-white from the default styles.
and also want to remove the bg-indigo and the text-white from the default styles. Alright, and we let Copilot handle the other looks because it knows exactly what we're trying to do. Yes and yes. Did you give me a transparent background? There's a border bg-y, yeah. Okay, I'll just go bg-transparent here. Okay, so our class name is getting a little bit out of hand, but we should have the base styles, the size variations, the look variations and the overrides.
but we should have the base styles, the size variations, the look variations and the overrides. So let's create a couple more buttons, secondary and the outline. Thank you very much. Good boy Copilot. And here we go. Here are our variation. AI generated. Not too bad. One thing I do not like, if I zoom in, as you can see that the border or outline actually spreads out compared to the other buttons.
or outline actually spreads out compared to the other buttons. This is not the case in shazi and UI buttons. And we can fix that by other having an insetRing or we could have a transparentBorder on the elements here that don't have a border. So I'll do that real quick. You can see here that we have a border and then it's slate-300. So what I could do is on the base styles
and then it's slate 300. So what I could do is on the base styles or on the two looks, but I'll do it on the base styles, we have tailwind-merge anyway, is to have a border, which is by default gray-200 I think. Uh, it doesn't look good at all, but we can also add border-transparent. And now the padding of the button will fill that border space.
And now the padding of the button will fill that border space. And so you can see that it's perfectly aligned. If I use the precision screenshot tool, you will see that the buttons are perfectly lined up now. Okay, so now we have multiple variants, pretty cool, but keep in mind this is a pretty contrived example and that thing is starting to look gnarly. So let's organize our styles a little bit differently. I wanna take the conditional logic in the styling here out.
Refactoring with Lookup Objects11:45
So let's organize our styles a little bit differently. I wanna take the conditional logic in the styling here out of the class name to have a slightly simpler to reason about class name stack. So I'll go at the top of the file and I'll create what you can refer to as a styleLookup object size classes. And basically this is going to be an object that references the possible classes based on the possible sizes, cons, and copilot instantly knew
that references the possible classes based on the possible sizes, cons, and copilot instantly knew exactly what I wanted to do. And while we at it, let's also do the look classes since copilot seems to be on a roll and it should give me the variation for all three. Perfect. Well I say perfect, but I haven't looked at it, but it looks pretty good. So the idea behind doing these size classes
but I haven't looked at it, but it looks pretty good. So the Idea behind doing these size classes and look classes is that we can then use the size and look strings pass to the component and then go and look up, find the correct key and have these classes output instead of having the logic here. In other words, I can replace these three size lines here and go fetch in the size classes object the size key that we've passed.
and go fetch in the size classes object the size key that we've passed. And the same for the look classes. So I can remove these three lines. And so I don't know about you, but this feels a lot more digestible and easy to reason about. We have base classes, size classes, look classes and overrides and everything should still work the same and it does.
and overrides and everything should still work the same and it does. So in my opinion, this is a pretty good improvement. We have a simple Button component here and all the logic for the styles leaves up here. Now there's a serious risk in doing this. And let me expose this by changing the small key here to small. You can see that I've butchered the small components, although it's arguably small.
You can see that I've butchered the small components, although it's arguably small. Uh, but the stars are not what we were expecting. And the reason is that here, and you can see TypeScript has picked that up. We are trying to access the size classes for the size property, but the size property can only be large, medium or small. And that doesn't exist because we have passed small instead. So to make sure that this is a little bit more robust,
Typing Variant Maps13:52
And that doesn't exist because we have passed small instead. So to make sure that this is a little bit more robust, what we can do is add some types to the size classes object. What we want these size classes to be is an object that has all exactly all the same keys as the size possible values, all of them no more and no wrong ones. And so we can reference this as a record in TypeScript. So I'm going to say that we want it to be a record, which is essentially a series of key value pairs.
So I'm going to say that we want it to be a record, which is essentially a series of key value pairs. So think of it as key value. And we want each key to be each one of the size properties. And so we can go myButtonProps and then reach for the size values. And then the values can be anything. So we'll have a string here. Alright? And look at this. The small key has been picked up being not part of large, medium and small.
The small key has been picked up being not part of large, medium and small. This is exactly what we wanted and there seem to be another issue. The string can be undefined. And one way to do this is to make that my button prop size non knowable. So I can wrap this in a nullable in brackets like that. And that should work. And don't let this scare you.
in brackets like that. And that should work. And don't let this scare you. This is actually super useful. It's just making sure that we always have the correct values for the classes object. So if I change this to small now it's going to be happy, but if I was missing a key like medium, it's going to instantly tell me, Hey, property medium is missing. Pretty cool, right? And I will copy the record non knowable string key value pairs here.
Pretty cool, right? And I will copy the record non knowable string key value pairs here. And for the look classes we can do exactly the same, but obviously replace my button props size with look and same deal. If we had a typo, we would know immediately about it. Or if a key was missing, we would also know about it instantly. I think that approach would style lookup object that have the correct shape thanks.
I think that approach would style lookup object that have the correct shape thanks to TypeScript works really well. And I personally use this in many, many projects where I want multiple style variants. Okay, I wanna do one more thing. This is not completely necessary, but because we have size classes and look classes, I'm going to take this string and replace it with base classes.
and look classes, I'm going to take this string and replace it with base classes. That doesn't exist yet, but before the different lookup object, I'll have base classes and I will have const base classes and paste that here. So now we have all our style concerns at the top, and then our button definition is pretty minimal. And I like how it merges the different classes. This is very, very readable. And you know what, let's go back to the top one more time
Comparing to CVA Approach16:23
This is very, very readable. And you know what, let's go back to the top one more time and look at the structure of our codes. We have types, then we have styleVariance, and then we have the components definition. And now let's compare this with the ChatsyNUIButton component where we have styleVariants. Here are the base styles and then the variant styles. And then later on we have a button definition.
Here are the base styles and then the variant styles. And then later on we have a button definition with a fairly simple class name that merges multiple styles variants together. So there are a few more things going like these data slots or the child, but the main difference is there is no type definition. You can see that types seems to be inferred from the button variants using variant props here.
to be inferred from the button variants using variant props here. So the button variants are actually all these styles variants here. And then the types are inferred from these button variants using CVA variant props type. And so now that you know what it takes to create multiple style variants by hand, we are going to implement them with CVA and see how it simplifies the process.
to implement them with CVA and see how it simplifies the process and makes it a bit more functional.
