Writing clean classed components
tw-classed
accepts any number of class arguments. This guide aims to help you organize your classed functions in a way that makes sense for any developer. This is a very subjective topic, so I'll try to give you some ideas and let you decide what works best for you.
Creating your first classed component
Lets create a button. We'll start with a simple button that has a default class and a variant.
First decide whether you want to call the function directly or call classed.button()
.
// Button.tsx
import { classed } from "@tw-classed/react";
const Button = classed("button", "px-4 py-2 bg-blue-500 text-white rounded-md");
const Button = classed.button("px-4 py-2 bg-blue-500 text-white rounded-md");
These two are equivalent. For the rest of the guide, we'll continue with the second option.
Adding variants
Variants are a great way to add additional classes to your component. They can be used to add additional styles or to change the appearance of the component. Let's make our button change colors based on props.
const Button = classed.button("px-4 py-2 bg-blue-500 text-white rounded-md", {
variants: {
color: {
primary: "bg-blue-500 text-white",
secondary: "bg-black text-white",
},
},
});
Now we can use the color
prop to change the appearance of the button.
() => <Button color="primary">Primary</Button>;
() => <Button color="secondary">Secondary</Button>;
Using default variants
The above code works just fine, however it results in duplicate color classes when it's rendered. This is because we are already applying bg-blue-500
& text-white
in the default class string. This often causes big UI issues and developer confusion. We can fix this by using the defaultVariant
option, and removing the default classes from the main class string.
const Button = classed.button("px-4 py-2", {
// No colors by default
variants: {
color: {
primary: "bg-blue-500 text-white",
secondary: "bg-black text-white",
},
},
defaultVariants: {
color: "primary", // Default to primary
},
});
Now, when we render the button, it will have the color classes applied with no defaults.
() => <Button>Primary</Button>;
() => <Button color="secondary">Secondary</Button>;
Organizing your classed components
Organizing your classed components is a very subjective topic. I'll give you some ideas and let you decide what works best for you. tw-classed
accepts any number of classes and arguments. Let's say we have a simple card and want to add some media queries, hover and focus states. Lets look at some different ways to organize this.
The card should:
- Change padding and width based on screen size
- Change background color on hover and focus
1. Dumping all classes in one string
const CardButton = classed.a(
"p-4 md:p-6 lg:p-8 w-[200px] md:w-[300px] lg:w-[400px] text-center bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:bg-blue-700"
);
() => <SimpleButton>Simple Button</SimpleButton>;
This is the simplest way to organize your classes. It's also the most error prone. If you have a lot of classes, it can be hard to find the one you're looking for.
2. Using multiple arguments
const CardButton = classed.a(
"p-4 w-[200px] text-center text-white bg-blue-500 rounded-md", // Defaults
"md:p-6 lg:p-8 md:w-[300px] lg:w-[400px]", // Media queries
"hover:bg-blue-600 focus:bg-blue-700" // Hover & focus
);
This is a bit better than the first option. It's easier to find the classes you're looking for.
An even simpler example would be a Grid component
const Grid = classed.div(
"grid gap-4", // Mobile first
"md:grid-cols-2 md:gap-6", // Tablet
"lg:grid-cols-3 lg:gap-8", // Desktop
"xl:grid-cols-4" // Large desktop
);
Composing classes like this is a great way to organize your components. It's much more readable than the first option.
Composition
What if I want to add variants to my Grid?
Thats easy too. Just add the variants to the end of the classed function. In fact, they can even go in between.
const Grid = classed.div(
"grid gap-4", // Mobile first
"md:grid-cols-2 md:gap-6", // Tablet
"lg:grid-cols-3 lg:gap-8", // Desktop
"xl:grid-cols-4", // Large desktop
{
variants: {
columns: {
1: "!grid-cols-1", // Notice the cheeky ! to override the current grid-cols
2: "!grid-cols-2",
3: "!grid-cols-3",
4: "!grid-cols-4",
},
},
}
);
() => (
<Grid columns={2}>
<div>1</div>
<div>2</div>
</Grid>
); // Regardless of screen size, this will always have 2 columns
I want a a special Grid that has background color.
No problem. Simply create a new classed component and add the Grid component as a child.
const CardGrid = classed.div(
"bg-gray-100 p-4 rounded-md",
Grid // Add the Grid component as a child
);
() => (
<CardGrid columns={2}>
<div>1</div>
<div>2</div>
</Grid>
);
Want to make the colors configurable? No problem. Just add the variants to the CardGrid component.
const CardGrid = classed.div(
"p-4 rounded-md",
Grid, // Inherit the Grid's classes and variants
{
variants: {
color: {
gray: "bg-gray-500"
blue: "bg-blue-500",
red: "bg-red-500",
},
},
}
);
() => (
<CardGrid columns={2} color="red">
<div>1</div>
<div>2</div>
</Grid>
);
Why is the Grid component in the middle of the classed function?
We added it in the middle because we want the Grid's classes and variants, not render it. If we wanted to render it, we would add it to the beginning of the classed function.
This works with any component that accepts a className
prop. For example, next/link
.
const CardGrid = classed(
Grid, // Render the Grid component
"p-4 rounded-md",
{
variants: {
color: {
gray: "bg-gray-500",
blue: "bg-blue-500",
red: "bg-red-500",
},
},
},
);
() => (
<CardGrid columns={2} color="red">
<div>1</div>
<div>2</div>
</Grid>
);
Conclusion
I hope this article has given you some ideas on how to organize and compose your components. I've been using this pattern for a while now and I'm really happy with it. I hope you find it useful too.