Skip to content

Conversation

adamwathan
Copy link
Member

@adamwathan adamwathan commented Mar 6, 2018

This PR introduces some significant changes to the API of Tailwind plugins, with a focus on making things much more accessible and less verbose.

Breaking Changes

Although it was never officially announced and documentation was never published, the existing plugin system does exist in v0.4.3, so I think it's only responsible to address any breaking changes, even though it probably affects literally zero people.

  • The rule helper is gone
  • The atRule helper is gone
  • The utility helper is gone
  • addUtilities will respect the user's prefix and important options by default
  • addComponents will respect the user's prefix option by default

All of these have been replaced by CSS-in-JS syntax, with the option to drop down to raw PostCSS if necessary (I haven't actually encountered any situations where this is needed.)

New syntax

Instead of the horrendous rule/atRule driven API we have currently, this PR adds support for a CSS-in-JS-style object syntax.

Demonstrated by example, this code:

{
  // ...
  plugins: [
    function ({ rule, atRule, addUtilities, addComponents }) {
      addUtilities([
        rule('.object-fill', {
          'object-fit': 'fill',
        }),
        rule('.object-contain', {
          'object-fit': 'contain',
        }),
        rule('.object-cover', {
          'object-fit': 'cover',
        }),
      ], ['responsive'])

      addComponents([
        rule('.btn-blue', {
          'background-color': 'blue',
          'color': 'white',
          'padding': '.5rem 1rem',
          'border-radius': '.25rem',
        }),
        rule('.btn-blue:hover', {
          'background-color': 'darkblue',
        }),
        rule('.container', {
          'width': '100%',
        }),
        atRule('@media (min-width: 100px)', [
          rule('.container', {
            'max-width': '100px',
          }),
        ]),
        atRule('@media (min-width: 200px)', [
          rule('.container', {
            'max-width': '200px',
          }),
        ]),
        atRule('@media (min-width: 300px)', [
          rule('.container', {
            'max-width': '300px',
          }),
        ]),
      ])
    }
  ],
}

...would now be written like this:

{
  // ...
  plugins: [
    function ({ addUtilities, addComponents }) {
      addUtilities({
        '.object-fill': {
          objectFit: 'fill',
        },
        '.object-contain': {
          objectFit: 'contain',
        },
        '.object-cover': {
          objectFit: 'cover',
        },
      }, ['responsive'])

      addComponents({
        '.btn-blue': {
          backgroundColor: 'blue',
          color: 'white',
          padding: '.5rem 1rem',
          borderRadius: '.25rem',
          '&:hover': {
            backgroundColor: 'darkblue',
          },
        },
        '.container': {
          width: '100%',
          '@media (min-width: 100px)': {
            maxWidth: '100px',
          },
          '@media (min-width: 200px)': {
            maxWidth: '200px',
          },
          '@media (min-width: 300px)': {
            maxWidth: '300px',
          },
        },
      })
    }
  ],
}

CSS-in-JS styles can either be passed in as one big object:

addUtilities({
  '.object-fill': {
    objectFit: 'fill',
  },
  '.object-contain': {
    objectFit: 'contain',
  },
  '.object-cover': {
    objectFit: 'cover',
  },
})

...or as an array of objects, which is useful to avoid clobbering if you need two instances of the same selector:

addComponents([
  {
    '.container': {
      width: '100%',
    },
    '@media (min-width: 100px)': {
      '.container': {
        maxWidth: '100px',
      },
    },
  },
  {
    '.btn': {
      padding: '1rem .5rem',
      display: 'block',
    },
    '@media (min-width: 100px)': {
      '.btn': {
        display: 'inline-block',
      },
    },
  },
])

Awesome things about this change:

  • It supports nesting like you are used to in Less/Sass
  • It supports camelCase property names, although snake-case is fine too
  • It's way less crap to type

Complete updated plugin signature

The updated plugin signature now looks like this:

function ({ config, prefix, e, addUtilities, addComponents }) {
  // ...
}
  • config(path, defaultValue) lets you access data from the user's config
  • prefix(selector) allows you to apply the user's configured prefix to a selector
  • e(string) lets you escape a string to make it valid as part of a CSS class name; useful if your plugin generates class names derived from user input
  • addUtilities(utilities, options) lets you add new utilities
  • addComponents(components, options) lets you add new components

addUtilities improvements

Previously, the second argument to addUtilities was just a list of variants to generate for those utilities. While this is still supported, you can also pass an object as the second argument that exposes two new options:

function ({ addUtilities }) {
  addUtilities({
    '.object-fill': {
      objectFit: 'fill',
    },
  }, {
    variants: ['responsive', 'hover'],
    respectPrefix: true,
    respectImportant: true,
  })
}
  • respectPrefix lets you specify whether Tailwind should automatically prefix your classes with the user's configured prefix. Defaults to true.
  • respectImportant lets you specify whether Tailwind should automatically mark declarations as !important if the user has configured that in their config file. Defaults to true.

I doubt people will need to disable these two options very often, but the option is there when it's necessary.

addComponents improvements

addComponents now takes a second options argument as well, but only exposes one option:

function ({ addComponents }) {
  addComponents({
    '.btn': {
      display: 'inline-block',
      // ...
    },
  }, {
    respectPrefix: true,
  })
}

Just like with utilities, respectPrefix (defaults to true) makes it easy for you to apply the user's configured prefix to your component classes automatically. Set this to false if you need fine grained control over if or how prefixes should be applied.

prefix improvements

Previously, the prefix function was very dumb in its implementation and always assumed the selector your were trying to prefix was a simple single class, like .object-contain.

Now, prefix will intelligently prefix every class in a selector, leaving non-classes untouched:

prefix('.btn-blue > h1.text-xl + a .bar')
// .tw-btn-blue > h1.tw-text-xl + a .tw-bar

This behavior applies when using the respectPrefix option as well as when using the prefix helper directly.

If you only want to prefix certain classes in a selector and not others (why I don't know but whatever), you can build up the prefixed selector yourself:

`${prefix('.btn-blue')} > h1.text-xl + a ${prefix('.bar')}`
// .tw-btn-blue > h1.text-xl + a .tw-bar

Using raw PostCSS

Both addUtilities and addComponents can also accept an array of raw PostCSS nodes. If you need that much power for any reason, simply import postcss into your plugin and work with the PostCSS API directly:

import postcss from 'postcss'

export default function({ addUtilities }) {
  addUtilities([
    postcss.rule({ selector: '.object-fill' }).append([
      postcss.decl({
        prop: 'object-fit',
        value: 'fill',
      }),
    ]),
    postcss.rule({ selector: '.object-contain' }).append([
      postcss.decl({
        prop: 'object-fit',
        value: 'contain',
      }),
    ]),
    postcss.rule({ selector: '.object-cover' }).append([
      postcss.decl({
        prop: 'object-fit',
        value: 'cover',
      }),
    ]),
  ])
}

You can also mix raw PostCSS nodes with CSS-in-JS:

import postcss from 'postcss'

export default function({ addUtilities }) {
  addUtilities([
    {
      '.object-fill': {
        objectFit: 'fill',
      },
    },
    postcss.rule({ selector: '.object-contain' }).append([
      postcss.decl({
        prop: 'object-fit',
        value: 'contain',
      }),
    ]),
    postcss.rule({ selector: '.object-cover' }).append([
      postcss.decl({
        prop: 'object-fit',
        value: 'cover',
      }),
    ]),
  ])
}

For more examples, check out the tests.

@reinink
Copy link
Member

reinink commented Mar 6, 2018

Wow. :feelsgood:

@HellPat
Copy link

HellPat commented Mar 6, 2018

Nice

@adamwathan
Copy link
Member Author

@inxilpro You might like this!

@ThibaudDauce
Copy link

Is there a way to @apply some TailwindCSS base utility in plugins?

@inxilpro
Copy link

inxilpro commented Mar 7, 2018

👍👏

@adamwathan
Copy link
Member Author

@ThibaudDauce:

Is there a way to @apply some TailwindCSS base utility in plugins?

Technically there's nothing stopping you from doing this:

plugins: [
  function ({ addComponents }) {
    addComponents({
      '.btn-indigo': {
        '@apply .px-4 .py-2 .inline-block .bg-indigo .text-white': {}
      },
    })
  }
]

...which will totally work, but I strongly recommend against it, because out of the 5 classes in that example, you can only safely expect .inline-block to actually exist, since all of the other ones are generated from a user-defined customizable scale.

@apply should be reserved for use in your own CSS where it's reasonable for you to know that .px-4 exists since you're the one who is in control of your config. Plugin authors who are building things for other people to install should just write the styles directly 👍

@adamwathan adamwathan merged commit dbb4802 into master Mar 7, 2018
@adamwathan adamwathan mentioned this pull request Mar 8, 2018
@garygreen
Copy link

garygreen commented Mar 8, 2018

Would it be possible to do something like this (psuedocode) ?

@apply {
    .px-4 || 2rem
    .py-2 || 4rem
    .inline-block
    .bg-indigo || blue
    .text-white || white
}

It seems a lot of the "css in js" config surrounds mostly adding default values, so it would be nice to keep the concise @apply style syntax with default value overrides.

...then again I guess people could turn off padding module, or have px-4 be some crazy value which doesn't make buttons look like the original component author intended.

@garygreen
Copy link

garygreen commented Mar 8, 2018

Also, could you just add a bunch of raw css to it instead, in case you find adding css in js too much?

addUtilities(`
    .object-fill {
       object-fit: fill;
   }
`);

@adamwathan
Copy link
Member Author

...then again I guess people could turn off padding module, or have px-4 be some crazy value which doesn't make buttons look like the original component author intended.

Yeah this is the annoying problem that makes it sort of a bad idea to work that way inside of plugins 😕 There might be a way to make that experience a bit better though, and the nice thing is since it's all just JS it's easy to build helpers outside of the main framework and test them out there.

Also, could you just add a bunch of raw css to it instead, in case you find adding css in js too much?

Yep could totally make this work; as it stands you could do this already if you import postcss into your plugin:

addUtilities(postcss.parse(`
    .object-fill {
       object-fit: fill;
   }
`));

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