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

Support extend in variants config #2651

Merged
merged 3 commits into from Oct 23, 2020
Merged

Support extend in variants config #2651

merged 3 commits into from Oct 23, 2020

Conversation

adamwathan
Copy link
Member

This PR makes it possible to extend your variants configuration to avoid having to write out the entire default variant list whenever you want to enable an extra variant.

For example, this is what it looks like to add focus-within to the backgroundColor plugin traditionally:

module.exports = {
  // ...
  variants: {
    backgroundColor: ['responsive', 'focus-within', 'hover', 'focus']
  }
}

This is what it would look like after this PR is merged:

module.exports = {
  // ...
  variants: {
    extend: {
      backgroundColor: ['focus-within']
    }
  }
}

This is a non-breaking change and doesn't break or remove any existing functionality.


Motivation

Trying to extend variants has been the source of a lot of issues, Discord questions, forum posts, etc. over the years, as it's very common for people to do something like this and then be surprised that all of the default variants stopped working:

module.exports = {
  // ...
  variants: {
    backgroundColor: ['focus-within']
  }
}

It's challenging to solve this problem though because the order of your variants matters, because it affects the order your CSS is generated in and CSS source order affects specificity.

For example, take a look at this HTML:

<button class="bg-blue-500 hover:bg-pink-500 focus:bg-green-500">
  <!-- ... -->
</button>

Given this configuration:

module.exports = {
  // ...
  variants: {
    backgroundColor: ['hover', 'focus']
  }
}

...when the button is both hovered and focused, the background is green, because the focus styles are defined after the hover styles.

With this configuration:

module.exports = {
  // ...
  variants: {
    backgroundColor: ['focus', 'hover']
  }
}

...when the button is both hovered and focused, the background is pink, because now the hover styles are taking precedence over the focus styles.

For this reason we punted on trying to solve this problem for a long time, because when someone needed to add a new variant, they also needed some way to say where in the list that variant should be added.

Eventually, I added this function-driven API that let you specify the position of a variant using helpers like before and after like so:

module.exports = {
  // ...
  variants: {
    backgroundColor: ({ before }) => before(['focus-within'], 'hover')
  }
}

This would add the focus-within variant before the hover variant in the existing list.

This totally works and is a very flexible API, but it is fairly cryptic looking, especially to people who aren't expert JavaScript programmers. It gets especially cryptic when you start trying to compose before and after together like this:

module.exports = {
  variants: {
    // Defaults are ['responsive', 'hover', 'focus']
    backgroundColor: ({ before, after, without }) => without(
      ['focus'],
      before(['active'], 'hover', after(['focus-within'], 'responsive'))
    ),
    // Output: [responsive', 'focus-within', 'active', 'hover']
  },
}

Yeah it works but man that is some insane looking shit.

We need a better way! I regret adding this API and will likely stop documenting it after this PR is merged.


Detailed design

This PR uses a new approach based on the existing extend behavior people are used to from the theme section. You simply add the variants you want to enable under the extend section, and they are merged with any other configured variants for that plugin.

module.exports = {
  // ...
  variants: {
    extend: {
      backgroundColor: ['focus-within']
    }
  }
}

Handling sort order

To handle the sort order, we maintain a "recommended sort order" list in Tailwind that is stored in the default config under variantOrder:

module.exports = {
  // ...
  variantOrder: [
    'first',
    'last',
    'odd',
    'even',
    'visited',
    'checked',
    'group-hover',
    'group-focus',
    'focus-within',
    'hover',
    'focus',
    'focus-visible',
    'active',
    'disabled',
  ],
  // ...
}

When you use extend, we merge your extended variants with any other configured variants, then sort them using this list as a reference.

Overriding sort order

If you want to change the sort order, you can override the variantOrder key in your own config:

module.exports = {
  // ...
  variantOrder: [
    'focus',
    'last',
    'odd',
    'group-hover',
    'visited',
    'checked',
    'group-focus',
    'hover',
    'disabled',
    'even',
    'focus-within',
    'focus-visible',
    'active',
    'first',
  ],
  // ...
}

Tailwind will use this as the new reference list when sorting things.

Overriding sort order per plugin

Tailwind will only sort your variants if you are actually using the extend feature for that plugin. If you'd like to have total control over the sort order per plugin, just set up your variant list the same way you alread do now:

module.exports = {
  // ...
  variants: {
    backgroundColor: ['responsive', 'focus', 'hover', 'focus-within']
  }
}

Since in this example we aren't using the extend feature for the backgroundColor plugin, Tailwind will not attempt to sort it, and instead trust that you have put the variants in the order that you want them in.

Working with third-party variant plugins

If you are using any plugins that add variants to Tailwind, those variants will always be added at the beginning of the variant list by default when using extend and triggering sorting:

module.exports = {
  // ...
  variants: {
    extend: {
      backgroundColor: ['focus-within', 'custom-variant']
      // => 'responsive', 'custom-variant', 'focus-within', 'hover', 'focus'
    }
  }
}

You can put the variant in a specific place by simply not using extend and providing the whole list yourself:

module.exports = {
  // ...
  variants: {
      backgroundColor: ['responsive', 'focus-within', 'hover', 'custom-variant', 'focus']
      // => 
  }
}

...or by adding the custom variant to your variantOrder configuration:

module.exports = {
  // ...
  variantOrder: [
    'first',
    'last',
    'odd',
    'even',
    'visited',
    'checked',
    'group-hover',
    'group-focus',
    'focus-within',
    'custom-variant',
    'hover',
    'focus',
    'focus-visible',
    'active',
    'disabled',
  ],
  // ...
}

We recommend just copying the default sort order from the default config and using it as a starting point when you need to do this, effectively just taking ownership of the list and managing it yourself.


Future improvements

We may eventually consider supporting a function syntax for variantOrder so you can more easily insert custom variants in specific positions in the list, but that is not included at the time of this PR.

@Sparragus
Copy link

I love this API. Seems like it's going to feel perfect. Just like extending a theme.

@simonswiss
Copy link
Contributor

Man, you are on some next-level stuff, this is looking great! Definitely easier to learn/teach than the before and after helper functions 👍

@valtism
Copy link
Contributor

valtism commented Oct 23, 2020

This is a great move.

@adamwathan adamwathan merged commit 11af870 into master Oct 23, 2020
@adamwathan adamwathan deleted the extend-variants branch October 23, 2020 12:58
@QWp6t
Copy link

QWp6t commented Nov 10, 2020

We may eventually consider supporting a function syntax for variantOrder

Another potential option would be to use an object literal keyed by variant with the value being an integer representing the priority.

  variantOrder: {
    focus: 100,
    last: 200,
    odd: 300,
    "group-hover": 400,
    visited: 500,
    // ...
  },
  // => 'focus', 'last', 'odd', 'group-hover', 'visited'
  extend: {
    focus: 450,
    'custom-variant': 250,
  },
  // => 'last', 'custom-variant', 'odd', 'group-hover', 'focus', 'visited'

Maybe you guys have already looked into doing something like this and ruled it out, I'm not sure. But I figured I'd mention it in case it's something you hadn't yet considered.

Also, if you were going to build a function syntax on top of this approach, it would allow for something like this...

extend: variantOrder => ({
    ...variantOrder,
    'a-thing-after-focus': variantOrder.focus + 1
})

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.

None yet

5 participants