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`.**
This commit is contained in:
Jordan Pittman 2024-10-03 10:25:11 -04:00 • committed by GitHub
parent 35b84cc313
commit af15e1bdc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 310 additions and 2 deletions

View file

@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Expose timing information in debug mode ([#14553](https://github.com/tailwindlabs/tailwindcss/pull/14553))
- Add support for `blocklist` in config files ([#14556](https://github.com/tailwindlabs/tailwindcss/pull/14556))
- Add `color-scheme` utilities ([#14567](https://github.com/tailwindlabs/tailwindcss/pull/14567))
- Add support wrapping utilities in a selector ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448))
- Add support marking all utilities as `!important` ([#14448](https://github.com/tailwindlabs/tailwindcss/pull/14448))
- _Experimental_: Migrate `@import "tailwindcss/tailwind.css"` to `@import "tailwindcss"` ([#14514](https://github.com/tailwindlabs/tailwindcss/pull/14514))
- _Experimental_: Migrate `@apply` utilities with the template codemods ([#14574](https://github.com/tailwindlabs/tailwindcss/pull/14574))
- _Experimental_: Add template codemods for migrating variant order ([#14524](https://github.com/tailwindlabs/tailwindcss/pull/14524]))

View file

@ -1,4 +1,4 @@
import { toCss, walk, type AstNode } from '../ast'
import { rule, toCss, walk, WalkAction, type AstNode } from '../ast'
import type { DesignSystem } from '../design-system'
import type { Theme, ThemeKey } from '../theme'
import { withAlpha } from '../utilities'
@ -229,6 +229,29 @@ export async function applyCompatibilityHooks({
designSystem.theme.prefix = resolvedConfig.prefix
}
// If an important strategy has already been set in CSS don't override it
if (!designSystem.important && resolvedConfig.important === true) {
designSystem.important = true
}
if (typeof resolvedConfig.important === 'string') {
let wrappingSelector = resolvedConfig.important
walk(ast, (node, { replaceWith, parent }) => {
if (node.kind !== 'rule') return
if (node.selector !== '@tailwind utilities') return
// The AST node was already manually wrapped so there's nothing to do
if (parent?.kind === 'rule' && parent.selector === wrappingSelector) {
return WalkAction.Stop
}
replaceWith(rule(wrappingSelector, [node]))
return WalkAction.Stop
})
}
for (let candidate of resolvedConfig.blocklist) {
designSystem.invalidCandidates.add(candidate)
}

View file

@ -1372,6 +1372,78 @@ test('a prefix must be letters only', async () => {
)
})
test('important: `#app`', async () => {
let input = css`
@tailwind utilities;
@config "./config.js";
@utility custom {
color: red;
}
`
let compiler = await compile(input, {
loadModule: async (_, base) => ({
base,
module: { important: '#app' },
}),
})
expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(`
"#app {
.custom {
color: red;
}
.underline {
text-decoration-line: underline;
}
.hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through;
}
}
}
}
"
`)
})
test('important: true', async () => {
let input = css`
@tailwind utilities;
@config "./config.js";
@utility custom {
color: red;
}
`
let compiler = await compile(input, {
loadModule: async (_, base) => ({
base,
module: { important: true },
}),
})
expect(compiler.build(['underline', 'hover:line-through', 'custom'])).toMatchInlineSnapshot(`
".custom {
color: red!important;
}
.underline {
text-decoration-line: underline!important;
}
.hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through!important;
}
}
}
"
`)
})
test('blocklisted canddiates are not generated', async () => {
let compiler = await compile(
css`

View file

@ -29,6 +29,7 @@ interface ResolutionContext {
let minimal: ResolvedConfig = {
blocklist: [],
prefix: '',
important: false,
darkMode: null,
theme: {},
plugins: [],
@ -69,6 +70,10 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv
if ('blocklist' in config && config.blocklist !== undefined) {
ctx.result.blocklist = config.blocklist ?? []
}
if ('important' in config && config.important !== undefined) {
ctx.result.important = config.important ?? false
}
}
// Merge themes

View file

@ -87,3 +87,12 @@ export interface UserConfig {
export interface ResolvedConfig {
blocklist: string[]
}
// `important` support
export interface UserConfig {
important?: boolean | string
}
export interface ResolvedConfig {
important: boolean | string
}

View file

@ -140,7 +140,7 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem
for (let nodes of asts) {
let propertySort = getPropertySort(nodes)
if (candidate.important) {
if (candidate.important || designSystem.important) {
applyImportant(nodes)
}

View file

@ -15,6 +15,9 @@ export type DesignSystem = {
invalidCandidates: Set<string>
// Whether to mark utility declarations as !important
important: boolean
getClassOrder(classes: string[]): [string, bigint | null][]
getClassList(): ClassEntry[]
getVariants(): VariantEntry[]
@ -48,6 +51,7 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
variants,
invalidCandidates: new Set(),
important: false,
candidatesToCss(classes: string[]) {
let result: (string | null)[] = []

View file

@ -0,0 +1,89 @@
import { expect, test } from 'vitest'
import { compile } from '.'
const css = String.raw
test('Utilities can be wrapped in a selector', async () => {
// This is the v4 equivalent of `important: "#app"` from v3
let input = css`
#app {
@tailwind utilities;
}
`
let compiler = await compile(input)
expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
"#app {
.underline {
text-decoration-line: underline;
}
.hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through;
}
}
}
}
"
`)
})
test('Utilities can be marked with important', async () => {
// This is the v4 equivalent of `important: true` from v3
let input = css`
@import 'tailwindcss/utilities' important;
`
let compiler = await compile(input, {
loadStylesheet: async (id: string, base: string) => ({
base,
content: '@tailwind utilities;',
}),
})
expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
".underline {
text-decoration-line: underline!important;
}
.hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through!important;
}
}
}
"
`)
})
test('Utilities can be wrapped with a selector and marked as important', async () => {
// This does not have a direct equivalent in v3 but works as a consequence of
// the new APIs
let input = css`
@media important {
#app {
@tailwind utilities;
}
}
`
let compiler = await compile(input)
expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
"#app {
.underline {
text-decoration-line: underline!important;
}
.hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through!important;
}
}
}
}
"
`)
})

View file

@ -76,6 +76,7 @@ async function parseCss(
await substituteAtImports(ast, base, loadStylesheet)
// Find all `@theme` declarations
let important: boolean | null = null
let theme = new Theme()
let customVariants: ((designSystem: DesignSystem) => void)[] = []
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
@ -236,6 +237,35 @@ async function parseCss(
return WalkAction.Skip
}
if (node.selector.startsWith('@media')) {
let features = segment(node.selector.slice(6), ' ')
let shouldReplace = true
for (let i = 0; i < features.length; i++) {
let part = features[i]
// Drop instances of `@media important`
//
// We support `@import "tailwindcss" important` to mark all declarations
// in generated utilities as `!important`.
if (part === 'important') {
important = true
shouldReplace = true
features[i] = ''
}
}
let remaining = features.filter(Boolean).join(' ')
node.selector = `@media ${remaining}`
if (remaining.trim() === '' && shouldReplace) {
replaceWith(node.nodes)
}
return WalkAction.Skip
}
if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return
let [themeOptions, themePrefix] = parseThemeOptions(node.selector)
@ -288,6 +318,10 @@ async function parseCss(
let designSystem = buildDesignSystem(theme)
if (important) {
designSystem.important = important
}
// Apply hooks from backwards compatibility layer. This function takes a lot
// of random arguments because it really just needs access to "the world" to
// do whatever ungodly things it needs to do to make things backwards

View file

@ -1,7 +1,10 @@
import { expect, test } from 'vitest'
import { __unstable__loadDesignSystem } from '.'
import { buildDesignSystem } from './design-system'
import { Theme } from './theme'
const css = String.raw
function loadDesignSystem() {
let theme = new Theme()
theme.add('--spacing-0_5', '0.125rem')
@ -104,3 +107,70 @@ test('Can produce CSS per candidate using `candidatesToCss`', () => {
]
`)
})
test('Utilities do not show wrapping selector in intellisense', async () => {
let input = css`
@import 'tailwindcss/utilities';
@config './config.js';
`
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
base,
content: '@tailwind utilities;',
}),
loadModule: async () => ({
base: '',
module: {
important: '#app',
},
}),
})
expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
[
".underline {
text-decoration-line: underline;
}
",
".hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through;
}
}
}
",
]
`)
})
test('Utilities, when marked as important, show as important in intellisense', async () => {
let input = css`
@import 'tailwindcss/utilities' important;
`
let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
base,
content: '@tailwind utilities;',
}),
})
expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
[
".underline {
text-decoration-line: underline!important;
}
",
".hover\\:line-through {
&:hover {
@media (hover: hover) {
text-decoration-line: line-through!important;
}
}
}
",
]
`)
})