Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Experimental_: Add `wrap-anywhere`, `wrap-break-word`, and `wrap-normal` utilities ([#12128](https://github.com/tailwindlabs/tailwindcss/pull/12128))
- _Experimental_: Add `@source inline(…)` ([#17147](https://github.com/tailwindlabs/tailwindcss/pull/17147))
- Add support for literal values in `--value('…')` and `--modifier('…')` ([#17304](https://github.com/tailwindlabs/tailwindcss/pull/17304))
- Add suggestions when `--spacing(--value(integer, number))` is used ([#17308](https://github.com/tailwindlabs/tailwindcss/pull/17308))

### [4.0.15] - 2025-03-20

Expand Down
33 changes: 33 additions & 0 deletions packages/tailwindcss/src/intellisense.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,9 @@ test('Custom functional @utility', async () => {

--leading-foo: 1.5;
--leading-bar: 2;

--spacing: 0.25rem;
--spacing-custom: 123px;
}

@utility tab-* {
Expand All @@ -488,6 +491,18 @@ test('Custom functional @utility', async () => {
line-height: --modifier(--leading, 'normal');
}

@utility with-custom-spacing-* {
size: --value(--spacing);
}

@utility with-integer-spacing-* {
size: --spacing(--value(integer));
}

@utility with-number-spacing-* {
size: --spacing(--value(number));
}

@utility -negative-* {
margin: --value(--tab-size- *);
}
Expand Down Expand Up @@ -515,6 +530,24 @@ test('Custom functional @utility', async () => {
expect(classNames).not.toContain('-tab-4')
expect(classNames).not.toContain('-tab-github')

expect(classNames).toContain('with-custom-spacing-custom')
expect(classNames).not.toContain('with-custom-spacing-0')
expect(classNames).not.toContain('with-custom-spacing-0.5')
expect(classNames).not.toContain('with-custom-spacing-1')
expect(classNames).not.toContain('with-custom-spacing-1.5')

expect(classNames).not.toContain('with-integer-spacing-custom')
expect(classNames).toContain('with-integer-spacing-0')
expect(classNames).not.toContain('with-integer-spacing-0.5')
expect(classNames).toContain('with-integer-spacing-1')
expect(classNames).not.toContain('with-integer-spacing-1.5')

expect(classNames).not.toContain('with-number-spacing-custom')
expect(classNames).toContain('with-number-spacing-0')
expect(classNames).toContain('with-number-spacing-0.5')
expect(classNames).toContain('with-number-spacing-1')
expect(classNames).toContain('with-number-spacing-1.5')

expect(classNames).toContain('-negative-1')
expect(classNames).toContain('-negative-2')
expect(classNames).toContain('-negative-4')
Expand Down
183 changes: 115 additions & 68 deletions packages/tailwindcss/src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,43 @@ import * as ValueParser from './value-parser'
const IS_VALID_STATIC_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*$/
const IS_VALID_FUNCTIONAL_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*-\*$/

const DEFAULT_SPACING_SUGGESTIONS = [
'0',
'0.5',
'1',
'1.5',
'2',
'2.5',
'3',
'3.5',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
'12',
'14',
'16',
'20',
'24',
'28',
'32',
'36',
'40',
'44',
'48',
'52',
'56',
'60',
'64',
'72',
'80',
'96',
]

type CompileFn<T extends Candidate['kind']> = (
value: Extract<Candidate, { kind: T }>,
) => AstNode[] | undefined | null
Expand Down Expand Up @@ -476,44 +513,7 @@ export function createUtilities(theme: Theme) {

suggest(name, () => [
{
values: theme.get(['--spacing'])
? [
'0',
'0.5',
'1',
'1.5',
'2',
'2.5',
'3',
'3.5',
'4',
'5',
'6',
'7',
'8',
'9',
'10',
'11',
'12',
'14',
'16',
'20',
'24',
'28',
'32',
'36',
'40',
'44',
'48',
'52',
'56',
'60',
'64',
'72',
'80',
'96',
]
: [],
values: theme.get(['--spacing']) ? DEFAULT_SPACING_SUGGESTIONS : [],
supportsNegative,
supportsFractions,
valueThemeKeys: themeKeys,
Expand Down Expand Up @@ -4731,11 +4731,20 @@ export function createCssUtility(node: AtRule) {
// If you then use `foo-1/2`, this is invalid, because the modifier is not used.

return (designSystem: DesignSystem) => {
let valueThemeKeys = new Set<`--${string}`>()
let valueLiterals = new Set<string>()

let modifierThemeKeys = new Set<`--${string}`>()
let modifierLiterals = new Set<string>()
let storage = {
'--value': {
usedSpacingInteger: false,
usedSpacingNumber: false,
themeKeys: new Set<`--${string}`>(),
literals: new Set<string>(),
},
'--modifier': {
usedSpacingInteger: false,
usedSpacingNumber: false,
themeKeys: new Set<`--${string}`>(),
literals: new Set<string>(),
},
}

// Pre-process the AST to make it easier to work with.
//
Expand All @@ -4762,6 +4771,41 @@ export function createCssUtility(node: AtRule) {
// `\\*` or not inserting whitespace) then most of these can go away.
ValueParser.walk(declarationValueAst, (fn) => {
if (fn.kind !== 'function') return

// Track usage of `--spacing(…)`
if (
fn.value === '--spacing' &&
// Quick bail check if we already know that `--value` and `--modifier` are
// using the full `--spacing` theme scale.
!(storage['--modifier'].usedSpacingNumber && storage['--value'].usedSpacingNumber)
) {
ValueParser.walk(fn.nodes, (node) => {
if (node.kind !== 'function') return
if (node.value !== '--value' && node.value !== '--modifier') return
const key = node.value

for (let arg of node.nodes) {
if (arg.kind !== 'word') continue

if (arg.value === 'integer') {
storage[key].usedSpacingInteger ||= true
} else if (arg.value === 'number') {
storage[key].usedSpacingNumber ||= true

// Once both `--value` and `--modifier` are using the full
// `number` spacing scale, then there's no need to continue
if (
storage['--modifier'].usedSpacingNumber &&
storage['--value'].usedSpacingNumber
) {
return ValueParser.ValueWalkAction.Stop
}
}
}
})
return ValueParser.ValueWalkAction.Continue
}

if (fn.value !== '--value' && fn.value !== '--modifier') return

let args = segment(ValueParser.toCss(fn.nodes), ',')
Expand Down Expand Up @@ -4796,23 +4840,13 @@ export function createCssUtility(node: AtRule) {
node.value[0] === node.value[node.value.length - 1]
) {
let value = node.value.slice(1, -1)

if (fn.value === '--value') {
valueLiterals.add(value)
} else if (fn.value === '--modifier') {
modifierLiterals.add(value)
}
storage[fn.value].literals.add(value)
}

// Track theme keys
else if (node.kind === 'word' && node.value[0] === '-' && node.value[1] === '-') {
let value = node.value.replace(/-\*.*$/g, '') as `--${string}`

if (fn.value === '--value') {
valueThemeKeys.add(value)
} else if (fn.value === '--modifier') {
modifierThemeKeys.add(value)
}
storage[fn.value].themeKeys.add(value)
}
}
})
Expand Down Expand Up @@ -4949,20 +4983,33 @@ export function createCssUtility(node: AtRule) {
})

designSystem.utilities.suggest(name.slice(0, -2), () => {
let values = []
for (let value of valueLiterals) {
values.push(value)
}
for (let value of designSystem.theme.keysInNamespaces(valueThemeKeys)) {
values.push(value)
}
let values: string[] = []
let modifiers: string[] = []

for (let [target, { literals, usedSpacingNumber, usedSpacingInteger, themeKeys }] of [
[values, storage['--value']],
[modifiers, storage['--modifier']],
] as const) {
// Suggest literal values. E.g.: `--value('literal')`
for (let value of literals) {
target.push(value)
}

let modifiers = []
for (let modifier of modifierLiterals) {
modifiers.push(modifier)
}
for (let value of designSystem.theme.keysInNamespaces(modifierThemeKeys)) {
modifiers.push(value)
// Suggest `--spacing(…)` values. E.g.: `--spacing(--value(integer))`
if (usedSpacingNumber) {
target.push(...DEFAULT_SPACING_SUGGESTIONS)
} else if (usedSpacingInteger) {
for (let value of DEFAULT_SPACING_SUGGESTIONS) {
if (isPositiveInteger(value)) {
target.push(value)
}
}
}

// Suggest theme values. E.g.: `--value(--color-*)`
for (let value of designSystem.theme.keysInNamespaces(themeKeys)) {
target.push(value)
}
}

return [{ values, modifiers }] satisfies SuggestionGroup[]
Expand Down