Skip to content

Commit e19ef88

Browse files
committed
tweak tailwind post
1 parent baaffd9 commit e19ef88

File tree

1 file changed

+108
-58
lines changed

1 file changed

+108
-58
lines changed

content/posts/tailwind-targeting-child-elements.md

Lines changed: 108 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,59 @@ draft = false
88

99
The whole point of Tailwind is applying utility classes directly to elements. Styling generic elements like `p` or `div` with descendant selectors goes against the grain—it's the kind of thing Tailwind was designed to replace.
1010

11-
But sometimes you don't have a choice. Maybe it's content from a CMS, a third-party component, or dynamically generated HTML. You need to style elements you don't control. In vanilla CSS, you'd write a selector like `.third-party-stuff > p` and move on. But what about Tailwind?
11+
But sometimes you don't have a choice. Maybe it's content from a CMS, a third-party component, or dynamically generated HTML. You need to style elements you don't control.
12+
13+
**Let's be clear upfront:** adding a small piece of vanilla CSS to handle this is often the simplest and most sensible solution. A dedicated stylesheet for CMS content is a perfectly valid approach. But if you're committed to staying within Tailwind's utility-class paradigm—or just curious about what's possible—this post shows how you _can_ target child elements using arbitrary variants.
1214

1315
## The Problem
1416

15-
Let's say you have a container with some paragraphs:
17+
Let's say you have a container with embedded HTML you don't control:
1618

1719
```html
18-
<div class="content">
19-
<p>First paragraph</p>
20-
<p>Second paragraph</p>
20+
<div class="cms-content">
21+
<p>Some text with a <a href="#">link</a> in it.</p>
22+
<ul>
23+
<li>List item one</li>
24+
<li>List item two</li>
25+
</ul>
2126
</div>
2227
```
2328

24-
You want all paragraphs inside `.content` to have specific styling. The traditional approach? Write custom CSS:
29+
You want all links inside to have specific styling—underlines on hover, a distinct color that differs from the inherited text color, maybe a font weight. The traditional approach? Write custom CSS:
2530

2631
```css
27-
.content > p {
28-
color: blue;
29-
margin-bottom: 1rem;
32+
.cms-content a {
33+
font-weight: 600;
34+
text-decoration: none;
35+
}
36+
.cms-content a:hover {
37+
text-decoration: underline;
38+
}
39+
.cms-content li {
40+
list-style-type: disc;
41+
margin-left: 1.5rem;
3042
}
3143
```
3244

33-
But that defeats the purpose of utility-first CSS. You're back to maintaining a separate stylesheet, naming things, and context-switching. Not the end of the world, but not let's see if if there's another way!
45+
That's a perfectly valid approach—and often the right one! A small stylesheet for CMS content is simple and maintainable. But let's see what Tailwind offers if you want to keep everything in utility classes.
3446

35-
## Arbitrary Variants to the Rescue
47+
## Arbitrary Variants: The Tailwind Way
3648

3749
Tailwind's arbitrary variants let you write any CSS selector directly in your class names using square bracket notation:
3850

3951
```html
40-
<div class="[&>p]:text-blue-500 [&>p]:mb-4">
41-
<p>First paragraph</p>
42-
<p>Second paragraph</p>
52+
<div
53+
class="[&_a]:font-semibold [&_a]:no-underline [&_a:hover]:underline [&_li]:list-disc [&_li]:ml-6"
54+
>
55+
<p>Some text with a <a href="#">link</a> in it.</p>
56+
<ul>
57+
<li>List item one</li>
58+
<li>List item two</li>
59+
</ul>
4360
</div>
4461
```
4562

46-
That `[&>p]` syntax might look strange at first, but it's straightforward once you understand what's happening.
63+
That `[&_a]` syntax might look strange at first, but it's straightforward once you understand what's happening.
4764

4865
## Breaking Down the Syntax
4966

@@ -52,19 +69,18 @@ The magic is in understanding what `&` means. In Tailwind's arbitrary variants,
5269
So when you write:
5370

5471
```html
55-
<div class="[&>p]:text-blue-500"></div>
72+
<div class="[&_a]:font-semibold"></div>
5673
```
5774

5875
Tailwind generates CSS that looks like this:
5976

6077
```css
61-
.\[\&\>p\]\:text-blue-500 > p {
62-
--tw-text-opacity: 1;
63-
color: rgb(59 130 246 / var(--tw-text-opacity));
78+
.\[\&_a\]\:font-semibold a {
79+
font-weight: 600;
6480
}
6581
```
6682

67-
The class name gets escaped (those backslashes), but the important part is `> p`. The `&` gets replaced with the generated class selector, and then your selector (`>p`) is appended. The result: any direct child `<p>` of an element with this class gets the styling.
83+
The class name gets escaped (them backslashes...), but the important part is the `a` descendant selector. The `&` gets replaced with the generated class selector, and then your selector (`a`) is appended. The result: any `<a>` element anywhere inside an element with this class gets the styling.
6884

6985
## Common Patterns
7086

@@ -73,95 +89,129 @@ Here are some useful child-targeting patterns:
7389
### Direct Children
7490

7591
```html
76-
<!-- All direct paragraph children -->
77-
<div class="[&>p]:text-gray-600">...</div>
78-
79-
<!-- All direct divs -->
92+
<!-- All direct divs get borders and padding -->
8093
<div class="[&>div]:border [&>div]:p-4">...</div>
8194

82-
<!-- First direct child only -->
95+
<!-- First direct child removes top margin -->
8396
<div class="[&>*:first-child]:mt-0">...</div>
97+
98+
<!-- Last child removes bottom border -->
99+
<div class="[&>*:last-child]:border-b-0">...</div>
84100
```
85101

86102
### All Descendants
87103

88104
```html
89-
<!-- All paragraphs anywhere inside -->
90-
<div class="[&_p]:text-gray-600">...</div>
105+
<!-- All links get hover underlines -->
106+
<div class="[&_a]:no-underline [&_a:hover]:underline">...</div>
107+
108+
<!-- All list items get disc markers -->
109+
<div class="[&_li]:list-disc [&_li]:ml-6">...</div>
91110

92-
<!-- All links anywhere inside -->
93-
<div class="[&_a]:text-blue-500 [&_a]:underline">...</div>
111+
<!-- All images get rounded corners -->
112+
<div class="[&_img]:rounded-lg">...</div>
94113
```
95114

96115
Note the difference: `>` targets direct children only, while a space (represented as `_` in Tailwind) targets all descendants.
97116

98-
### Specific Elements
117+
### Pseudo-states on Children
99118

100119
```html
101-
<!-- Style the second child -->
102-
<ul class="[&>li:nth-child(2)]:font-bold">
103-
...
104-
</ul>
105-
106120
<!-- Hover state on child elements -->
107121
<div class="[&>button:hover]:bg-blue-600">...</div>
108122

109-
<!-- Disabled inputs anywhere inside -->
110-
<form class="[&_input:disabled]:bg-gray-100">...</form>
123+
<!-- Disabled inputs get muted background -->
124+
<form
125+
class="[&_input:disabled]:bg-gray-100 [&_input:disabled]:cursor-not-allowed"
126+
>
127+
...
128+
</form>
129+
130+
<!-- Focus styles for nested inputs -->
131+
<div class="[&_input:focus]:ring-2 [&_input:focus]:ring-blue-500">...</div>
111132
```
112133

113-
## When To Use This
134+
## When This Makes Sense
114135

115-
This approach shines when you're dealing with:
136+
To be honest, a vanilla CSS stylesheet is often the better choice for styling embedded content. It's simpler, more readable, and easier to maintain. But this arbitrary variant approach might make sense when:
116137

117-
- **CMS content**: You're styling HTML you don't control
118-
- **Third-party components**: The component doesn't expose enough styling props
119-
- **Prose content**: Markdown-rendered content that needs consistent styling
120-
- **Dynamic content**: Content generated at runtime
138+
- **You're already all-in on Tailwind** and want to avoid context-switching to CSS
139+
- **You need just one or two rules** and a whole stylesheet feels like overkill
140+
- **Your build pipeline makes adding CSS awkward** (though this is a smell worth addressing)
141+
- **You want the styling colocated** with the component that renders the content
121142

122-
For content you do control, just apply classes directly to the elements. That's still the Tailwind way.
143+
For content you _do_ control, just apply classes directly to the elements. That's still the Tailwind way—and frankly, it's simpler than any of this.
123144

124145
## A Practical Example: CMS Content
125146

126-
Here's the scenario that prompted this post: we display articles from a headless CMS (well, not _quite_, but for the sake of keeping things simple, let's leave it at at that). The content arrives as pre-rendered HTML that we wrap in our own container. We don't control the inner markup—it might contain divs, paragraphs, links, whatever the CMS produces.
147+
Here's the scenario that prompted this post: we display articles from a headless CMS at my client's. The content arrives as pre-rendered HTML that we wrap in our own container. We don't control the inner markup—it might contain paragraphs, links, lists, images, whatever the CMS produces. The structure, elements used (and lack of ability to add classes where we want) adds some interesting constraints.
148+
149+
(Note: always sanitize embedded content! But that's out of scope for this post.)
127150

128-
(Note: what we _also_ do before thinking of styling is sanitize the content! But that's out of scope for this post.)
151+
### The Vanilla CSS Approach (Often Best)
129152

130-
The solution is a simple wrapper that applies styles to its children:
153+
For anything beyond a few simple rules, a dedicated stylesheet is usually cleaner:
154+
155+
```css
156+
.cms-content a {
157+
font-weight: 600;
158+
}
159+
.cms-content a:hover {
160+
text-decoration: underline;
161+
}
162+
.cms-content img {
163+
border-radius: 0.5rem;
164+
max-width: 100%;
165+
}
166+
.cms-content li {
167+
list-style-type: disc;
168+
margin-left: 1.5rem;
169+
}
170+
```
171+
172+
This is readable, maintainable, and doesn't require learning special syntax. For many projects, this is the right answer.
173+
174+
### The Tailwind Approach
175+
176+
But if you're committed to keeping styles in your component, here's how it looks (in Elm, as usual):
131177

132178
```elm
133179
viewArticleContent : List (Html msg) -> Html msg
134-
viewArticleContent someThirdPartyContentWeDontControl =
180+
viewArticleContent content =
135181
Html.article
136182
[ Attr.class "p-4"
137-
, Attr.class "[&_div]:max-w-prose"
138-
, Attr.class "[&_a]:text-blue-600 [&_a]:font-bold [&_a:hover]:underline"
183+
, Attr.class "[&_a]:font-semibold [&_a:hover]:underline"
184+
, Attr.class "[&_img]:rounded-lg [&_img]:max-w-full"
185+
, Attr.class "[&_li]:list-disc [&_li]:ml-6"
139186
]
140-
someThirdPartyContentWeDontControl
187+
content
141188
```
142189

143-
Or the equivalent in React:
190+
Or in React:
144191

145192
```jsx
146193
const ArticleContent = ({ children }) => (
147194
<article
148195
className="
149-
p-4
150-
[&_div]:max-w-[65ch]
151-
[&_a]:text-blue-600 [&_a]:font-bold [&_a:hover]:underline
152-
"
196+
p-4
197+
[&_a]:font-semibold [&_a:hover]:underline
198+
[&_img]:rounded-lg [&_img]:max-w-full
199+
[&_li]:list-disc [&_li]:ml-6
200+
"
153201
>
154202
{children}
155203
</article>
156204
);
157205
```
158206

159-
All the styling lives in the wrapper component, applied to whatever HTML gets rendered inside. No separate stylesheet, no CSS modules, no fighting with specificity. When the design changes, we update the classes in one place.
207+
All the styling lives in the wrapper component. When the design changes, you update the classes in one place.
160208

161209
(Tailwind also has a `@tailwindcss/typography` plugin with a `prose` class that handles rich text styling, and if you're lucky that's enough in and by itself—but sometimes you need finer control, or you're matching an existing design system.)
162210

163211
## The Takeaway
164212

165213
Arbitrary variants with `[&...]` syntax let you write virtually any CSS selector within Tailwind's utility-class paradigm. The `&` represents the element your class is on, and everything after it is standard CSS selector syntax (with `_` for spaces).
166214

167-
It's not always the prettiest solution, but it keeps your styling colocated with your markup—which is the whole point of utility-first CSS.
215+
Is this the best approach? Probably not! A small vanilla CSS stylesheet for embedded content is often simpler, more readable, and easier for your team to maintain. Tailwind and traditional CSS can coexist just fine.
216+
217+
But if you want to (or have to) stay within Tailwind's utility-class model—or you're curious about what's possible—now you know how.

0 commit comments

Comments
 (0)