Advanced strategies for production
Built a design system in Angular + Tailwind
This site is in Remix + Tailwind + Vanilla-Extract
Tailwind has been the subject of much controversy and you are curious!
You want/need/have to use it...
Tailwind has pretty much "won" the CSS-framework contest
Good with CSS
Maybe have tried Tailwind CSS before
Knowledgeable on either React or Angular. Or else some other component based fremework.
Slides available at
https:// ageorge.dev /talks/tailwindInformation presented is more relevant to UX/VD professionals.
Advanced or Niche topic. Safe to skip.
My subjective opinion. I may be utterly wrong about this.
Fundamentals of Tailwind CSS
Anti-patterns
Lean configuration
Cognitive load
Componentisation
Plugins
Theming with overrides
Dark mode
CSS-in-JS
The quick version
1 2 3 4
<button class="text-white text-lg bg-blue hover:bg-grey rounded px-2 mb-2"> Click Me! </button>
Each class basically maps to a single css property-value pair
Lots of css classes needed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
.text-white { color: white; } .text-lg { font-size: 1.25rem; } .bg-blue { background-color: #3490dc; } .hover\:bg-grey:hover { background-color: #e8e8e8; } .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; } .mb-2 { margin-bottom: 0.5rem; }
1 2 3
<button class="btn btn-primary"> Cick Me! </button>
Individual classes encapsulate full sets of styles
Need both HTML and CSS to author the UI
Clean HTML
Styles hidden away
Only layout based CSS skills needed
1 2 3 4
<button class="text-white text-lg bg-blue hover:bg-grey rounded px-2 mb-2"> Click Me! </button>
Classes are atomic. Each describes a specific CSS property-value pair
HTML becomes the authoring language
HTML overloaded with styling
Styles co-located with markup
CSS skills required. It is just a one-to-one mapping of CSS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// tailwind.config.js export default { // ... content: ["./src/**/*.{html,js,jsx,ts,tsx}"] theme: { colors: { transparent: "transparent", white: "#ffffff", purple: "#3f3cbb", midnight: "#121063", "bubble-gum": "#ff77e9", }, spacing: { 1: "8px", 2: "12px", 3: "16px", large: '32px', massive: "70px", }, }, }
1 2 3 4 5 6 7
<!-- view.html --> <h1 class="mb-large text-purple">Hello world!</h1> <button class="p-3">Click Me!</button> <!-- p-10 and bg-blue are not valid classes, ignored --> <button class="p-10 bg-blue">Another button</button>
These are foundations of a design system
Talk to your designer to fill this config!
1 2 3 4
<!-- view.html --> <h1 class="mb-large text-purple">Hello world!</h1> <button class="p-3">Click Me!</button>
1 2 3 4 5
/* styles.css */ @tailwind base; @tailwind components; @tailwind utilities;
1 2 3 4 5 6 7 8 9 10 11 12 13
/* dist/styles.css */ /* ... reset and other styles */ .mb-large { margin-bottom: 32px; } .p-3 { padding: 16px; } .text-purple { --tw-text-opacity: 1; color: rgb(63 60 187 / var(--tw-text-opacity)); }
PostCSS magic
The built css file gets the same hash if no new classes are used. This means it gets cached by the user's browser!
Visit https://tailwindcss.com to learn more.
1 2 3 4 5 6 7 8 9 10
/* style.css */ .my-btn { @apply text-lg text-white bg-blue-600 transition-colors; @apply px-3 py-2 rounded; } .my-btn:hover { @apply bg-blue-900; }
1 2 3
<!-- view.html --> <button class="my-btn">Click Me!</button>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/* dist/style.css */ .my-btn { --tw-bg-opacity: 1; background-color: rgb(37 99 235 / var(--tw-bg-opacity)); font-size: 1.125rem; line-height: 1.75rem; --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 0.15s; border-radius: 0.25rem; padding: 0.5rem 0.75rem; } .my-btn:hover { --tw-bg-opacity: 1; background-color: rgb(30 58 138 / var(--tw-bg-opacity)); }
The @apply directive is not a convenience construct. It has repurcussions
Use sparingly
When writing complicated styles for psuedo selectors in one-off scenarios
Creating small highly re-usable classes if creating components or plugins not a great option
1 2 3 4
<div> <i class="fa-solid fa-magnifying-glass mt-[3px]"></i> <span>Search</span> </div>
1 2 3
.mt-\[3px\] { margin-top: 3px; }
But...
1 2 3
<div class="shadow-[0_35px_60px_-15px_rgba(0,0,0,0.3)]"> Oh my god, my eyes!! </div>
1 2 3 4 5 6 7 8 9 10 11 12 13
/* tailwind.config.js */ export default { // ... theme: { extend: { colors: { purple: "#3f3cbb", midnight: "#121063", }, }, }, };
Allows adding onto the existing Tailwind values
Easy for devs to use non-compliant design
Tailwind tends to have too many values
Look at their colors and spacing scale
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/* tailwind.config.js */ export default { // ... theme: { spacing: { 1: "8px", 2: "12px", 3: "16px", large: "32px", massive: "70px", }, // ... non-specified sets picked up // from tailwind defaults }, };
Limits the possible values to a strict design system
You are free to choose more meaninful property names
The tailwind documentation becomes partially useless,
you will need your own documentation describing all possible properties
Storybook can be a good solution
The earlier you move to custom sets, the better
Pro tip: Get a strong spacing scale from your designer before it is too late.
The Tailwind default is trash.
1 2 3 4 5 6 7 8 9 10
/* tailwind.config.js */ export default { // ... corePlugins: { float: false, // only modern layouts please zIndex: false, // we don't mess with z-index animation: false // will be custom anyway }, };
Core plugins provide the various css features that Tailwind supports
You can disable any of these plugins to stop tailwind from generating those classes
Allows you to restrict to CSS best practices and reduce confusion
You also have the option of disabling all core plugins and use Tailwind as an
engine to drive your plugins. More on that later
It is a good idea to prune out CSS practices you don't need
1 2 3 4 5 6 7 8 9 10 11
.imaginary-selector { color: hsl(83, 27%, 53%); font-weight: bold; text-decoration: underline; padding: 0.5rem; border: 1px solid hsl(83, 27%, 53%); display: flex; justify-content: center; }
1 2 3 4
<div class="text-primary-200 font-bold underline p-2 border border-primary-200 flex items-center"> <!-- --> </div>
We are adding mental steps to the development process
Things get more complicated if we have heavily customised the configuration
This reduces the amount of Tailwind most developers need to work on
If you have a strong enough design system with components, most devs only need to learn the layout classes in Tailwind
A heavily customised config makes the Tailwind docs partially useless
Use Storybook to create documentation which allows devs to peruse and disover the design tokens available
Let me show you what I mean...
Over time, devs adapt to it, but the cost is present
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<div className="max-w-sm rounded overflow-hidden shadow-lg"> <img className="w-full" src="http://dummy.com/img.jpeg" alt="User Avatar" /> <div className="px-6 py-4"> <h3 className="font-bold text-xl mb-3"> John Doe </h3> <p className="text-gray-700 text-base mb-2"> Professional Web developer with several years of experience </p> <p className="text-gray-700 text-base"> Loves basketball </p> </div> <div className="px-6 py-4 flex gap-2"> <span className="bg-gray-200 rounded-full px-3 py-1 text-sm text-gray-700"> Web Developer </span> <span className="bg-gray-200 rounded-full px-3 py-1 text-sm text-gray-700"> UI/UX Designer </span> </div> </div>
1 2 3 4 5 6 7 8 9 10
// DO NOT BREAK UP TAILWIND CLASS NAMES const CommonStyles = "text-2xl bg-primary-100"; const MoreStyles = cls("shadow-sm px-2", otherStyles); const Variants = { primary: 'bg-primary-300 hover:bg-primary-400', secondary: 'bg-secondary-200 hover:text-white' }
Manage the tailwind classes in javascript like regular strings
Make sure you don't split the class names
Works pretty well in React
Works passably well in Angular. Bit more awkward getting the strings onto
the templates since they need to be part of the component class.
1 2 3 4 5 6 7 8 9 10 11 12
export function Card({ className, ...otherProps }: React.HTMLProps<HTMLDivElement>) { return ( <div {...otherProps} className={`shadow border-gray border-2 mb-4 ${className ?? ''}`} /> ); }
1 2 3 4 5 6 7
@Directive({ selector: '[appCard]', }) export class CardDirective { @HostBinding('class') classes = 'shadow border-gray border-2 mb-4'; }
1
<section class='bg-grey' appCard>Notes</section>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<Card> <img className="w-full" src="http://dummy.com/img.jpeg" alt="User Avatar" /> <div className="px-6 py-4"> <Heading2 className='mb-3'>John Doe</Heading2> <Body strength="subtle" className='mb-2'> Professional Web developer with several years of experience </Body> <Body strength="subtle"> Loves basketball </Body> </div> <div className="px-6 py-4 flex gap-2"> <SkillBadge> Web Developer </SkillBadge> <SkillBadge> UI/UX Designer </SkillBadge> <SkillBadge>Photographer</SkillBadge> </div> </Card>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
type Heading1Props = React.PropsWithChildren & React.HTMLAttributes<HTMLHeadingElement>; function Heading1({ className, children, ...otherProps }: Heading1Props) { return ( <h1 {...otherProps} className={Heading1.classes + ' ' + (className ?? '') } > {children} </h1> ); } Heading1.classes = 'text-2xl font-bold md:text-3xl';
1 2 3 4 5 6 7 8
<Heading1>Hello There</Heading1> <section className={Heading1.classes + ' bg-blue-100 rounded'}> <p>A section with common styling</p> <span>Pretty nifty</p> </section>
Similar can be somewhat achieved in Angular using
static fields in Components/Directives
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
@tailwind base; @tailwind components; @tailwind utilities; @layer components { .layout-master { grid-template-columns: minmax(200px, 2fr) 8fr; grid-template-rows: 100px 1fr 50px; grid-template-areas: "header header" "sidebar main" "footer footer"; } .layout-master-stacked { grid-template-areas: "header" "sidebar" "main" "footer"; } .area-header { grid-area: header; } .area-main { grid-area: main; } }
@layer moves your css into the right spot within tailwind stylesheet
@layer can only be used within the context of the core stylesheet
Classes in @layer only added to stylsheet if used and they work with modifiers
1 2 3 4 5 6
<section class="grid layout-master-stacked lg:layout-master"> <header class="area-header">header</header> <section class="area-main">main</section> <aside class="area-sidebar">sidebar</aside> <footer class="area-footer">footer</footer> </section>
1 2 3 4 5 6 7 8 9 10 11
/* tailwind.config.js */ const plugin = require('tailwindcss/plugin') module.exports = { plugins: [ plugin(function({ addUtilities, addComponents, e, config }) { // Add your custom styles here }), ] }
Tailwind is built out of plugins
You can create your own plugins to enable your own kind of CSS generation
There are different types of plugins
We will only focus on a couple. Read the docs for more.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
plugin(({ addBase, theme }) => { addBase({ '.typo-h1': { fontSize: theme('fontSize.4xl'), fontFamily: theme('fontFamily.serif'), }, '.typo-h2': { fontSize: theme('fontSize.3xl'), fontFamily: theme('fontFamily.serif'), letterSpacing: '0.05em', }, '.typo-body': { fontSize: theme('fontSize.base'), }, '.typo-interface': { fontSize: theme('fontSize.sm'), textTransform: 'uppercase', } }) })
1 2 3
<span class="typo-h2 lg:typo-h1"> Some content </span>
Your utilties can work with Tailwind modifiers!
1 2 3 4 5 6 7 8
plugin(({ addVariant }) => { // Overwrite the default invalid:* modifier to support angular classes addVariant('invalid', ['&.ng-invalid.ng-touched', '&:invalid']); // For group and peer modifiers addVariant('group-invalid', [':merge(.group).ng-invalid.ng-touched &', ':merge(.group):invalid &']); addVariant('peer-invalid', [':merge(.peer).ng-invalid.ng-touched ~ &', ':merge(.peer):invalid ~ &']); });
1
<app-custom-form-control class="invalid:ring-red-500" ></app-custom-form-control>
1 2 3 4 5 6 7 8 9 10 11 12 13
const gridPlugin = plugin(({ matchComponents, theme }) => { matchComponents( { layout: (value) => ({ gridTemplateAreas: value.areas .map((area) => `"${area}"`).join(" "), gridTemplateColumns: value.columns, gridTemplateRows: value.rows, }), }, { values: theme("layouts") } ); });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
plugins: [gridPlugin], theme: { layouts: { master: { areas: [ "header header", "sidebar main", "footer footer" ], columns: "minmax(200px, 2fr) 8fr", rows: "100px 1fr 50px", }, ["master-stacked"]: { areas: [ "header", "sidebar", "main", "footer" ], }, }, },
If you are in a single application and need to build simple plugins
If you in a MonoRepo and need to support multiple projects
Tailwind presets allow you to share entire tailwind configurations including the plugins across projects easily
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/* tailwind.config.js */ export default { theme: { colors: { primary: { DEFAULT: "#5B8C5A", light: "#9CBF9B", dark: "#406440", }, secondary: { DEFAULT: "#E3655B", light: "#ED9C96", dark: "#DC392E", } } }, };
You can't change colors at run-time.
1 2 3 4 5 6 7
.selector { /* hsl(<hue> <saturation> <lightness>) */ color: hsl(205 30% 90%) /* hsl(<hue> <saturation> <lightness> / <alpha-value>) */ background: hsl(205 30% 90% / .4) }
1 2 3 4 5 6 7 8
/* styles.css */ @layer base { :root { --color-primary-light: hsl(321 12% 48%); --color-primary-base: hsl(321 12% 29%); --color-primary-dark: hsl(119 22% 44%); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/* tailwind.config.js */ export default { theme: { colors: { primary: { DEFAULT: "var(--color-primary-base)", light: "var(--color-primary-light)", dark: "var(--color-primary-dark)", }, secondary: { DEFAULT: "var(--color-secondary-base)", light: "var(--color-secondary-light)", dark: "var(--color-secondary-dark)", } } }, };
1 2 3 4 5
primary: { DEFAULT: "var(--color-primary-base, hsl(321 12% 29%))", light: "var(--color-primary-light, hsl(321 12% 48%))", dark: "var(--color-primary-dark, hsl(119 22% 44%))", }
1
<div class='bg-primary-dark/30 text-neutral/60'>...</div>
This fails with our vars setup!
It is debatable whether color opacity is a good practice from a design perspective
Talk to your designer!
1 2 3
:root { --color-primary-base: 207 49% 65%; }
You actually might want to hardcode opacities into colors instead
Talk to your designer!
1 2 3
primary: { DEFAULT: "hsl( var(--color-primary-base) / <alpha-value>)", }
1 2 3 4 5
/* tailwind.config.js */ export default { darkMode: 'class' // ... rest of the config }
1 2 3
<html class="dark"> <!-- ... --> </html>
1 2 3
<button class="bg-primary-100 dark:bg-primary-800 dark:text-white"> Click me </button>
Raw Colors
Parchment
Timber
Accent
Neutral
Contextual Colors
Light Mode
Page far
Page near
Accent
Neutral text
Inverse text
Dark Mode
Page far
Page near
Accent
Neutral text
Inverse text
1 2 3 4 5 6 7 8 9 10
:root { --rc-parchment-500: hsl(39 100% 89%); --rc-timber-300: hsl(189, 31%, 13%); --rc-p-accent-300: hsl(83 27% 53%); --rc-p-accent-500: hsl(83 49% 30%); --rc-d-neutral-500: hsl(189 67% 6%); --rc-l-neutral-500: hsl(45 100% 99%); }
1 2 3 4 5 6
:root { --cc-page-far: var(--rc-parchment-500); --cc-accent: var(--rc-p-accent-500); --cc-neutral: var(--rc-d-neutral-500); --cc-neutral-inverse: var(--rc-l-neutral-500); }
1 2 3 4 5 6
:root.dark { --cc-page-far: var(--rc-timber-300); --cc-accent: var(--rc-p-accent-300); --cc-neutral: var(--rc-l-neutral-500); --cc-neutral-inverse: var(--rc-d-neutral-500); }
1 2 3 4 5 6 7 8 9 10 11 12
/* tailwind.config.js */ export default { theme: { colors: { 'cc-page-far': 'var(--cc-page-far)', 'cc-accent': 'var(--cc-accent)', 'cc-neutral': 'var(--cc-neutral)', //... others } } }
1 2 3
<section class="bg-cc-page-far text-cc-neutral"> <button class="bg-cc-accent">Click me</button> </section>
It allows all combinations!
1 2 3 4
<button class="text-cc-page-far">Click me</button> <div class="bg-cc-neutral"> <!-- other stuff --> </div>
Let application logic manipulate CSS safely using design system tokens
Migrating to tailwind from an existing system
Some things are better done without Tailwind?
You want the flexibility
1 2 3 4 5 6 7 8 9 10
import resolveConfig from 'tailwindcss/resolveConfig' import tailwindConfig from './tailwind.config.js' const fullConfig = resolveConfig(tailwindConfig) fullConfig.theme.width[4] // => '1rem' fullConfig.theme.screens.md // => '768px'
Using this includes a lot of tailwind dependencies into the client bundle
There are ways around this, but be careful
Prior to v3.3, the tokens file needs to be commonJS
Post v3.3, tailwind supports ESM config file
Will your docs be as comprehensive?
Will build tools automatically exclude unused styles?
Will your dev team already have experience with your CSS approach?
In fact, most of the time, we build an inferior Tailwind ourselves...
https://ageorge.dev/talks/tailwind