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:
parent
35b84cc313
commit
af15e1bdc0
10 changed files with 310 additions and 2 deletions
|
|
@ -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]))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)[] = []
|
||||
|
|
|
|||
89
packages/tailwindcss/src/important.test.ts
Normal file
89
packages/tailwindcss/src/important.test.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
",
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue