Skip to content

Commit af15e1b

Browse files
Add support for important in v4 (#14448)
This PR allows modifying utility output by wrapping all utility declarations in a custom selector or marking all utility declarations as `!important`. This is the v4 equivalent to the `important` option in the `tailwind.config.js` file. ## Mark all utility declarations as `!important` To add `!important` to all utility declarations, you add an `important` flag after the `@import` statement for `tailwindcss` or `tailwindcss/utilities`: ```css /** Importing `tailwindcss` */ @import "tailwindcss" important; /** Importing the utilities directly */ @import "tailwindcss/utilities" important; ``` Example utility output: ```css .mx-auto { margin-left: auto !important; margin-right: auto !important; } .text-center { text-align: center !important; } ``` This is equivalent to adding `important: true` to the `tailwind.config.js` file — which is still supported for backwards compatibility. ## Wrap all utility declarations in a custom selector To nest all utilities in an `#app` selector you add `selector(#app)` flag after the `@import` statement for `tailwindcss` or `tailwindcss/utilities`: ```css /** Importing `tailwindcss` */ @import "tailwindcss" selector(#app); /** Importing the utilities directly */ @import "tailwindcss/utilities" selector(#app); ``` Example utility output: ```css .mx-auto { #app & { margin-left: auto; margin-right: auto; } } .text-center { #app & { text-align: center; } } ``` This is equivalent to adding `important: "#app"` to the `tailwind.config.js` file — which is still supported for backwards compatibility. **This _does not_ bring back support for the `respectImportant` flag in `addUtilities` / `matchUtilities`.**
1 parent 35b84cc commit af15e1b

File tree

10 files changed

+310
-2
lines changed

10 files changed

+310
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Expose timing information in debug mode ([#14553](https://github.com/tailwindlabs/tailwindcss/pull/14553))
1414
- Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556))
1515
- Add `color-scheme` utilities ([#14567](https://github.com/tailwindlabs/tailwindcss/pull/14567))
16+
- Add support wrapping utilities in a selector ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448))
17+
- Add support marking all utilities as `!important` ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448))
1618
- _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514))
1719
- _Experimental_: Migrate `@apply` utilities with the template codemods ([#14574](https://github.com/tailwindlabs/tailwindcss/pull/14574))
1820
- _Experimental_: Add template codemods for migrating variant order ([#14524](https://github.com/tailwindlabs/tailwindcss/pull/14524]))

packages/tailwindcss/src/compat/apply-compat-hooks.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { toCss, walk, type AstNode } from '../ast'
1+
import { rule, toCss, walk, WalkAction, type AstNode } from '../ast'
22
import type { DesignSystem } from '../design-system'
33
import type { Theme, ThemeKey } from '../theme'
44
import { withAlpha } from '../utilities'
@@ -229,6 +229,29 @@ export async function applyCompatibilityHooks({
229229
designSystem.theme.prefix = resolvedConfig.prefix
230230
}
231231

232+
// If an important strategy has already been set in CSS don't override it
233+
if (!designSystem.important && resolvedConfig.important === true) {
234+
designSystem.important = true
235+
}
236+
237+
if (typeof resolvedConfig.important === 'string') {
238+
let wrappingSelector = resolvedConfig.important
239+
240+
walk(ast, (node, { replaceWith, parent }) => {
241+
if (node.kind !== 'rule') return
242+
if (node.selector !== '@tailwind utilities') return
243+
244+
// The AST node was already manually wrapped so there's nothing to do
245+
if (parent?.kind === 'rule' && parent.selector === wrappingSelector) {
246+
return WalkAction.Stop
247+
}
248+
249+
replaceWith(rule(wrappingSelector, [node]))
250+
251+
return WalkAction.Stop
252+
})
253+
}
254+
232255
for (let candidate of resolvedConfig.blocklist) {
233256
designSystem.invalidCandidates.add(candidate)
234257
}

packages/tailwindcss/src/compat/config.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,78 @@ test('a prefix must be letters only', async () => {
13721372
)
13731373
})
13741374

1375+
test('important: `#app`', async () => {
1376+
let input = css`
1377+
@tailwind utilities;
1378+
@config "./config.js";
1379+
1380+
@utility custom {
1381+
color: red;
1382+
}
1383+
`
1384+
1385+
let compiler = await compile(input, {
1386+
loadModule: async (_, base) => ({
1387+
base,
1388+
module: { important: '#app' },
1389+
}),
1390+
})
1391+
1392+
expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(`
1393+
"#app {
1394+
.custom {
1395+
color: red;
1396+
}
1397+
.underline {
1398+
text-decoration-line: underline;
1399+
}
1400+
.hover\\:line-through {
1401+
&:hover {
1402+
@media (hover: hover) {
1403+
text-decoration-line: line-through;
1404+
}
1405+
}
1406+
}
1407+
}
1408+
"
1409+
`)
1410+
})
1411+
1412+
test('important: true', async () => {
1413+
let input = css`
1414+
@tailwind utilities;
1415+
@config "./config.js";
1416+
1417+
@utility custom {
1418+
color: red;
1419+
}
1420+
`
1421+
1422+
let compiler = await compile(input, {
1423+
loadModule: async (_, base) => ({
1424+
base,
1425+
module: { important: true },
1426+
}),
1427+
})
1428+
1429+
expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(`
1430+
".custom {
1431+
color: red!important;
1432+
}
1433+
.underline {
1434+
text-decoration-line: underline!important;
1435+
}
1436+
.hover\\:line-through {
1437+
&:hover {
1438+
@media (hover: hover) {
1439+
text-decoration-line: line-through!important;
1440+
}
1441+
}
1442+
}
1443+
"
1444+
`)
1445+
})
1446+
13751447
test('blocklisted canddiates are not generated', async () => {
13761448
let compiler = await compile(
13771449
css`

packages/tailwindcss/src/compat/config/resolve-config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface ResolutionContext {
2929
let minimal: ResolvedConfig = {
3030
blocklist: [],
3131
prefix: '',
32+
important: false,
3233
darkMode: null,
3334
theme: {},
3435
plugins: [],
@@ -69,6 +70,10 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv
6970
if ('blocklist' in config && config.blocklist !== undefined) {
7071
ctx.result.blocklist = config.blocklist ?? []
7172
}
73+
74+
if ('important' in config && config.important !== undefined) {
75+
ctx.result.important = config.important ?? false
76+
}
7277
}
7378

7479
// Merge themes

packages/tailwindcss/src/compat/config/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,12 @@ export interface UserConfig {
8787
export interface ResolvedConfig {
8888
blocklist: string[]
8989
}
90+
91+
// `important` support
92+
export interface UserConfig {
93+
important?: boolean | string
94+
}
95+
96+
export interface ResolvedConfig {
97+
important: boolean | string
98+
}

packages/tailwindcss/src/compile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem
140140
for (let nodes of asts) {
141141
let propertySort = getPropertySort(nodes)
142142

143-
if (candidate.important) {
143+
if (candidate.important || designSystem.important) {
144144
applyImportant(nodes)
145145
}
146146

packages/tailwindcss/src/design-system.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export type DesignSystem = {
1515

1616
invalidCandidates: Set<string>
1717

18+
// Whether to mark utility declarations as !important
19+
important: boolean
20+
1821
getClassOrder(classes: string[]): [string, bigint | null][]
1922
getClassList(): ClassEntry[]
2023
getVariants(): VariantEntry[]
@@ -48,6 +51,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
4851
variants,
4952

5053
invalidCandidates: new Set(),
54+
important: false,
5155

5256
candidatesToCss(classes: string[]) {
5357
let result: (string | null)[] = []
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { expect, test } from 'vitest'
2+
import { compile } from '.'
3+
4+
const css = String.raw
5+
6+
test('Utilities can be wrapped in a selector', async () => {
7+
// This is the v4 equivalent of `important: "#app"` from v3
8+
let input = css`
9+
#app {
10+
@tailwind utilities;
11+
}
12+
`
13+
14+
let compiler = await compile(input)
15+
16+
expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
17+
"#app {
18+
.underline {
19+
text-decoration-line: underline;
20+
}
21+
.hover\\:line-through {
22+
&:hover {
23+
@media (hover: hover) {
24+
text-decoration-line: line-through;
25+
}
26+
}
27+
}
28+
}
29+
"
30+
`)
31+
})
32+
33+
test('Utilities can be marked with important', async () => {
34+
// This is the v4 equivalent of `important: true` from v3
35+
let input = css`
36+
@import 'tailwindcss/utilities' important;
37+
`
38+
39+
let compiler = await compile(input, {
40+
loadStylesheet: async (id: string, base: string) => ({
41+
base,
42+
content: '@tailwind utilities;',
43+
}),
44+
})
45+
46+
expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
47+
".underline {
48+
text-decoration-line: underline!important;
49+
}
50+
.hover\\:line-through {
51+
&:hover {
52+
@media (hover: hover) {
53+
text-decoration-line: line-through!important;
54+
}
55+
}
56+
}
57+
"
58+
`)
59+
})
60+
61+
test('Utilities can be wrapped with a selector and marked as important', async () => {
62+
// This does not have a direct equivalent in v3 but works as a consequence of
63+
// the new APIs
64+
let input = css`
65+
@media important {
66+
#app {
67+
@tailwind utilities;
68+
}
69+
}
70+
`
71+
72+
let compiler = await compile(input)
73+
74+
expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
75+
"#app {
76+
.underline {
77+
text-decoration-line: underline!important;
78+
}
79+
.hover\\:line-through {
80+
&:hover {
81+
@media (hover: hover) {
82+
text-decoration-line: line-through!important;
83+
}
84+
}
85+
}
86+
}
87+
"
88+
`)
89+
})

packages/tailwindcss/src/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ async function parseCss(
7676
await substituteAtImports(ast, base, loadStylesheet)
7777

7878
// Find all `@theme` declarations
79+
let important: boolean | null = null
7980
let theme = new Theme()
8081
let customVariants: ((designSystem: DesignSystem) => void)[] = []
8182
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
@@ -236,6 +237,35 @@ async function parseCss(
236237
return WalkAction.Skip
237238
}
238239

240+
if (node.selector.startsWith('@media')) {
241+
let features = segment(node.selector.slice(6), ' ')
242+
let shouldReplace = true
243+
244+
for (let i = 0; i < features.length; i++) {
245+
let part = features[i]
246+
247+
// Drop instances of `@media important`
248+
//
249+
// We support `@import "tailwindcss" important` to mark all declarations
250+
// in generated utilities as `!important`.
251+
if (part === 'important') {
252+
important = true
253+
shouldReplace = true
254+
features[i] = ''
255+
}
256+
}
257+
258+
let remaining = features.filter(Boolean).join(' ')
259+
260+
node.selector = `@media ${remaining}`
261+
262+
if (remaining.trim() === '' && shouldReplace) {
263+
replaceWith(node.nodes)
264+
}
265+
266+
return WalkAction.Skip
267+
}
268+
239269
if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return
240270

241271
let [themeOptions, themePrefix] = parseThemeOptions(node.selector)
@@ -288,6 +318,10 @@ async function parseCss(
288318

289319
let designSystem = buildDesignSystem(theme)
290320

321+
if (important) {
322+
designSystem.important = important
323+
}
324+
291325
// Apply hooks from backwards compatibility layer. This function takes a lot
292326
// of random arguments because it really just needs access to "the world" to
293327
// do whatever ungodly things it needs to do to make things backwards

0 commit comments

Comments
 (0)