Beyond prototyping with Tailwind CSS
Advanced strategies for production
Anish
George
Lead UI Developer
Built a design system in Angular + Tailwind
This site is in Remix + Tailwind + Vanilla-Extract
Why should you care?
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
What I'm assuming of you
Good with CSS
Maybe have tried Tailwind CSS before
Knowledgeable on either React or Angular. Or else some other component based fremework.
How I roll!
Slides available at
https:// ageorge.dev /talks/tailwindSome slides have callouts
Information 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.
Topics covered
Fundamentals of Tailwind CSS
Anti-patterns
Lean configuration
Cognitive load
Componentisation
Plugins
Theming with overrides
Dark mode
CSS-in-JS
Fundamentals of Tailwind
The quick version
Utility first fundamentals
1<button class="text-white text-lg bg-blue hover:bg-grey
2 rounded px-2 mb-2">
3 Click Me!
4</button>Each class basically maps to a single css property-value pair
Lots of css classes needed
1.text-white { color: white; }
2
3.text-lg { font-size: 1.25rem; }
4
5.bg-blue {
6 background-color: #3490dc;
7}
8
9.hover\:bg-grey:hover {
10 background-color: #e8e8e8;
11}
12
13.px-2 {
14 padding-left: 0.5rem;
15 padding-right: 0.5rem;
16}
17
18.mb-2 {
19 margin-bottom: 0.5rem;
20}How does Tailwind compare?
A typical CSS framework
1<button class="btn btn-primary">
2 Cick Me!
3</button>
4Individual 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
Tailwind CSS
1<button class="text-white text-lg bg-blue hover:bg-grey
2 rounded px-2 mb-2">
3 Click Me!
4</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
Finite values through configuration
1// tailwind.config.js
2
3export default {
4 // ...
5 content: ["./src/**/*.{html,js,jsx,ts,tsx}"]
6 theme: {
7 colors: {
8 transparent: "transparent",
9 white: "#ffffff",
10 purple: "#3f3cbb",
11 midnight: "#121063",
12 "bubble-gum": "#ff77e9",
13 },
14 spacing: {
15 1: "8px",
16 2: "12px",
17 3: "16px",
18 large: '32px',
19 massive: "70px",
20 },
21 },
22}1<!-- view.html -->
2
3<h1 class="mb-large text-purple">Hello world!</h1>
4<button class="p-3">Click Me!</button>
5
6<!-- p-10 and bg-blue are not valid classes, ignored -->
7<button class="p-10 bg-blue">Another button</button>These are foundations of a design system
Talk to your designer to fill this config!
Tailwind only generates the minimum CSS required
1<!-- view.html -->
2
3<h1 class="mb-large text-purple">Hello world!</h1>
4<button class="p-3">Click Me!</button>1/* styles.css */
2
3@tailwind base;
4@tailwind components;
5@tailwind utilities;1/* dist/styles.css */
2
3/* ... reset and other styles */
4.mb-large {
5 margin-bottom: 32px;
6}
7.p-3 {
8 padding: 16px;
9}
10.text-purple {
11 --tw-text-opacity: 1;
12 color: rgb(63 60 187 / var(--tw-text-opacity));
13}PostCSS magic
CSS file size stays to a minimum
The built css file gets the same hash if no new classes are used. This means it gets cached by the user's browser!
And that was the quick version!
Visit https://tailwindcss.com to learn more.
Anti-patterns with Tailwind CSS
The @apply
1/* style.css */
2
3.my-btn {
4 @apply text-lg text-white bg-blue-600 transition-colors;
5 @apply px-3 py-2 rounded;
6}
7
8.my-btn:hover {
9 @apply bg-blue-900;
10}1<!-- view.html -->
2
3<button class="my-btn">Click Me!</button>1/* dist/style.css */
2
3.my-btn {
4 --tw-bg-opacity: 1;
5 background-color: rgb(37 99 235 / var(--tw-bg-opacity));
6 font-size: 1.125rem;
7 line-height: 1.75rem;
8 --tw-text-opacity: 1;
9 color: rgb(255 255 255 / var(--tw-text-opacity));
10 transition-property: color, background-color, border-color,
11 text-decoration-color, fill, stroke;
12 transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
13 transition-duration: 0.15s;
14 border-radius: 0.25rem;
15 padding: 0.5rem 0.75rem;
16}
17.my-btn:hover {
18 --tw-bg-opacity: 1;
19 background-color: rgb(30 58 138 / var(--tw-bg-opacity));
20}When to use @apply
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
Arbitrary value notation
1<div>
2 <i class="fa-solid fa-magnifying-glass mt-[3px]"></i>
3 <span>Search</span>
4</div>1.mt-\[3px\] {
2 margin-top: 3px;
3}But...
1<div class="shadow-[0_35px_60px_-15px_rgba(0,0,0,0.3)]">
2 Oh my god, my eyes!!
3</div>strategy #1
Lean Configuration
Extending Tailwind configuration
1/* tailwind.config.js */
2
3export default {
4 // ...
5 theme: {
6 extend: {
7 colors: {
8 purple: "#3f3cbb",
9 midnight: "#121063",
10 },
11 },
12 },
13};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
Overriding Tailwind value sets
1/* tailwind.config.js */
2
3export default {
4 // ...
5 theme: {
6 spacing: {
7 1: "8px",
8 2: "12px",
9 3: "16px",
10 large: "32px",
11 massive: "70px",
12 },
13 // ... non-specified sets picked up
14 // from tailwind defaults
15 },
16};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.
A good spacing scale?
Tailwind default
ageorge.dev
Disable core plugins
1/* tailwind.config.js */
2
3export default {
4 // ...
5 corePlugins: {
6 float: false, // only modern layouts please
7 zIndex: false, // we don't mess with z-index
8 animation: false // will be custom anyway
9 },
10};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
an important bit...
Cognitive load with Tailwind
With tailwind you need to remember 2 syntaxes
1.imaginary-selector {
2
3 color: hsl(83, 27%, 53%);
4 font-weight: bold;
5 text-decoration: underline;
6 padding: 0.5rem;
7 border: 1px solid hsl(83, 27%, 53%);
8 display: flex;
9 justify-content: center;
10
11}1<div class="text-primary-200 font-bold underline p-2
2 border border-primary-200 flex items-center">
3 <!-- -->
4</div>We are adding mental steps to the development process
Things get more complicated if we have heavily customised the configuration
How do we deal with this?
Step 1/2: Create components!
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
Step 2/2: Solid documentation through Storybook
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...
Not a silver bullet
Over time, devs adapt to it, but the cost is present
strategy #2
DRY tailwind styles
What do you do about this?
1<div className="max-w-sm rounded overflow-hidden shadow-lg">
2 <img className="w-full" src="http://dummy.com/img.jpeg" alt="User Avatar" />
3 <div className="px-6 py-4">
4 <h3 className="font-bold text-xl mb-3">
5 John Doe
6 </h3>
7 <p className="text-gray-700 text-base mb-2">
8 Professional Web developer with several years of experience
9 </p>
10 <p className="text-gray-700 text-base">
11 Loves basketball
12 </p>
13 </div>
14 <div className="px-6 py-4 flex gap-2">
15 <span className="bg-gray-200 rounded-full px-3 py-1 text-sm text-gray-700">
16 Web Developer
17 </span>
18 <span className="bg-gray-200 rounded-full px-3 py-1 text-sm text-gray-700">
19 UI/UX Designer
20 </span>
21 </div>
22</div>Maintaining strings in Javascript
1// DO NOT BREAK UP TAILWIND CLASS NAMES
2
3const CommonStyles = "text-2xl bg-primary-100";
4
5const MoreStyles = cls("shadow-sm px-2", otherStyles);
6
7const Variants = {
8 primary: 'bg-primary-300 hover:bg-primary-400',
9 secondary: 'bg-secondary-200 hover:text-white'
10}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.
Componentisation
1export function Card({
2 className, ...otherProps
3}: React.HTMLProps<HTMLDivElement>) {
4 return (
5 <div
6 {...otherProps}
7
8 className={`shadow border-gray border-2 mb-4
9 ${className ?? ''}`}
10 />
11 );
12}1@Directive({
2 selector: '[appCard]',
3})
4export class CardDirective {
5 @HostBinding('class') classes
6 = 'shadow border-gray border-2 mb-4';
7}1<section class='bg-grey' appCard>Notes</section>Looks better already
1<Card>
2 <img className="w-full" src="http://dummy.com/img.jpeg" alt="User Avatar" />
3 <div className="px-6 py-4">
4 <Heading2 className='mb-3'>John Doe</Heading2>
5 <Body strength="subtle" className='mb-2'>
6 Professional Web developer with several years of experience
7 </Body>
8 <Body strength="subtle">
9 Loves basketball
10 </Body>
11 </div>
12 <div className="px-6 py-4 flex gap-2">
13 <SkillBadge>
14 Web Developer
15 </SkillBadge>
16 <SkillBadge>
17 UI/UX Designer
18 </SkillBadge>
19 <SkillBadge>Photographer</SkillBadge>
20 </div>
21</Card>Best of both worlds
1type Heading1Props = React.PropsWithChildren &
2 React.HTMLAttributes<HTMLHeadingElement>;
3
4function Heading1({
5 className, children, ...otherProps
6}: Heading1Props) {
7 return (
8 <h1 {...otherProps}
9 className={Heading1.classes + ' ' + (className ?? '') }
10 >
11 {children}
12 </h1>
13 );
14}
15
16Heading1.classes = 'text-2xl font-bold md:text-3xl';1<Heading1>Hello There</Heading1>
2
3<section
4 className={Heading1.classes + ' bg-blue-100 rounded'}>
5
6 <p>A section with common styling</p>
7 <span>Pretty nifty</p>
8</section>Similar can be somewhat achieved in Angular using
static fields in Components/Directives
strategy #3
Plugins
Understanding @layer
1@tailwind base;
2@tailwind components;
3@tailwind utilities;
4
5@layer components {
6 .layout-master {
7 grid-template-columns: minmax(200px, 2fr) 8fr;
8 grid-template-rows: 100px 1fr 50px;
9 grid-template-areas: "header header"
10 "sidebar main"
11 "footer footer";
12 }
13
14 .layout-master-stacked {
15 grid-template-areas: "header"
16 "sidebar"
17 "main"
18 "footer";
19 }
20
21 .area-header { grid-area: header; }
22 .area-main { grid-area: main; }
23}@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<section class="grid layout-master-stacked lg:layout-master">
2 <header class="area-header">header</header>
3 <section class="area-main">main</section>
4 <aside class="area-sidebar">sidebar</aside>
5 <footer class="area-footer">footer</footer>
6</section>Understanding plugins
1/* tailwind.config.js */
2
3const plugin = require('tailwindcss/plugin')
4
5module.exports = {
6 plugins: [
7 plugin(function({ addUtilities, addComponents, e, config }) {
8 // Add your custom styles here
9 }),
10 ]
11}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.
A plugin for typography
1plugin(({ addBase, theme }) => {
2 addBase({
3 '.typo-h1': {
4 fontSize: theme('fontSize.4xl'),
5 fontFamily: theme('fontFamily.serif'),
6 },
7 '.typo-h2': {
8 fontSize: theme('fontSize.3xl'),
9 fontFamily: theme('fontFamily.serif'),
10 letterSpacing: '0.05em',
11 },
12 '.typo-body': {
13 fontSize: theme('fontSize.base'),
14 },
15 '.typo-interface': {
16 fontSize: theme('fontSize.sm'),
17 textTransform: 'uppercase',
18 }
19 })
20})1<span class="typo-h2 lg:typo-h1">
2 Some content
3</span>Your utilties can work with Tailwind modifiers!
Creating your own modifiers
1plugin(({ addVariant }) => {
2 // Overwrite the default invalid:* modifier to support angular classes
3 addVariant('invalid', ['&.ng-invalid.ng-touched', '&:invalid']);
4
5 // For group and peer modifiers
6 addVariant('group-invalid', [':merge(.group).ng-invalid.ng-touched &', ':merge(.group):invalid &']);
7 addVariant('peer-invalid', [':merge(.peer).ng-invalid.ng-touched ~ &', ':merge(.peer):invalid ~ &']);
8});
91<app-custom-form-control class="invalid:ring-red-500" ></app-custom-form-control>match* plugins
1const gridPlugin = plugin(({ matchComponents, theme }) => {
2 matchComponents(
3 {
4 layout: (value) => ({
5 gridTemplateAreas: value.areas
6 .map((area) => `"${area}"`).join(" "),
7 gridTemplateColumns: value.columns,
8 gridTemplateRows: value.rows,
9 }),
10 },
11 { values: theme("layouts") }
12 );
13});1plugins: [gridPlugin],
2theme: {
3 layouts: {
4 master: {
5 areas: [
6 "header header",
7 "sidebar main",
8 "footer footer"
9 ],
10 columns: "minmax(200px, 2fr) 8fr",
11 rows: "100px 1fr 50px",
12 },
13 ["master-stacked"]: {
14 areas: [
15 "header", "sidebar",
16 "main", "footer"
17 ],
18 },
19 },
20},What to use?
@layer styles
If you are in a single application and need to build simple plugins
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
strategy #4
Theming using overrides
The problem with plain colors
1/* tailwind.config.js */
2
3export default {
4 theme: {
5 colors: {
6 primary: {
7 DEFAULT: "#5B8C5A",
8 light: "#9CBF9B",
9 dark: "#406440",
10 },
11 secondary: {
12 DEFAULT: "#E3655B",
13 light: "#ED9C96",
14 dark: "#DC392E",
15 }
16 }
17 },
18};Inflexible
You can't change colors at run-time.
Side note: HSL rocks!
1.selector {
2 /* hsl(<hue> <saturation> <lightness>) */
3 color: hsl(205 30% 90%)
4
5 /* hsl(<hue> <saturation> <lightness> / <alpha-value>) */
6 background: hsl(205 30% 90% / .4)
7}How to make Tailwind more flexible?
1/* styles.css */
2@layer base {
3 :root {
4 --color-primary-light: hsl(321 12% 48%);
5 --color-primary-base: hsl(321 12% 29%);
6 --color-primary-dark: hsl(119 22% 44%);
7 }
8}1/* tailwind.config.js */
2
3export default {
4 theme: {
5 colors: {
6 primary: {
7 DEFAULT: "var(--color-primary-base)",
8 light: "var(--color-primary-light)",
9 dark: "var(--color-primary-dark)",
10 },
11 secondary: {
12 DEFAULT: "var(--color-secondary-base)",
13 light: "var(--color-secondary-light)",
14 dark: "var(--color-secondary-dark)",
15 }
16 }
17 },
18};Using fallbacks for more flexibility
1primary: {
2 DEFAULT: "var(--color-primary-base, hsl(321 12% 29%))",
3 light: "var(--color-primary-light, hsl(321 12% 48%))",
4 dark: "var(--color-primary-dark, hsl(119 22% 44%))",
5}About color opacity modifiers
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!
We can make use of color channels!
1:root {
2 --color-primary-base: 207 49% 65%;
3}You actually might want to hardcode opacities into colors instead
Talk to your designer!
1primary: {
2 DEFAULT: "hsl( var(--color-primary-base) / <alpha-value>)",
3}strategy #5
Dark mode
Full Tailwind support
1/* tailwind.config.js */
2export default {
3 darkMode: 'class'
4 // ... rest of the config
5}1<html class="dark">
2<!-- ... -->
3</html>1<button class="bg-primary-100 dark:bg-primary-800 dark:text-white">
2 Click me
3</button>We can do better
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
All together now...
1:root {
2 --rc-parchment-500: hsl(39 100% 89%);
3 --rc-timber-300: hsl(189, 31%, 13%);
4
5 --rc-p-accent-300: hsl(83 27% 53%);
6 --rc-p-accent-500: hsl(83 49% 30%);
7
8 --rc-d-neutral-500: hsl(189 67% 6%);
9 --rc-l-neutral-500: hsl(45 100% 99%);
10}1:root {
2 --page-far: var(--rc-parchment-500);
3 --accent: var(--rc-p-accent-500);
4 --neutral: var(--rc-d-neutral-500);
5 --neutral-inverse: var(--rc-l-neutral-500);
6}1:root.dark {
2 --page-far: var(--rc-timber-300);
3 --accent: var(--rc-p-accent-300);
4 --neutral: var(--rc-l-neutral-500);
5 --neutral-inverse: var(--rc-d-neutral-500);
6}and a straightforward Tailwind config
1/* tailwind.config.js */
2
3export default {
4 theme: {
5 colors: {
6 'cc-page-far': 'var(--page-far)',
7 'cc-accent': 'var(--accent)',
8 'cc-neutral': 'var(--neutral)',
9 //... others
10 }
11 }
12}1<section class="bg-page-far text-neutral">
2 <button class="bg-accent">Click me</button>
3</section>The biggest problem with Tailwind
It allows all combinations!
1<button class="text-page-far">Click me</button>
2<div class="bg-neutral">
3 <!-- other stuff -->
4</div>a final word on colors
If it gets any more complicated, don't use Tailwind's color system
strategy #6
Tailwind CSS + CSS-in-JS
Why?...
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
Tailwind approved support
1import resolveConfig from 'tailwindcss/resolveConfig'
2import tailwindConfig from './tailwind.config.js'
3
4const fullConfig = resolveConfig(tailwindConfig)
5
6fullConfig.theme.width[4]
7// => '1rem'
8
9fullConfig.theme.screens.md
10// => '768px'Using this includes a lot of tailwind dependencies into the client bundle
There are ways around this, but be careful
What if we don't want Tailwind to
own our Design Tokens?
A basic token pipeline
Prior to v3.3, the tokens file needs to be commonJS
Post v3.3, tailwind supports ESM config file
bonus
A CSS-in-JS library that works well with Tailwind CSS...
Vanilla Extract
Final thoughts
I am a CSS expert, I don't need Tailwind!
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...
Tailwind is not a CSS framework..
it is a framework to build your own CSS framework
that's all folks!
Thank You
https://ageorge.dev/talks/tailwind