Skip to content

Add functional utility syntax #15455

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jan 8, 2025
Merged

Conversation

RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented Dec 19, 2024

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:

@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-*):

@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:

@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:

@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:

@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:

@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:

@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:

@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:

@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:

/* 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]);
}

@RobinMalfait RobinMalfait marked this pull request as ready for review December 20, 2024 12:24
@RobinMalfait RobinMalfait requested a review from a team as a code owner December 20, 2024 12:25
@RobinMalfait RobinMalfait force-pushed the feat/functional-utility-syntax branch from bf7dc05 to 13e75f4 Compare December 20, 2024 14:23
@imjeehoo
Copy link

imjeehoo commented Dec 20, 2024

In Resolving against @theme values, is tab-3 valid by somehow inferring from tab-1 or is it typo of tab-4?

@thecrypticace
Copy link
Contributor

it was a typo — fixed.

@RobinMalfait RobinMalfait force-pushed the feat/functional-utility-syntax branch from c91ccb7 to 4eba187 Compare December 22, 2024 23:21
@RobinMalfait RobinMalfait force-pushed the feat/functional-utility-syntax branch 2 times, most recently from cfe0f82 to 99b89e4 Compare January 6, 2025 18:36
import { compoundsForSelectors } from './variants'
export type Config = UserConfig

const IS_VALID_PREFIX = /^[a-z]+$/
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
const IS_VALID_FUNCTIONAL_UTILITY_NAME = /^-?[a-z][a-zA-Z0-9/%._-]*-\*$/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oxide currently ignores capitals in any class name. Both of these regexes should probably account for that? Or we should relax that restriction in Oxide (but iirc it resulted in a pretty decent reduction in potential candidates).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, what do you think, just drop the A-Z part?

Complex part is if you have tab-* but you use tab-myTabSize, should we validate that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So after looking into this, what we disallow right now (in Oxide) is camelCasing if it's the first segment of the candidate. So fooBar would not work. foo-Bar would work, hover:fooBar would also work.

This is a bit confusing in general, but feels like outside of the scope of this PR to fix. However, @thecrypticace and I talked about this, and we believe the proper way to solve this, is to provide the roots of the static and functional utilities and the variants to Oxide such that we have a much smaller and more correct list to work with. There could of course still be false positives (e.g.: text-md that looks okay, but might not exist). In that case we could provide the full theme as well, but that seems overkill.

Will explore this after the v4 launch, probably.

Copy link
Contributor

@thecrypticace thecrypticace left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example, from the tests:

@utility example-* {
  --value-as-number: value(number);
  --value-as-percentage: value(percentage);
  --value-as-ratio: value(ratio);
}

produces this for example-2/3:

.example-2\\/3 {
  --value-as-number: 2;
  --value-as-ratio: 2 / 3;
}

which feels incorrect. I'm pretty sure it should be:

.example-2\\/3 {
  --value-as-ratio: 2 / 3;
}

Especially since this:

@utility example-* {
  --value-as-number: value(number);
}

prints no CSS at all for example-2/3.

@thecrypticace
Copy link
Contributor

Also, we're going to want to change value(…) -> --value(…) and modifier(…) -> --modifier(…)

Copy link
Member

@philipp-spiess philipp-spiess left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome stuff! @thecrypticace brought up some good points

@RobinMalfait
Copy link
Member Author

This example, from the tests:

@utility example-* {
  --value-as-number: value(number);
  --value-as-percentage: value(percentage);
  --value-as-ratio: value(ratio);
}

produces this for example-2/3:

.example-2\\/3 {
  --value-as-number: 2;
  --value-as-ratio: 2 / 3;
}

which feels incorrect. I'm pretty sure it should be:

.example-2\\/3 {
  --value-as-ratio: 2 / 3;
}

Especially since this:

@utility example-* {
  --value-as-number: value(number);
}

prints no CSS at all for example-2/3.

Yeah that's true and confusing. The idea is that if you have example-2/3 that you either have to use --value(number) and --modifier(number) to get the 2 and 3 out. OR you have to use --value(ratio) and then the others shouldn't resolve because that's a conflict. I think we can refine that.

- If you are using a bare value or modifier that is a number, then we
  make sure that it is a valid multiplier of `0.25`
- If you are using a bare value or modifier that is a percentage, then
  we make sure that it is a valid positive integer.
- If you are using a fraction, then we make sure that both the numerator
  and denominator are positive integers.
- If the bare value resolves to a non-ratio value, and if a modifier is
  used, then we need to make sure that the modifier resolves as well.
  E.g.: `example-1/2.3` this won't resolve to a `ratio` because the
  denominator is invalid. This will resolve to an `integer` or `number`
  for the value of `1`, but then we need to make sure that `2.3` is a
  valid modifier.
@RobinMalfait RobinMalfait force-pushed the feat/functional-utility-syntax branch from 99b89e4 to 4d2c36b Compare January 7, 2025 21:36
@RobinMalfait RobinMalfait force-pushed the feat/functional-utility-syntax branch from f58c659 to 392dce9 Compare January 7, 2025 21:56
@RobinMalfait RobinMalfait force-pushed the feat/functional-utility-syntax branch from 1aadda6 to 04705ab Compare January 8, 2025 13:40
Because of `@utility -foo-*`, this means that we can (and should) drop
the `supportsNegative` from the suggestions API, otherwise it would
suggestion `--foo-123`.
Copy link
Member

@philipp-spiess philipp-spiess left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good already! Left a question inline about the ratio branches, cause I wonder if we can simplify this a bit if we can avoid resolving --value(integer) at all when a ratio is used. Not sure if we can, though

Comment on lines +4606 to +4608
// - If a candidate looks like `foo-2/3`, then the `--value(ratio)` should
// be used OR the `--value(…)` and `--modifier(…)` must be used. But not
// both.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any valid use case for resolving a ratio like foo-2/3 as --value(…) and --modifier(…)? If not I wonder if this might simplify things a bit since we can just determine in advance wether the input looks like a ratio and never resolve the --value(number) in this case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly there is, if you define your font-sizes as numbers instead of tshirt sizes. E.g.: text-4/7 could be text-4/7 (font-size of 4/7 whatever that means) or text-4 with line-height of 7.

@RobinMalfait RobinMalfait merged commit ee3add9 into next Jan 8, 2025
5 checks passed
@RobinMalfait RobinMalfait deleted the feat/functional-utility-syntax branch January 8, 2025 15:04
@stafyniaksacha
Copy link

Could we pipe this to @apply directive ?

Using new variables:

@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-*);
}

@utility foo {
  @apply tab-1
}

Using existing variables:

@theme {
  // ...
  --color-amber-500: #fff;
  // ...
}

@utility card-* {
  /* card-sky-100 */
  background-color: --value(--color-*);
}

@utility bar {
  @apply card-sky-100
}

@mscofield0
Copy link

Could we pipe this to @apply directive ?

Using new variables:

@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-*);
}

@utility foo {
  @apply tab-1
}

Using existing variables:

@theme {
  // ...
  --color-amber-500: #fff;
  // ...
}

@utility card-* {
  /* card-sky-100 */
  background-color: --value(--color-*);
}

@utility bar {
  @apply card-sky-100
}

I would also like an answer to this, I would assume any tailwind utility is able to be used in @apply.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants