Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add support for prefixes ([#14501](https://github.com/tailwindlabs/tailwindcss/pull/14501))
- 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))

### Fixed

Expand Down
8 changes: 8 additions & 0 deletions packages/tailwindcss/src/compat/apply-compat-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ 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) {
designSystem.important =
typeof resolvedConfig.important === 'string'
? `${resolvedConfig.important} &`
: resolvedConfig.important
}

// Replace `resolveThemeValue` with a version that is backwards compatible
// with dot-notation but also aware of any JS theme configurations registered
// by plugins or JS config files. This is significantly slower than just
Expand Down
76 changes: 76 additions & 0 deletions packages/tailwindcss/src/compat/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1371,3 +1371,79 @@ test('a prefix must be letters only', async () => {
`[Error: The prefix "__" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.]`,
)
})

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(`
".custom {
#app & {
color: red;
}
}
.underline {
#app & {
text-decoration-line: underline;
}
}
.hover\\:line-through {
#app & {
&: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;
}
}
}
"
`)
})
5 changes: 5 additions & 0 deletions packages/tailwindcss/src/compat/config/resolve-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface ResolutionContext {

let minimal: ResolvedConfig = {
prefix: '',
important: false,
darkMode: null,
theme: {},
plugins: [],
Expand Down Expand Up @@ -64,6 +65,10 @@ export function resolveConfig(design: DesignSystem, files: ConfigFile[]): Resolv
if ('prefix' in config && config.prefix !== undefined) {
ctx.result.prefix = config.prefix ?? ''
}

if ('important' in config && config.important !== undefined) {
ctx.result.important = config.important ?? false
}
}

// Merge themes
Expand Down
9 changes: 9 additions & 0 deletions packages/tailwindcss/src/compat/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,12 @@ export interface UserConfig {
export interface ResolvedConfig {
prefix: string
}

// `important` support
export interface UserConfig {
important?: boolean | string
}

export interface ResolvedConfig {
important: boolean | string
}
6 changes: 5 additions & 1 deletion packages/tailwindcss/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,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 === true) {
applyImportant(nodes)
}

Expand All @@ -154,6 +154,10 @@ export function compileAstNodes(candidate: Candidate, designSystem: DesignSystem
if (result === null) return []
}

if (typeof designSystem.important === 'string') {
node.nodes = [rule(designSystem.important, node.nodes)]
}

rules.push({
node,
propertySort,
Expand Down
8 changes: 8 additions & 0 deletions packages/tailwindcss/src/design-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type DesignSystem = {
utilities: Utilities
variants: Variants

important: string | boolean

getClassOrder(classes: string[]): [string, bigint | null][]
getClassList(): ClassEntry[]
getVariants(): VariantEntry[]
Expand Down Expand Up @@ -45,6 +47,12 @@ export function buildDesignSystem(theme: Theme): DesignSystem {
utilities,
variants,

// How to mark important utilities
// - wrap with a selector (any string)
// - add an !important (true)
// - do nothing (false)
important: false,

candidatesToCss(classes: string[]) {
let result: (string | null)[] = []

Expand Down
64 changes: 64 additions & 0 deletions packages/tailwindcss/src/important.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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`
@import 'tailwindcss/utilities' selector(#app);
`

let compiler = await compile(input, {
loadStylesheet: async (id: string, base: string) => ({
base,
content: '@tailwind utilities;',
}),
})

expect(compiler.build(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
".underline {
#app & {
text-decoration-line: underline;
}
}
.hover\\:line-through {
#app & {
&: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;
}
}
}
"
`)
})
31 changes: 31 additions & 0 deletions packages/tailwindcss/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ async function parseCss(
await substituteAtImports(ast, base, loadStylesheet)

// Find all `@theme` declarations
let important: string | true | null = null
let theme = new Theme()
let customVariants: ((designSystem: DesignSystem) => void)[] = []
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
Expand Down Expand Up @@ -232,6 +233,32 @@ async function parseCss(
return WalkAction.Skip
}

// Drop instances of `@media selector(…)`
//
// We support `@import "tailwindcss" selector(…)` as a way to
// nest utilities under a custom selector.
if (node.selector.startsWith('@media selector(')) {
let themeParams = node.selector.slice(16, -1)

important = `${themeParams} &`

replaceWith(node.nodes)

return WalkAction.Skip
}

// Drop instances of `@media important`
//
// We support `@import "tailwindcss" important` to mark all declarations
// in generated utilities as `!important`.
if (node.selector.startsWith('@media important')) {
important = true

replaceWith(node.nodes)

return WalkAction.Skip
}

if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return

let [themeOptions, themePrefix] = parseThemeOptions(node.selector)
Expand Down Expand Up @@ -284,6 +311,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
Expand Down
67 changes: 67 additions & 0 deletions packages/tailwindcss/src/intellisense.test.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -83,3 +86,67 @@ test('The variant `has-force` does not crash', () => {

expect(has.selectors({ value: 'force' })).toMatchInlineSnapshot(`[]`)
})

test('Utilities show when nested in a selector in intellisense', async () => {
let input = css`
@import 'tailwindcss/utilities' selector(#app);
`

let design = await __unstable__loadDesignSystem(input, {
loadStylesheet: async (_, base) => ({
base,
content: '@tailwind utilities;',
}),
})

expect(design.candidatesToCss(['underline', 'hover:line-through'])).toMatchInlineSnapshot(`
[
".underline {
#app & {
text-decoration-line: underline;
}
}
",
".hover\\:line-through {
#app & {
&: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;
}
}
}
",
]
`)
})