Styling with Tailwind
Tailwind is a set of small CSS classes you combine in your markup instead of writing CSS files. This lesson is a complete working reference — you should not need to leave it to style anything in this course.
The idea
<button className="rounded-lg bg-emerald-600 px-4 py-2 font-medium text-white hover:bg-emerald-700">
Add to cart
</button>
Each class does one thing: rounded-lg sets a border radius, px-4 sets
horizontal padding, bg-emerald-600 sets a background colour.
The first reaction is that this is ugly, and it is noisier than a class name
like .btn-primary. What you get for it:
Styles cannot leak. There is no global stylesheet where a rule from another page overrides yours.
Deleting a component deletes its styles. No orphaned CSS accumulating for years.
No naming. A large share of CSS effort goes into inventing class names, and this removes it.
Consistency by default. p-4 is always 16px. Nobody types padding: 15px
by accident, so a design stays even without discipline.
The cost is verbose markup, and the answer to that is components — you write the button once.
The scale
Almost every spacing and sizing number is a step on one scale. 1 unit = 0.25rem = 4px.
| Class | Value |
|---|---|
p-0 |
0 |
p-0.5 |
2px |
p-1 |
4px |
p-2 |
8px |
p-3 |
12px |
p-4 |
16px |
p-6 |
24px |
p-8 |
32px |
p-12 |
48px |
p-16 |
64px |
Knowing 4 = 16px lets you work out any of them.
Spacing
| Prefix | Sets |
|---|---|
p-* |
padding, all sides |
px-* py-* |
padding horizontal / vertical |
pt-* pr-* pb-* pl-* |
padding one side |
m-* mx-* mt-* … |
margin, same pattern |
-mt-4 |
negative margin |
gap-* |
gap between flex or grid children |
space-y-4 |
vertical space between children |
Prefer gap over margins in flex and grid layouts. Margins collapse, need
clearing on the last item, and fight you; gap does not.
Layout
<div className="flex items-center justify-between gap-4">
<div className="grid grid-cols-3 gap-6">
| Class | Does |
|---|---|
flex |
flex row |
flex-col |
flex column |
items-center |
cross-axis centre |
items-start items-end |
cross-axis start / end |
justify-center |
main-axis centre |
justify-between |
space between |
flex-1 |
grow to fill |
shrink-0 |
never shrink |
flex-wrap |
allow wrapping |
grid |
grid |
grid-cols-4 |
four equal columns |
col-span-2 |
span two columns |
hidden |
display: none |
block inline-block |
display |
shrink-0 matters more than it looks — a flex child containing a long word will
squash its siblings without it.
Sizing
| Class | Does |
|---|---|
w-full h-full |
100% |
w-screen h-screen |
viewport |
w-4 h-10 |
the spacing scale |
max-w-7xl |
80rem — a typical page width |
min-h-screen |
at least full height |
aspect-square |
1:1 |
size-10 |
width and height together |
Colour
The pattern is {property}-{colour}-{shade}, with shades from 50 (lightest) to
950 (darkest).
bg-emerald-600 text-gray-900 border-gray-200
| Use | Typical |
|---|---|
| Page background | bg-white, bg-gray-50 |
| Body text | text-gray-900 |
| Secondary text | text-gray-600 |
| Muted text | text-gray-500 |
| Borders | border-gray-200 |
| Primary action | bg-emerald-600 |
| Destructive | bg-red-600, text-red-600 |
| Success | text-emerald-600 |
Opacity with a slash: bg-black/50, text-white/70.
Keep to a small set. Three greys, one brand colour and red for errors covers most of a shop. A palette that grows uncontrolled is how interfaces start looking untidy.
Text
| Class | Value |
|---|---|
text-xs |
12px |
text-sm |
14px |
text-base |
16px |
text-lg |
18px |
text-xl |
20px |
text-2xl |
24px |
text-3xl |
30px |
font-medium font-semibold font-bold |
weight |
text-center text-right |
alignment |
leading-tight leading-relaxed |
line height |
truncate |
one line with an ellipsis |
line-clamp-2 |
two lines then ellipsis |
tabular-nums |
equal-width digits |
line-clamp-2 is what keeps product titles from breaking a grid. tabular-nums
stops prices jittering in a list.
Borders, corners, shadows
| Class | Does |
|---|---|
border |
1px, all sides |
border-2 border-t |
width / one side |
rounded rounded-lg rounded-xl |
4 / 8 / 12px |
rounded-full |
pill or circle |
shadow-sm shadow shadow-lg |
elevation |
ring-1 ring-gray-200 |
outline that does not affect layout |
ring is better than border for focus states, because adding a border changes
an element's size and shifts the layout.
State and responsive prefixes
Any class can be prefixed:
<button className="bg-emerald-600 hover:bg-emerald-700 focus-visible:ring-2 disabled:opacity-50">
| Prefix | When |
|---|---|
hover: |
pointer over it |
focus: |
focused |
focus-visible: |
focused by keyboard only |
active: |
being pressed |
disabled: |
disabled |
group-hover: |
a parent with group is hovered |
sm: |
≥ 640px |
md: |
≥ 768px |
lg: |
≥ 1024px |
xl: |
≥ 1280px |
dark: |
dark mode |
Breakpoints are minimum widths, so unprefixed classes are the mobile styles.
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 lg:gap-6">
Two columns on a phone, three on a tablet, four on a laptop. This is mobile-first and it is the right way round: most of your customers are on a phone.
Use focus-visible: rather than focus: for rings, so they appear for keyboard
users and not on every mouse click.
Conditional classes
<button className={`rounded-lg px-4 py-2 ${isActive ? "bg-emerald-600 text-white" : "bg-gray-100 text-gray-700"}`}>
Template literals work and get unreadable quickly. clsx is the standard fix:
npm install clsx --workspace=apps/web
import clsx from "clsx";
<button
className={clsx(
"rounded-lg px-4 py-2 font-medium",
isActive && "bg-emerald-600 text-white",
!isActive && "bg-gray-100 text-gray-700",
disabled && "cursor-not-allowed opacity-50"
)}
>
Falsy values are dropped. Note isActive && here is safe because these are
strings, not numbers — the JSX 0 trap does not apply inside clsx.
Never build class names dynamically
<div className={`bg-${colour}-600`} /> // produces nothing
Tailwind scans your source as text at build time and generates only the
classes it literally finds. bg-${colour}-600 never appears as a string, so
that CSS is never generated.
Map to complete class names instead:
const STYLES = {
success: "bg-emerald-100 text-emerald-800",
error: "bg-red-100 text-red-800",
} as const;
<span className={STYLES[variant]} />
This is the single most common Tailwind bug. The symptom is a style that works in development and vanishes in production, or simply never applies.
Custom values
<div className="w-[347px] bg-[#1f2937] grid-cols-[200px_1fr]" />
Square brackets take an arbitrary value. Use them sparingly — a design of arbitrary values has lost the consistency that was the point.
For anything reused, put it in the config instead:
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
brand: { DEFAULT: "#059669", dark: "#047857" },
},
},
},
};
Then bg-brand and hover:bg-brand-dark work everywhere.
Check your work
Answers to the practice below, so you can verify without a mentor.
A card: rounded-xl border border-gray-200 bg-white p-4 shadow-sm
A primary button:
rounded-lg bg-emerald-600 px-4 py-2 font-medium text-white hover:bg-emerald-700 focus-visible:ring-2 focus-visible:ring-emerald-500 disabled:opacity-50
A row with the name left and price right:
flex items-center justify-between gap-4 — justify-between is what pushes
them apart.
Responsive grid, 2 / 3 / 4 columns:
grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 lg:gap-6
Title clamped to two lines: line-clamp-2
Why bg-${colour}-600 fails: Tailwind reads your files as text at build
time and only generates classes that appear literally. That string never exists
in the source, so the CSS is never written.
Mobile-first order: unprefixed classes apply at every size; sm: and above
override them on wider screens. So write the phone layout first and add
prefixes for larger screens.
Practice
- Style a card: white background, 1px light border, 12px corners, 16px padding, small shadow.
- Style a primary button with hover, keyboard focus ring and a disabled state.
- Lay out a row with the product name on the left and price on the right.
- Build a grid that is 2 columns on a phone, 3 on a tablet, 4 on a laptop.
- Clamp a long product title to two lines.
- Write
bg-${colour}-600withcolour = "emerald". Confirm nothing happens, then fix it with a lookup object. - Install
clsxand use it to switch a button between active and inactive. - Add a
brandcolour totailwind.config.tsand usebg-brand. - Resize your browser through all four breakpoints and check nothing overflows horizontally at 320px.
Next: turning these classes into components you write once.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship