Skip to content

Restructure config file for 1.0 #637

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
Feb 5, 2019
Merged

Restructure config file for 1.0 #637

merged 25 commits into from
Feb 5, 2019

Conversation

adamwathan
Copy link
Member

@adamwathan adamwathan commented Feb 1, 2019

This PR changes some decisions about the config file structure that we never would have made had we known that eventually we would be adding the modules key and the options key.

TL;DR: Some sections have moved or been renamed, and we are going to start encouraging people to keep their config files much simpler and inherit our defaults unless they explicitly want to override or extend them.

Example of what a new Tailwind config file might look like:

const defaultTheme = require('tailwindcss/defaultTheme')()

module.exports = {
  important: true,
  theme: {
    colors: {
      ...defaultTheme.colors,
      primary: defaultTheme.colors['blue']
    },
    margin: {
      ...defaultTheme.margin,
      '48': '12rem',
      '64': '16rem',
    }
  },
  variants: {
    opacity: ['responsive', 'hover']
  }
}

Many thanks to @reinink, @samselikoff, @taylorotwell, @derrickreimer, @davidhemphill, and @jasonmccreary for putting up with my incessant badgering, whining, desperate requests for feedback and border-line depression trying to make this not suck over the last three weeks. You don't even want to see all of the iterations this went through 😩

How the config file structure has changed

  1. Options are now top-level.

    The options key as been removed and the separator, prefix, and important options are now top-level keys in the config file:

    module.exports = {
      prefix: '',
      important: false,
      separator: ':',
      // ...
    }

    This is part of a larger vision where the config file is truly more of a config file, and details about your design system are just a specific part of that rather than the top-level focus. Think of it more like a postcss.config.js or a babel.config.js file — a bunch of options for configuring Tailwind the tool, where your styles are just one part of that.

  2. Style-related options have moved to a new theme section.

    All of the top-level options related to your design system (screens, colors, fonts, backgroundColors, padding, margin, etc) have been nested inside a new theme section.

    After introducing the modules, options, and plugins sections, it felt very off to have all of the style options be direct siblings of options that were much more configuration-ish.

    It also made things look a bit weird if you (like many) kept your style-related configuration in other files, because you need to spread your styles into the configuration at the top-level:

    module.exports = {
      ...styles,
      modules: [
        // ...
      ],
      plugins: [
        // ...
      ],
      options: {
        prefix: '',
        important: false,
        separator: ':',
      }
    }

    With everything nested underneath the new theme section, the example above would look like this:

    module.exports = {
      prefix: '',
      important: false,
      separator: ':',
      theme: styles,
      modules: [
        // ...
      ],
      plugins: [
        // ...
      ],
    }

    Not a huge difference, but conceptually I think it makes a lot more sense for those options to be grouped together instead of all sitting at the top-level alongside things like options and modules.

  3. Your theme section is merged with the default theme values.

    Tailwind already works this way in 0.x, but it's worth noting here for clarity.

    You can leave your theme section completely empty and your build will just use Tailwind's default values.

    If you provide any keys, those keys will override the default values.

    module.exports = {
      theme: {
        // This will completely replace the default tracking values
        tracking: {
          '1': '-0.05em',
          '2': '0',
          '3': '0.05em',
        },
      },
    }

    To extend any of Tailwind's defaults, you can grab the default values from tailwindcss/defaultTheme and spread them in:

    const defaultTheme = require('tailwindcss/defaultTheme')()
    
    module.exports = {
      theme: {
        // This will extend the default tracking values
        tracking: {
          ...defaultTheme.tracking,
          wider: '0.1em',
          widest: '0.15em',
        },
      },
    }

    Note that the tailwindcss/defaultTheme module exports a function that returns the default theme (to avoid accidental mutation).

    Based on a lot of user interviews I've done over the last few weeks and a poll I conducted on Twitter, I learned that a lot of people really value having Tailwind provide well-considered default values.

    So for v1.0, we are going to take a bit of a more opinionated stance and encourage partial customization of the default config by just overriding and extending the parts you want to change instead of publishing the entire default config by default for you to own and manipulate.

    Everything of course will still be as customizable as ever, but the hope is that most people's config files will end up being much shorter/simpler, and serve just as a reference for the user's customizations, rather than having all of the defaults and their customizations combined into one giant file, where it's hard to tell what came with Tailwind out of the box vs. what was added by the user.

  4. The modules section has been renamed to variants.

    "Modules" was a word we just kinda grabbed because we needed something, and we wanted to use that section of the config to both specify variants and disable modules if necessary.

    Now that all of Tailwind's internal "modules" are actually just core plugins, I want to deprecate this terminology entirely, and make this section of the config purely about configuring variants for core plugins.

      module.exports = {
        // ...
    -   modules: {
    +   variants: {
          appearance: ['responsive'],
          backgroundAttachment: ['responsive'],
          backgroundColors: ['responsive', 'hover', 'focus'],
          // ...
          zIndex: ['responsive'],
        },
        // ...
      }
  5. Your variants section is merged with the default variants values.

    Just like the theme section, you only need to provide values for the variants you'd like to override.

    If you are fine with the defaults, you don't even need to provide the key.

    To add hover variants to opacity for example, you'd just add the opacity key and list the variants you want to generate:

    module.exports = {
      // ...
      variants: {
        opacity: ['responsive', 'hover'],
      },
      // ...
    }

    It's important to note that whatever you provide overrides the default value, it's not merged. So to add a hover variant to opacity, you need to re-specify the responsive variant too, unless of course you don't want to generate it.

    We may make it easier to simply extend this configuration later, but I don't think this will be a real pain point for anyone in practice.

  6. Modules are disabled in the corePlugins section, not the variants section.

    Instead of setting a module core plugin to false in the variants section of your config to disable it, you now set that plugin to false in a new corePlugins section of the config:

      variants: {
        // ...
        negativeMargin: ['responsive'],
    -   objectFit: false,
    -   objectPosition: false,
        opacity: ['responsive'],
        // ...
      },
    + corePlugins: {
    +   objectFit: false,
    +   objectPosition: false,
    + },

    The motivation behind this is that in the near future, I plan to rewrite our preflight styles as a core plugin as well, and if users want to disable preflight, doing so in the variants section of the config makes no sense because there's no way to add "responsive" or "hover" versions of preflight.

    We may also move the container plugin back to being a true "core plugin" instead of just a third-party plugin that happens to be in the same repository, so this change is also important in order for people to disable that plugin if we make that change.

    Overall, it just makes sense that disabling core plugins is divorced from variant configuration, because the idea of variants isn't relevant for every core plugin.

  7. Core plugins can be configured directly through the corePlugins section.

    In Convert built-in utility modules to private plugins #620, all of Tailwind's built-in modules were rewritten to use the plugin system, so there is no difference between the code we write to add new core functionality to Tailwind and the code that someone might write when authoring a third-party plugin.

    Part of this included each plugin becoming directly configurable (for Tailwind's benefit internally), much like a third-party plugin would be.

    Essentially, there's no reason you couldn't disable a core plugin, then re-introduce it by adding it your plugins key and passing it the appropriate configuration:

    corePlugins: {
      backgroundSize: false,
    },
    plugins: [
      require('tailwindcss/plugins/backgroundSize')({
        variants: ['responsive'],
        values: {
          auto: 'auto',
          cover: 'cover',
          contain: 'contain',
        },
      })
    ]

    In addition to allowing you to disable core plugins, the corePlugins key also allows you to completely override a core plugin's configuration, by providing a configuration object for that plugin.

    This example does exactly the same thing as the previous example:

    corePlugins: {
      backgroundSize: {
        variants: ['responsive'],
        values: {
          auto: 'auto',
          cover: 'cover',
          contain: 'contain',
        },
      },
    },

    There's essentially no reason at all to do this currently (it's much simpler to configure a core plugin through the variants and theme keys), but designing the configuration file this way means it could be possible to pass advanced configuration options to core plugins in the future that might not be exposed through the theme section.

    For example, perhaps the margin plugin could expose an additional option to let you customize the class names to margin-{side}-{size} instead of the default m{size}-{size} syntax:

    corePlugins: {
      margin: {
        variants: ['responsive'],
        values: {
          auto: 'auto',
          // ...
          '32': '8rem',
        },
        classPrefix: 'margin-',
      },
    },

    I don't have concrete plans to introduce features like this to the core plugins, but designing the config file this way at least makes it possible.

  8. Every section is optional, as is the entire config file.

    As I alluded to in point 3, one of the main themes for Tailwind v1.0 is going to be doubling down on the defaults and shouldering more of the burden for making sure the default values are awesome.

    Until now, it's been easy to tell people "just customize it in your config" if they weren't happy with a default value. Going forward, I'd like to try even harder to make the defaults as widely usable as possible so that customization because more of an escape hatch than a mandatory step in using Tailwind.

    As I mentioned before, there are no plans to make it harder or impossible to customize any of the things you can currently customize — being able to customize things is the whole reason I created the framework! But rather than sending the message of "you own every value the framework uses, customize the hell out of it", I want to start saying "we've worked really hard to give you the best possible starting point we could create, but if you need to change or extend the framework in any way, it's easy to do".

    Certain things are going to be very commonly completely replaced, like colors and fonts. But we should be able to make most of the other stuff suitable for just about any project right out of the box. Sure you might need to add an extra margin or padding value here and there, but in general you should be able to rely on our defaults and get pretty far.

    All that to say, in this config file, every single key is optional. You don't even need to provide a config file at all if you don't want:

    // postcss.config.js
    module.exports = {
      plugins: [
        // Pass a custom config file if you have one:
        require('tailwindcss')('./tailwind.js'),
    
        // Or pass nothing and just use the defaults:
        require('tailwindcss')(),
      ]
    }

    All of this is already true in Tailwind 0.x for what it's worth, but most people don't use Tailwind this way because it's not the path we encourage.

    So in a future PR, the tailwind init command will be changing as well, to create a much more minimal config file by default, with perhaps an extra flag you can pass if you want to generate the entire thing (like a tailwind init --eject or something) and not inherit the default values at all.

Upgrade path

This is a pretty significant breaking change with two components:

  1. You'll need to update your config file.

    This is the obvious one, and it's very easy because it just involves renaming modules to variants, moving any false values in modules to the corePlugins section, and nesting all of your style configuration under the new theme key. Minor annoyance but 10 minutes of effort, and we can probably write a tool to upgrade it for you automatically.

  2. You need to tweak any uses of the config() function in your custom CSS.

    This is the more annoying part. Previously you could get access to one of your colors for example by using config('colors.red'). Now that colors are nested under the theme section, you need to update those references to config('theme.colors.red'). There's a good chance I introduce a new theme() function you can use in your CSS that is automatically scoped to the theme section, and that should make this update easier.

    If anyone is actually using config() to access things like prefix, separator, important, or variant configuration, I'd love to know. I can't think of any good reason to ever do that, so we may even deprecate the config() function entirely in favor the theme() function I proposed above.

@m1guelpf
Copy link
Contributor

m1guelpf commented Feb 1, 2019

The only thing I don't like about this is that I can no longer peek into my tailwind configuration to check what width values do I have, etc.

@rathesDot
Copy link

All the changes look pretty fine and make the entire config file more readable since it is now better structured. Also, the entire PR description explains the changes understandable. All in all, great job! 👏

What I haven't understood entirely is how one would unset certain values. Like, if the default config has margin styles, but I don't want them in my stylesheet, how would I accomplish that? Do I have to do a delete config['margin']; or something like that?

@JacobBennett
Copy link

@rathesDot it seems to me like you could just do

module.exports = {
  theme: {
    // This will completely replace the default margin values
    margin: {},
  },
}

@m1guelpf You could just go look at the default tailwind config that will be inside the package right? If you set any custom ones it would work the exact same as it does now, just go look in your config, but if you don't set them explicitly, they will just be whatever the defaults are.

@mikemand
Copy link

mikemand commented Feb 1, 2019

@JacobBennett I think @m1guelpf's point was he could just open tailwind.js and see that m-3 would end up doing a margin of 0.75rem. Instead of having to open the docs and try to remember where that section is (or searching for it - if you remember the docs have a search 😆).

This isn't such a huge issue if you use an IDE. I can jump to the declaration of require('tailwindcss/defaultConfig') and from there jump to the declaration of require('./defaultConfig.stub.js').

Either way, you could still keep your config file as a clone (plus your changes) of the default config if you wanted to. That's the beauty of Tailwind. 😃 There are some extremely well-thought-out defaults set. Keep them all, add to them, or change everything. That's up to you to decide.

@imliam
Copy link

imliam commented Feb 1, 2019

Love the new style, all the changes make a lot of sense to me and I can't see any problems other than what's already been pointed out, the agonising over getting it right really paid off 👍🏻

I completely echo @m1guelpf's thoughts on how it'd be a bit less convenient to have to go between two files to find values. I imagine it would be fairly simple to have some tooling build up the combined config file into something viewable for the sake of development though, so it's not a huge concern.

@rathesDot
Copy link

@rathesDot it seems to me like you could just do

module.exports = {
  theme: {
    // This will completely replace the default margin values
    margin: {},
  },
}

@JacobBennett That is another approach, but it feels wrong to me to add stuff to your config to actually remove stuff.

A solution would be to have an option that allows disabling the default-fallback. Then Tailwind would only generate styles from the given values.

I think that this option could also help @m1guelpf's concern. You could then easily copy the default file to your own, disable the fallback and then work in the way as it does right now as well.

@pandroid
Copy link

pandroid commented Feb 2, 2019

Maybe this is the right moment to think about responsive values?

In my projects I'm very often use values in the tailwind-config with css-variables defined in a seperate css file. So its possible to change e.g. the text-size or margins and paddings depending on breakpoints.

Thinking of component-spaces for me it is very useful if I have to change the padding of all components on a higher breakpoint. Or thinking of text-sizes. You always have to change the text-size of your headlines for smaller screens. Or 'text-xs' is too small for mobile.

If you restructure the config file now maybe there is a way to do this in the configuration?

@adamwathan
Copy link
Member Author

Thanks everyone for the feedback so far!

The only thing I don't like about this is that I can no longer peek into my tailwind configuration to check what width values do I have, etc.

@m1guelpf: This is a totally valid concern and is one of the biggest trade-offs in taking this approach. What made me more comfortable with it was realizing that there are a whole host of other classes in Tailwind that you can't learn from your config file, and that you would have to consult the documentation for anyways, like all of the whitespace classes, overflow classes, text decoration classes, etc.

My hope is that if you are relying on the defaults, you will eventually just learn them and not have to reference any of those values in the docs anyways, and if you are not relying on the defaults, then your custom values will be right there in your config file just like they always were.

I am pretty convinced we'll include some sort of tailwind eject CLI option to clone all of the defaults into your configuration file so you are no longer inheriting the default values and instead just own your copy of them, just like how Tailwind 0.x works now.

Based on the number of 👍 reactions it looks like a lot of other people have this same hesitation, so please let me know if what I said eases your concerns or if you have any other thoughts.

What I haven't understood entirely is how one would unset certain values. Like, if the default config has margin styles, but I don't want them in my stylesheet, how would I accomplish that? Do I have to do a delete config['margin']; or something like that?

@rathesDot: Hey! So the recommend approach to disable the margin classes entirely is to disable the margin plugin in your corePlugins:

module.exports = {
  // ...
  corePlugins: {
    margin: false,
  }
}

This is really similar to how you would disable it already in Tailwind 0.x, except now you do it in corePlugins instead of modules 👍

That is another approach, but it feels wrong to me to add stuff to your config to actually remove stuff.

@rathesDot: I totally understand this feeling, and my first iterations of the new config file format actually had all of the core plugins listed right in the plugins section of your config, so to disable one you would just delete it from the list. The problem with this approach is that I feel like it made all of Tailwind's core plugins feel very disposable, which could be a positive depending on your perspective, but when I learned just how many people use Tailwind because of the default values (over 700 people care more about the default styles than the ability to customize) I decided it didn't make sense to make the defaults feel like merely an example, and that it made more sense to make them feel more baked in.

Plus the config file looked like a complete disaster:

https://gist.github.com/adamwathan/569cf056631950ff0eda7f4b50240e98

@rathesDot
Copy link

@adamwathan Even though I'm not entirely convinced, I understand your reasoning! Thanks for the explanation! For me it isn't a big hassle, it just felt strange. But I'm pretty sure that I won't notice that anymore once I get used to the new config file!

Also, a big hand for the awesome work!

@aparajita
Copy link

The only thing I don't like about this is that I can no longer peek into my tailwind configuration to check what width values do I have, etc.

Hey @adamwathan, I think the answer to this is to do sort of what eslint does, by adding another tailwind command: print-config, which would merge the default config with the user config and print out the result.

@m1guelpf
Copy link
Contributor

m1guelpf commented Feb 3, 2019

This would also be a great moment for someone to release a way to generate custom documentation from your tailwind config 😉

@aparajita
Copy link

print-config could also take a JSON path, so if you're only interested in the theme colors, you'd call:

tailwind print-config theme.colors

@sambeevors
Copy link

Extending from what @aparajita suggested, I think some sort of API for this would be great, not only for people who want to get a visual "tree" of their config, but also to help plugin developers. One of the biggest benefits for me of the current config is it's really easy to parse the config and know what the output is going to look like. Config looks really good though, I love the simplicity of it 😍

@rathesDot
Copy link

I like @aparajita's suggestion, but that sounds for me like a Nice-To-Have that can be perfectly added after the config was released as it is proposed in this PR...

@@ -18,14 +18,12 @@ describe('cli', () => {
it('creates a Tailwind config file', () => {
return cli(['init']).then(() => {
expect(utils.writeFile.mock.calls[0][0]).toEqual(constants.defaultConfigFile)
expect(utils.writeFile.mock.calls[0][1]).toContain('defaultConfig')
Copy link
Contributor

@MattStypa MattStypa Feb 5, 2019

Choose a reason for hiding this comment

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

What do you think about checking for defaultTheme instead of dropping this assertion?

expect(utils.writeFile.mock.calls[0][1]).toContain('defaultTheme')

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what I even want to scaffold for the config file when people use tailwind init yet so just gonna punt on it entirely for now I think.

@adamwathan
Copy link
Member Author

Going to merge this for now as I have a bunch of other stuff I'm working on that depends on it, still time to make changes if necessary though.

@jan-dh
Copy link

jan-dh commented Feb 10, 2019

What happens when you extend the default themes and use keys already set by the defaultTheme? They still override the default or would you get some duplicate keys - errors?

 margin: {
      ...defaultTheme.margin,
      '48': '12rem',
      '64': '16rem',
    }

It feels like a lot of work if I have to manually check if all the values I want to override exist in the default config or not. Sharing the concerns of @m1guelpf. Think the eject option as you mentioned would be my go-to setup from the start.

@tlgreg
Copy link

tlgreg commented Feb 10, 2019

@jan-dh It's just javascript, so if you do it in that order, you override the defaults.

@mdominguez
Copy link

Little typo in Item 7.

instead of the default m{size}-{size} syntax:

should be

instead of the default m{side}-{size} syntax:

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.