Skip to content

Commit ee3add9

Browse files
authored
Add functional utility syntax (#15455)
This PR adds support for functional utilities constructed via CSS. # Registering functional utilities in CSS To register a functional utility in CSS, use the `@utility potato-*` syntax, where the `-*` signals that this is a functional utility: ```css @Utility tab-* { tab-size: --value(--tab-size-*); } ``` ## Resolving values The special `--value(…)` function is used to resolve the utility value. ### Resolving against `@theme` values To resolve the value against a set of theme keys, use `--value(--theme-key-*)`: ```css @theme { --tab-size-1: 1; --tab-size-2: 2; --tab-size-4: 4; --tab-size-github: 8; } @Utility tab-* { /* tab-1, tab-2, tab-4, tab-github */ tab-size: --value(--tab-size-*); } ``` ### Bare values To resolve the value as a bare value, use `--value({type})`, where `{type}` is the data type you want to validate the bare value as: ```css @Utility tab-* { /* tab-1, tab-76, tab-971 */ tab-size: --value(integer); } ``` ### Arbitrary values To support arbitrary values, use `--value([{type}])` (notice the square brackets) to tell Tailwind which types are supported as an arbitrary value: ```css @Utility tab-* { /* tab-[1], tab-[76], tab-[971] */ tab-size: --value([integer]); } ``` ### Supporting theme values, bare values, and arbitrary values together All three forms of the `--value(…)` function can be used within a rule as multiple declarations, and any declarations that fail to resolve will be omitted in the output: ```css @theme { --tab-size-github: 8; } @Utility tab-* { tab-size: --value([integer]); tab-size: --value(integer); tab-size: --value(--tab-size-*); } ``` This makes it possible to treat the value differently in each case if necessary, for example translating a bare integer to a percentage: ```css @Utility opacity-* { opacity: --value([percentage]); opacity: calc(--value(integer) * 1%); opacity: --value(--opacity-*); } ``` The `--value(…)` function can also take multiple arguments and resolve them left to right if you don't need to treat the return value differently in different cases: ```css @theme { --tab-size-github: 8; } @Utility tab-* { tab-size: --value(--tab-size-*, integer, [integer]); } @Utility opacity-* { opacity: calc(--value(integer) * 1%); opacity: --value(--opacity-*, [percentage]); } ``` ### Negative values To support negative values, register separate positive and negative utilities into separate declarations: ```css @Utility inset-* { inset: calc(--var(--spacing) * --value([percentage], [length])); } @Utility -inset-* { inset: calc(--var(--spacing) * --value([percentage], [length]) * -1); } ``` ## Modifiers Modifiers are handled using the `--modifier(…)` function which works exactly like the `--value(…)` function but operates on a modifier if present: ```css @Utility text-* { font-size: --value(--font-size-*, [length]); line-height: --modifier(--line-height-*, [length], [*]); } ``` If a modifier isn't present, any declaration depending on a modifier is just not included in the output. ## Fractions To handle fractions, we rely on the CSS `ratio` data type. If this is used with `--value(…)`, it's a signal to Tailwind to treat the value + modifier as a single value: ```css /* The CSS `ratio` type is our signal to treat the value + modifier as a fraction */ @Utility aspect-* { /* aspect-square, aspect-3/4, aspect-[7/9] */ aspect-ratio: --value(--aspect-ratio-*, ratio, [ratio]); } ```
1 parent d6c4e72 commit ee3add9

File tree

7 files changed

+1211
-15
lines changed

7 files changed

+1211
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Add `@tailwindcss/browser` package to run Tailwind CSS in the browser ([#15558](https://github.com/tailwindlabs/tailwindcss/pull/15558))
1313
- Add `@reference "…"` API as a replacement for the previous `@import "…" reference` option ([#15565](https://github.com/tailwindlabs/tailwindcss/pull/15565))
14+
- Add functional utility syntax ([#15455](https://github.com/tailwindlabs/tailwindcss/pull/15455))
1415

1516
### Fixed
1617

packages/tailwindcss/src/index.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ import { substituteFunctions } from './css-functions'
2626
import * as CSS from './css-parser'
2727
import { buildDesignSystem, type DesignSystem } from './design-system'
2828
import { Theme, ThemeOptions } from './theme'
29+
import { createCssUtility } from './utilities'
2930
import { segment } from './utils/segment'
3031
import { compoundsForSelectors } from './variants'
3132
export type Config = UserConfig
3233

3334
const IS_VALID_PREFIX = /^[a-z]+$/
34-
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
3535

3636
type CompileOptions = {
3737
base?: string
@@ -174,25 +174,20 @@ async function parseCss(
174174
throw new Error('`@utility` cannot be nested.')
175175
}
176176

177-
let name = node.params
178-
179-
if (!IS_VALID_UTILITY_NAME.test(name)) {
177+
if (node.nodes.length === 0) {
180178
throw new Error(
181-
`\`@utility ${name}\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter.`,
179+
`\`@utility ${node.params}\` is empty. Utilities should include at least one property.`,
182180
)
183181
}
184182

185-
if (node.nodes.length === 0) {
183+
let utility = createCssUtility(node)
184+
if (utility === null) {
186185
throw new Error(
187-
`\`@utility ${name}\` is empty. Utilities should include at least one property.`,
186+
`\`@utility ${node.params}\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter.`,
188187
)
189188
}
190189

191-
customUtilities.push((designSystem) => {
192-
designSystem.utilities.static(name, () => structuredClone(node.nodes))
193-
})
194-
195-
return
190+
customUtilities.push(utility)
196191
}
197192

198193
// Collect paths from `@source` at-rules

packages/tailwindcss/src/intellisense.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,69 @@ test('Custom at-rule variants do not show up as a value under `group`', async ()
437437
expect(not.values).toContain('variant-3')
438438
expect(not.values).toContain('variant-4')
439439
})
440+
441+
test('Custom functional @utility', async () => {
442+
let input = css`
443+
@import 'tailwindcss/utilities';
444+
445+
@theme reference {
446+
--tab-size-1: 1;
447+
--tab-size-2: 2;
448+
--tab-size-4: 4;
449+
--tab-size-github: 8;
450+
451+
--text-xs: 0.75rem;
452+
--text-xs--line-height: calc(1 / 0.75);
453+
454+
--leading-foo: 1.5;
455+
--leading-bar: 2;
456+
}
457+
458+
@utility tab-* {
459+
tab-size: --value(--tab-size);
460+
}
461+
462+
@utility example-* {
463+
font-size: --value(--text);
464+
line-height: --value(--text- * --line-height);
465+
line-height: --modifier(--leading);
466+
}
467+
468+
@utility -negative-* {
469+
margin: --value(--tab-size- *);
470+
}
471+
`
472+
473+
let design = await __unstable__loadDesignSystem(input, {
474+
loadStylesheet: async (_, base) => ({
475+
base,
476+
content: '@tailwind utilities;',
477+
}),
478+
})
479+
480+
let classMap = new Map(design.getClassList())
481+
let classNames = Array.from(classMap.keys())
482+
483+
expect(classNames).toContain('tab-1')
484+
expect(classNames).toContain('tab-2')
485+
expect(classNames).toContain('tab-4')
486+
expect(classNames).toContain('tab-github')
487+
488+
expect(classNames).not.toContain('-tab-1')
489+
expect(classNames).not.toContain('-tab-2')
490+
expect(classNames).not.toContain('-tab-4')
491+
expect(classNames).not.toContain('-tab-github')
492+
493+
expect(classNames).toContain('-negative-1')
494+
expect(classNames).toContain('-negative-2')
495+
expect(classNames).toContain('-negative-4')
496+
expect(classNames).toContain('-negative-github')
497+
498+
expect(classNames).not.toContain('--negative-1')
499+
expect(classNames).not.toContain('--negative-2')
500+
expect(classNames).not.toContain('--negative-4')
501+
expect(classNames).not.toContain('--negative-github')
502+
503+
expect(classNames).toContain('example-xs')
504+
expect(classMap.get('example-xs')?.modifiers).toEqual(['foo', 'bar'])
505+
})

packages/tailwindcss/src/theme.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class Theme {
6868
}
6969
}
7070

71-
keysInNamespaces(themeKeys: ThemeKey[]): string[] {
71+
keysInNamespaces(themeKeys: Iterable<ThemeKey>): string[] {
7272
let keys: string[] = []
7373

7474
for (let namespace of themeKeys) {

0 commit comments

Comments
 (0)