Tailwind Patterns: Slots
2025-11-08
Tailwind has been my go-to styling solution for the web since the library's early days. I first discovered it when it was about version 0.4, and have loved it ever since. Being a newbie web dev at the time it made me feel like I could move so much faster, and the out-of-the-box "design system" that it shipped with made my designs consistent and look much more professional than something I might have cooked up by hand.
Enough glazing, I want to talk today about a pattern that I think improves Tailwind use in a variety of scenarios, but particularly in one that I often find myself in: creating primitive components. The pattern I'm talking about is "slots". This is a pattern that has become more popular after adoption by shadcn/ui and even the Tailwind team themselves.
Imagine that you are building a web application and one of your lower-level components is a menu item for a dropdown list. The menu item is used in various places, and has a few variants:
- Only text
- Text with an icon to the left
- Text with an icon to the right
- Text with icons on both sides
In use the component is going to end up looking something like:
<li>
<svg>...</svg>
<span>...</span>
<svg>...</svg>
</li>But how can we ensure consistent styles across usage? Slots.
We can make use of data attributes to create a sort of list of styles for well-known child components. In our case, we know that we want to support icons on either side of the menu item, and text that takes up the rest of the space. If there is no left icon, the text should align to the left side of the menu item.
<li class="*:data-[slot=icon]:size-5 *:data-[slot=content]:flex-1">
<svg data-slot="icon">...</svg>
<span data-slot="content">...</span>
<svg data-slot="icon">...</svg>
</li>I've omitted some classes here, but the main point is that we can target elements via data attributes, they don't even have to be direct children of the <li>. If we choose not to pass icons, the styles just won't get applied because there are no matching elements.
If you are consistent with your slot names, you can get some great composability out of this approach. For example, there are probably many components that accept icons as children: menu item, button, link, badge, input, just to name a few. If they all standardize on the same slot name, then reuse becomes simpler.
You might abstract the various parts so that data attributes are baked in and can't be mistyped:
function Icon({ name, ...props }) {
return (
<svg {...props} data-slot="icon">
<use href={`/svg/spritesheet.svg#${name}`}></use>
</svg>
);
}Now our previous example becomes something like:
<MenuItem>
<Icon name="user" />
<Text>Account</Text>
<Icon name="chevron-right" />
</MenuItem>And it all works together nicely. I can remove either or both of the icons and the styles do what we want.
I'd recommend checking out the following resources for examples of the slot pattern in action: