Skip to content

Instantly share code, notes, and snippets.

@christianalfoni
Last active May 31, 2020 00:14
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save christianalfoni/3ff86d8b8e4c51e57e19855aa27b0a0f to your computer and use it in GitHub Desktop.
Save christianalfoni/3ff86d8b8e4c51e57e19855aa27b0a0f to your computer and use it in GitHub Desktop.

Taking inspiration from tailwindcss and this idea, here is a specification for how to achieve it.

What is it

With TailwindCSS you have an amazing default design system, but it is not ideal. It lacks:

  • Validation of classnames: It does not validate the classnames you insert
  • Composition: It does not allow you to effectively compose together classnames into new classnames and it can not dynamically do so at runtime effectively
  • Defining by variables: Even though it is nice to write TailwindCSS inline with your elements, you can not define classes as variables, because the TailwindCSS extension does not understand it

This solution solves all of this with an even better experience of setup and consumption!

1. Install library

npm install awesome-css

2. Add babel plugin

{
  plugins: ['awesome-css']
}

3. Consume the complete design system with full typing

import { classnames, hover } from 'awesome-css'

const button = classnames('border-none', 'bg-gray-500', hover('bg-gray-300'))

4. Build for production

The babel plugin traverses your code and extracts the parts of the design system you have actually consumed in your app

5. Configure classes by category and selector

This will override the default values in the design system

// awesome-css.config.js
module.exports = {
  backgrounds: {
    backgroundColor: {
      'red-500': 'red'
    }
  }
}

How we can build it

Defining the design system

We use two core configuration files in the library.

awesome-css.config.js

This file defines the core configuration, inspired by TailwindCSS. This file can be customized by the consumer to override values.

module.exports = {
  backgrounds: {
    backgroundColor: {
      'red-500': 'red'
    }
  }
}

awesome-css-classes.js This file defines a datastructure of the classes, consuming the config to build up the actual classes. This is not something exposed to the consumer, only used to produce the actual css and we can even run a script on it to create all the typing.

module.exports = (config) => ({
  'bg-red-500': {
    'background-color': config.backgrounds.backgroundColor['red-500']
  }
})

We are completely opinionated by the name of the classes. The reason is typing. We want the typing out of the box and not add complexity of producing types based on custom config by the user.

Consuming the design system in DEVELOPMENT (BABEL-PLUGIN)

  1. The plugin reads the awesome-css.confg.js file from the library and merges in any awesome-css.config.js defined in the root of the project, by the consumer
  2. The configuration is now passed into the function exposed by awesome-css-classes.js. The returned result is a datastructure describing the classes to be created
  3. The plugin now transforms this datastructure into classes with all possible pseudo selectors as well:
.bg-color-500 {
  background-color: red;
}

.hover:bg-color-500:hover {
  background-color: red;
}

.focus:bg-color-500:focus {
  background-color: red;
}
  1. The css is injected into the app and everything is available

That means during development of the project the developer can freely add any css class from the library and compose together how they want to use it in the app by using the core classnames and pseudo selector functions:

import { classnames, hover } from 'awesome-css'

classnames('bg-color-500', hover('bg-color-500'))

Consuming the design system in PRODUCTION (BABEL-PLUGIN)

The plugin follows the same first two steps as in development.

  1. The plugin reads the awesome-css.confg.js file from the library and merges in any awesome-css.config.js defined in the root of the project, by the consumer
  2. The configuration is now passed into the function exposed by awesome-css-classes.js. The returned result is a datastructure describing the classes to be created

But then it gets really smart about it:

  1. The plugin now traverses the codebase and finds actual classes used and with what pseudo selectors
  2. The plugin now produces the production CSS content by filtering out classes and pseudo selectors not being used by the app

That means with the following code:

classnames('bg-color-500')

it would only produce the classname .bg-red-500 {}, not .hover:bg-red-500:hover {} or .focus:bg-red-500:focus {}, as those pseudo selectors are not in the code. 5. Finally it will flatten all the static compositions, meaning that classnames('bg-red-500', hover('bg-red-500')) will become bg-red-500 hover:bg-red-500

Configuring the design system

If the consumer has added their own awesome-css.config.js the babel plugin will merge that into the core config, now building the development and production CSS based on that.

Custom classes

To give this low threshold and awesome experience we can not configure custom classes, but it really does not make any sense to do that. If you want custom classes... just create them. If you want them typed we can use the Overmind trick:

.my-custom-class {
  color: green;
}
// global.d.ts
declare module 'awesome-css' {
  type TClasses = 'my-custom-class' | TBaseClasses
}
@mattapperson
Copy link

Only thing I see missing here is the ability to pass in props to the component. So that a button might be red if a prop of danger=true is passed in... again like styled-props

@rstrom
Copy link

rstrom commented Jan 26, 2020

Love that you are going the babel plugin route here. I have been exploring some similar ideas (haven't gotten very far so far) generating tailwind classes as props. If you're interested to see how it works in practice take a look at the codesandbox here https://codesandbox.io/s/csx-tailwind-example-i3w16 (repo is https://github.com/rstrom/csx)

I have been itching for developer experience like this so would love to get behind this idea in any form. Some drawbacks I've found in my approach so far: 1. TypeScript seems to only recognize <~2000ish max props at a time 2. name clashes and ordering still don't work great. Overall though it's feels like a really clean approach and I like that :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment