You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
10
10
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.
12
14
13
15
## The Problem
14
16
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:
16
18
17
19
```html
18
-
<divclass="content">
19
-
<p>First paragraph</p>
20
-
<p>Second paragraph</p>
20
+
<divclass="cms-content">
21
+
<p>Some text with a <ahref="#">link</a> in it.</p>
22
+
<ul>
23
+
<li>List item one</li>
24
+
<li>List item two</li>
25
+
</ul>
21
26
</div>
22
27
```
23
28
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:
25
30
26
31
```css
27
-
.content>p {
28
-
color: blue;
29
-
margin-bottom: 1rem;
32
+
.cms-contenta {
33
+
font-weight: 600;
34
+
text-decoration: none;
35
+
}
36
+
.cms-contenta:hover {
37
+
text-decoration: underline;
38
+
}
39
+
.cms-contentli {
40
+
list-style-type: disc;
41
+
margin-left: 1.5rem;
30
42
}
31
43
```
32
44
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.
34
46
35
-
## Arbitrary Variants to the Rescue
47
+
## Arbitrary Variants: The Tailwind Way
36
48
37
49
Tailwind's arbitrary variants let you write any CSS selector directly in your class names using square bracket notation:
<p>Some text with a <ahref="#">link</a> in it.</p>
56
+
<ul>
57
+
<li>List item one</li>
58
+
<li>List item two</li>
59
+
</ul>
43
60
</div>
44
61
```
45
62
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.
47
64
48
65
## Breaking Down the Syntax
49
66
@@ -52,19 +69,18 @@ The magic is in understanding what `&` means. In Tailwind's arbitrary variants,
52
69
So when you write:
53
70
54
71
```html
55
-
<divclass="[&>p]:text-blue-500"></div>
72
+
<divclass="[&_a]:font-semibold"></div>
56
73
```
57
74
58
75
Tailwind generates CSS that looks like this:
59
76
60
77
```css
61
-
.\[\&\>p\]\:text-blue-500>p {
62
-
--tw-text-opacity: 1;
63
-
color: rgb(59130246 / var(--tw-text-opacity));
78
+
.\[\&_a\]\:font-semibolda {
79
+
font-weight: 600;
64
80
}
65
81
```
66
82
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.
68
84
69
85
## Common Patterns
70
86
@@ -73,95 +89,129 @@ Here are some useful child-targeting patterns:
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:
116
137
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
121
142
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.
123
144
124
145
## A Practical Example: CMS Content
125
146
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.)
127
150
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)
129
152
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-contenta {
157
+
font-weight: 600;
158
+
}
159
+
.cms-contenta:hover {
160
+
text-decoration: underline;
161
+
}
162
+
.cms-contentimg {
163
+
border-radius: 0.5rem;
164
+
max-width: 100%;
165
+
}
166
+
.cms-contentli {
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):
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.
160
208
161
209
(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.)
162
210
163
211
## The Takeaway
164
212
165
213
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).
166
214
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