Skip to content
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

Improve matchUtilities API and make it work with the AOT engine #4232

merged 16 commits into from May 5, 2021


Copy link

@RobinMalfait RobinMalfait commented May 3, 2021

Written by @adamwathan

This PR improves the matchUtilities plugin API used by the JIT engine to make it both simpler and more declarative, and also make it compatible with the AOT engine. These improvements are still considered to be in preview, and may change without adhering to semantic versioning until the JIT engine is totally stable.

The main goal of this is to make it so users don't have to conditionally use different APIs depending on the mode. For example, this is what you might have had to write prior to these improvements:

function backdropContrast({ config, matchUtilities, addUtilities, theme, variants }) {
  if (config('mode') === 'jit') {
      'backdrop-contrast': (modifier, { theme }) => {
        let value = asValue(modifier, theme.backdropContrast)

        if (value === undefined) {
          return []

        return {
          [nameClass('backdrop-contrast', modifier)]: {
            '--tw-backdrop-contrast': `contrast(${value})`,
  } else {
    const utilities = _.fromPairs('backdropContrast'), (value, modifier) => {
        return [
          nameClass('backdrop-contrast', modifier),
            '--tw-backdrop-contrast': Array.isArray(value)
              ? => `contrast(${v})`).join(' ')
              : `contrast(${value})`,

    addUtilities(utilities, variants('backdropContrast'))

With these improvements, you can just use matchUtilities and it'll still work in AOT mode. To make this possible, we needed to rework the actual API of matchUtilities, but the changes are actually a huge improvement for both engines and make the code a lot more declarative and terse. Here's what the API looks like now:

function backdropContrast({ matchUtilities, theme, variants }) {
      'backdrop-contrast': (value) => {
        return { '--tw-backdrop-contrast': `contrast(${value})` }
      values: theme('backdropContrast'),
      variants: variants('backdropContrast'),
      type: 'any',

You no longer have to specify the actual class name which is really nice — that is automatic now since you aren't really allowed to change it anyways, you have to return a rule that matches the detected class name in the HTML or of course nothing is going to work, so before this change all that really was was an opportunity for someone to make a mistake.

So now you just need to return the declarations, which is all you need for 99% of cases. You also pass in the values (which is used by AOT mode to generate all the right classes, and by JIT mode to know which classes are valid matches for the lookup and which should be discarded), and variants (only used by AOT mode). The type option is used to coerce arbitrary values (like backdrop-contrast-[0.5]) and sort of specify what sort of input is valid.

Valid options are any, color, length, lookup, angle, and list. Might refine these as we discover more possible types, especially thinking list needs a bit of thought.

The only issue with this API is if you ever do need to generate more than just a basic utility class. There are two main situations that need to be dealt with:

  1. You need to add a pseudo-element like ::placeholder. Conveniently Tailwind supports nesting in plugins, so you just handle this with the & operator:
function placeholderColor({ matchUtilities, theme, variants }) {
      placeholder: (value) => {
        return {
          "&::placeholder": {
            color: value,
      values: flattenColorPalette(theme("placeholderColor")),
      variants: variants("placeholderColor"),
      type: "any",
  1. You need to add some "supporting" rule. Internally this is only needed for animation, where we need to add a matching keyframes rule alongside the actual utility. To handle this we added an includeRules helper that lets you add arbitrary raw CSS rules whenever a match is detected. Looks like this:
function animation({ matchUtilities, theme, variants }) {
  let keyframes = buildKeyframes(theme('keyframes'))

      animate: (value, { includeRules }) => {
        let { name: animationName } = parseAnimationValue(value)

        includeRules(keyframes[animationName], { respectImportant: false })

        return { animation: value }
    { values: theme('animation'), variants: variants('animation') }

Overall really happy with this API, drastically simplifies a lot of stuff without sacrificing any power. We had to make some changes to the actual generated CSS order in Tailwind but nothing that affects the actual behavior, so should be fine to include this in 2.2 once we find a couple other quick features to include.

Copy link

codecov-commenter commented May 4, 2021

Codecov Report

Merging #4232 (0514672) into master (d76c55a) will increase coverage by 1.05%.
The diff coverage is 62.31%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4232      +/-   ##
+ Coverage   70.11%   71.17%   +1.05%     
  Files         219      218       -1     
  Lines        5455     4700     -755     
  Branches     1029      760     -269     
- Hits         3825     3345     -480     
+ Misses       1468     1270     -198     
+ Partials      162       85      -77     
Impacted Files Coverage Δ
src/plugins/backgroundColor.js 56.25% <37.50%> (-4.22%) ⬇️
src/plugins/backdropBrightness.js 45.45% <42.85%> (-13.81%) ⬇️
src/plugins/backdropGrayscale.js 45.45% <42.85%> (-13.81%) ⬇️
src/plugins/divideOpacity.js 45.45% <42.85%> (-12.88%) ⬇️
src/plugins/saturate.js 45.45% <42.85%> (-18.55%) ⬇️
src/plugins/backdropBlur.js 45.45% <50.00%> (-18.55%) ⬇️
src/plugins/backdropContrast.js 45.45% <50.00%> (-13.81%) ⬇️
src/plugins/backdropHueRotate.js 45.45% <50.00%> (-13.81%) ⬇️
src/plugins/backdropInvert.js 45.45% <50.00%> (-13.81%) ⬇️
src/plugins/backdropOpacity.js 45.45% <50.00%> (-13.81%) ⬇️
... and 75 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d76c55a...0514672. Read the comment docs.

@adamwathan adamwathan changed the title Match utilities Improve matchUtilities API and make it work with the AOT engine May 4, 2021
@RobinMalfait RobinMalfait merged commit 6628eb0 into master May 5, 2021
2 checks passed
@RobinMalfait RobinMalfait deleted the match-utilities branch May 5, 2021
adamwathan added a commit that referenced this issue May 7, 2021

* implement matchUtilities2

* ensure animation names without keyframes are not prefixed

* remove matchBase

* call addUtilities for each group individually

* WIP: Write plugins using matchUtilities2


* Fix arbitrary value support for fontSize

* Fixes, update fixtures

* Rebuild fixtures

* Don't generate `divide` class with no modifier

* Fixes, rebuild fixtures

* Rename matchUtilities2 to matchUtilities

* Delete bad tests

* Remove temp files GROSS

* Clean stuff up

* Support no return in matchUtilities

Co-authored-by: Adam Wathan <>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
None yet
None yet

Successfully merging this pull request may close these issues.

None yet

3 participants