Session 2 of the Designing & Shipping with shadcn/ui in Figma series, in which Gordon learns to think in tokens and utilities and to translate any Auto Layout frame into Tailwind classes. It covers utility-first thinking - composing classes rather than writing bespoke CSS - and the four scales he lives in: spacing on a 4px base, the color palette, the type scale, and the radius scale, each with tables and SVG rulers. Auto Layout is mapped onto flexbox control by control, with a mapping table and an annotated frame, and a repeatable recipe lets him read any frame off as classes. The scaffolded your-turn build works on the Aster site: he builds the pricing card with Auto Layout and spacing and radius variables, compares an eyeballed rebuild against a token-based one, blocks out the five homepage sections on the scale, and reads the frame's Tailwind aloud. The frame-to-classes pattern comes before every check, and the session includes at least four checks, three traps, and homework.
Subject: shadcn/ui + Figma · 97 slides · applied lesson
Open the interactive version of this deck · Homework for this lesson
Title
Session 2 · Designing & Shipping with shadcn/ui
Stop writing bespoke CSS. Start composing tokens. By the end you can read any Auto Layout frame off as a string of Tailwind classes - for the Aster landing page.
Objectives
Last session you set up aster-site and met the three layers. Today the goal is one sentence: think in tokens and utilities, and translate any Auto Layout frame into Tailwind classes.
Warm-up
Discussion prompt
Before we open Session 2: Tailwind's System, Made Visual in Figma: without looking back, what was the main idea of Session 1: Foundations & the Design-to-Code Mental Model, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
Session 1 of 6, in 60 slides. It explains what Tailwind, Radix, and shadcn/ui actually are and how they relate, why copying components in and owning the code beats a closed library such as MUI or Ant, and how the Figma kit's Pages, Assets panel, and Variables panel each mirror the code one-to-one. A scaffolded your-turn build assembles an Aster profile card from Card, Avatar, Badge, and Button. The session runs the Aster studio landing-page example that continues across all six sessions.
Concept
You already design on a grid and reuse spacing values. Tailwind is that instinct, turned into a fixed vocabulary the code shares. Once you speak it, a frame and its code stop being two separate artifacts.
The payoff: when your Figma spacing, type, and radius all sit on the same scale Tailwind uses, handoff becomes translation, not reinterpretation. No more "is that 15 or 16 pixels?" arguments.
Counterexample
Discussion prompt
The payoff: when your Figma spacing, type, and radius all sit on the same scale Tailwind uses, handoff becomes translation, not reinterpretation. No more "is that 15 or 16 pixels?" arguments.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Concept
Five moves, each one earning the next. We finish by rebuilding pieces of the Aster homepage on the scale.
Analogy
Discussion prompt
Explain Today's roadmap by analogy to something with no shadcn/ui + Figma in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Five moves, each one earning the next. We finish by rebuilding pieces of the Aster homepage on the scale.
Section
Section 1
Concept
The old way: for every element you invent a class name, then hand-write a block of CSS - padding, colors, radius, all bespoke. Two hundred elements, two hundred little decisions to keep in sync.
The utility-first way: you compose an element from a fixed set of tiny, single-purpose classes right on the markup. px-4 always means the same padding. You stop inventing values and start selecting them.
utility class — A tiny, single-purpose class that sets one thing to one scale value - e.g. p-4 (16px padding all sides), gap-2 (8px gap), rounded-md (medium corner radius). You compose many of them on one element.
Explain it
Discussion prompt
Explain Styling is composing, not authoring to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
The old way: for every element you invent a class name, then hand-write a block of CSS - padding, colors, radius, all bespoke. Two hundred elements, two hundred little decisions to keep in sync.
Worked example
Here's an Aster primary button as bespoke CSS - a class name plus a rule you author by hand.
.aster-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 6px;
background: var(--primary);
color: var(--primary-foreground);
}Now the same button as utilities - no separate stylesheet, every value already on the scale.
<button class="inline-flex items-center gap-2 px-4 py-2
rounded-md bg-primary text-primary-foreground">
Get started
</button>| bespoke CSS | utility class | value |
|---|---|---|
| display: inline-flex | inline-flex | inline-flex |
| gap: 8px | gap-2 | 8px |
| padding: 8px 16px | py-2 px-4 | 8px / 16px |
| border-radius: 6px | rounded-md | from --radius |
Comparison
Comparison matrix
From The same button, two ways: refill the utility class column from what you know. The rest of the table is as it appeared.
| bespoke CSS | utility class | value |
|---|---|---|
| display: inline-flex | inline-flex | inline-flex |
| gap: 8px | gap-2 | 8px |
| padding: 8px 16px | py-2 px-4 | 8px / 16px |
| border-radius: 6px | rounded-md | from --radius |
Intuition
Figure (svg): Lego bricks with utility labels versus a lump of clay
Bespoke CSS is clay - infinite freedom, and every element is a fresh sculpt at a slightly different size. Freedom you didn't want, drift you didn't notice.
Utilities are Lego bricks from a fixed set. You don't get 15px; you snap p-4 (16px). The constraint is the feature - everything lines up because it comes from the same box.
Socratic
Discussion prompt
Bespoke CSS is clay - infinite freedom, and every element is a fresh sculpt at a slightly different size. Freedom you didn't want, drift you didn't notice.
Suppose that were not true. What is the first thing in Session 2: Tailwind's System, Made Visual in Figma that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Answer:
Utilities are Lego bricks from a fixed set. You don't get 15px; you snap p-4 (16px). The constraint is the feature - everything lines up because it comes from the same box.
Concept
A fair worry: a fixed set sounds limiting. In practice the set is large and combinatorial - a handful of scale steps across padding, gap, size, color, and radius compose into essentially any layout you'd actually design.
And the discipline is exactly what keeps an interface coherent. When you do need a true one-off, Tailwind allows arbitrary values like p-[15px] - but treat that as a last resort, a signal you've stepped off the system.
Concept
One quiet nicety: it doesn't matter what order you write the classes in - p-4 flex and flex p-4 render identically. Tailwind resolves each utility to its own rule; there's no cascade to reason about.
But for humans, keep a consistent reading order - layout first (flex flex-col), then spacing (gap-* p-*), then alignment, then size, then color and radius. That's the same order you'll read a frame in. Habit now, speed later.
Worked example
Utility-first in miniature. An Aster nav link needs: small padding, a small text size, rounded corners, and a subtle hover. Compose it - don't author a .nav-link rule.
<a class="px-3 py-2 text-sm rounded-md hover:bg-accent">
Work
</a>Read left to right: px-3 (12px) py-2 (8px) padding, text-sm label, rounded-md corners, and a state utility hover:bg-accent that only applies on hover.
No stylesheet, no naming, nothing to keep in sync. The next nav link reuses the exact same five classes - so they're identical by construction, not by discipline.
Anomaly
Predict first
A student writes this, and it looks reasonable:
You want a bit of horizontal padding, so you open a stylesheet and write a custom class.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: You've re-created what px-4 already does - now there are two ways to say the same thing, and they can drift apart.
Reach for the utility that already exists on the scale.
Why: You've re-created what px-4 already does - now there are two ways to say the same thing, and they can drift apart.
Trap
You want a bit of horizontal padding, so you open a stylesheet and write a custom class.
Author .aster-pad { padding: 0 16px; }
Why: You've re-created what px-4 already does - now there are two ways to say the same thing, and they can drift apart.
Apply .aster-pad next to Tailwind classes
Why: The bespoke rule sits outside the scale. A theme change to spacing won't touch it, and reviewers can't read the padding off the markup.
Reach for the utility that already exists on the scale.
Use px-4
Why: 16px horizontal padding, straight from the spacing scale - no new CSS, no second source of truth.
Read it off the markup
Why: Anyone can see the padding is px-4, and it moves in lockstep with every other px-4 on the site.
Section
Section 2
Concept
Almost every visual decision is one of four choices, and each is a scale - a short ordered list of allowed steps. Learn the four and you've learned most of Tailwind's vocabulary.
Matching
Match the pairs
From You live in four scales — match each one to what it actually does. The descriptions have been shuffled.
Why: Spacing, Type, Radius are easy to tell apart while they are sitting next to their descriptions and much harder afterwards, which is what this checks.
Concept
Figure (svg): Spacing ruler from step 1 (4px) to step 8 (32px)
The spacing scale has one base unit: 0.25rem = 4px. Every step is that unit times the number in the class. So the number is not pixels - it's quarter-rems.
p-4 is step 4 = 16px padding. gap-2 is step 2 = 8px gap. Same scale powers padding, gap, margin, width, height - m-*, w-*, h-*, size-* all read from it.
spacing scale — The ordered list of spacing steps built on a 4px base unit. The class number is multiplied by 4px: 1=4, 2=8, 3=12, 4=16, 6=24, 8=32, 12=48.
Definition probe
Sort into buckets
Every line below is part of the definition of utility class or of spacing scale — one or the other, never both. Put each where it belongs.
Worked example
Keep this table in your head. rem is the source of truth; px is what you see at the default root size.
| step | rem | px | example class |
|---|---|---|---|
| 1 | 0.25rem | 4px | p-1 / gap-1 |
| 2 | 0.5rem | 8px | gap-2 |
| 3 | 0.75rem | 12px | px-3 |
| 4 | 1rem | 16px | p-4 |
| 6 | 1.5rem | 24px | py-6 |
| 8 | 2rem | 32px | gap-8 |
| 12 | 3rem | 48px | py-12 |
Notice the multiply-by-4 shortcut: see p-6, think 6 × 4 = 24px. That mental math is the whole game. And going the other way: 32px seen in a mockup → divide by 4 → 8 → gap-8.
Trade off
Comparison matrix
From Reading the spacing table: every row here is a choice with a cost. Fill the example class column, then say which row you would actually pick and what you give up for it.
| step | rem | px | example class |
|---|---|---|---|
| 1 | 0.25rem | 4px | p-1 / gap-1 |
| 2 | 0.5rem | 8px | gap-2 |
| 3 | 0.75rem | 12px | px-3 |
| 4 | 1rem | 16px | p-4 |
| 6 | 1.5rem | 24px | py-6 |
| 8 | 2rem | 32px | gap-8 |
| 12 | 3rem | 48px | py-12 |
Intuition
A scale in steps of 4 is coarse on purpose. If any value were allowed, two designers - or the same designer on two days - would land on 14, 15, 16 for "the same" spacing, and the interface would quietly wobble.
Snapping everything to multiples of 4 makes spacing decisions instead of guesses. Fewer choices, more consistency. Your eye stops negotiating pixels and starts picking rungs.
Anomaly
Predict first
A student writes this, and it looks reasonable:
You read gap-2 and assume the number is a pixel count - 2px.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Reads the class number as pixels.
The number is quarter-rems: multiply by 4. gap-2 = 2 × 4 = 8px.
Why: Reads the class number as pixels. A 2px gap is almost invisible - the items nearly touch.
Trap
You read gap-2 and assume the number is a pixel count - 2px.
Set the row gap to 2px in Figma
Why: Reads the class number as pixels. A 2px gap is almost invisible - the items nearly touch.
Compare to code
Why: The code renders an 8px gap, so Figma and code disagree. The mockup looks tighter than the built page.
The number is quarter-rems: multiply by 4. gap-2 = 2 × 4 = 8px.
Set the row gap to 8px in Figma
Why: 8px is step 2 on the scale - exactly what gap-2 produces.
Compare to code
Why: Figma and code match. Every gap-2 on the page is the same 8px.
Worked example
The mockup calls for: 48px between the hero and the section above, 24px between the hero's stacked lines, and 32px of vertical breathing room inside. Convert each by dividing by 4.
| mockup value | ÷ 4 | class |
|---|---|---|
| 48px section gap | 12 | gap-12 |
| 24px line gap | 6 | gap-6 |
| 32px vertical padding | 8 | py-8 |
So the hero frame reads flex flex-col gap-6 py-8, sitting under a page column that uses gap-12. Every number came from the same 4px scale - divide, don't guess.
Concept
Color is also a scale, but a named one. Under the hood Tailwind ships a primitive palette - families like violet with steps violet-50 through violet-950, dark rising with the number.
| token | rough role | class |
|---|---|---|
| violet-100 | very light tint | bg-violet-100 |
| violet-600 | strong brand violet | bg-violet-600 |
| violet-950 | near-black violet | text-violet-950 |
Foreshadow (Session 3): you rarely name raw colors in real components. You'll wrap them in semantic tokens - bg-primary, text-primary-foreground - so one variable retints the whole app. For now, know the palette exists underneath.
Socratic
Discussion prompt
Color is also a scale, but a named one. Under the hood Tailwind ships a primitive palette - families like violet with steps violet-50 through violet-950, dark rising with the number.
Suppose that were not true. What is the first thing in Session 2: Tailwind's System, Made Visual in Figma that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Worked example
Aster's brand violet is oklch(0.55 0.24 285). You could reference a primitive class close to it, but watch what that costs you later.
<!-- primitive: hard-codes the exact violet everywhere -->
<button class="bg-violet-600 text-white">Get started</button>The semantic version names the role, not the color. Session 3's whole point: change one variable, retint everything.
<!-- semantic: role-based, retints from one variable -->
<button class="bg-primary text-primary-foreground">Get started</button>Today, either works visually. The habit to start now: think in roles - "this is the primary action" - even while you're still on the raw palette.
Concept
Figure (svg): Type scale samples from small to large
Font size is a scale too: text-sm, text-base, text-lg, text-xl, then 2xl, 3xl, 4xl. Each step also carries a sensible default line-height, so vertical rhythm comes along for free.
| class | rem | px |
|---|---|---|
| text-sm | 0.875rem | 14px |
| text-base | 1rem | 16px |
| text-lg | 1.125rem | 18px |
| text-xl | 1.25rem | 20px |
| text-2xl | 1.5rem | 24px |
| text-3xl | 1.875rem | 30px |
| text-4xl | 2.25rem | 36px |
Pattern
Step through it
Step through Scale 3 - Type one row at a time. What is driving the change, and what would the row after the last one be?
Worked example
Aster's hero has three text roles. Instead of picking arbitrary sizes, you pick steps.
<p class="text-sm">Freelance design studio</p>
<h1 class="text-4xl">Design that ships.</h1>
<p class="text-lg">Interfaces built on a real system.</p>| role | class | size |
|---|---|---|
| eyebrow | text-sm | 14px |
| headline | text-4xl | 36px |
| subhead | text-lg | 18px |
Three steps apart, instantly legible hierarchy - and every heading on the site can reuse the exact same rungs.
Pattern
Step through it
Step through Type scale on the Aster hero one row at a time. What is driving the change, and what would the row after the last one be?
Concept
Corner radius is the smallest scale, and it's derived. One variable, --radius (Aster's is 0.625rem), drives the whole set. The classes offset from it.
| class | definition | with --radius = 0.625rem |
|---|---|---|
| rounded-sm | calc(--radius - 4px) | ~6px |
| rounded-md | calc(--radius - 2px) | ~8px |
| rounded-lg | var(--radius) | 10px |
| rounded-xl | calc(--radius + 4px) | ~14px |
Because they're all offsets of one variable, dialing --radius up or down re-rounds every corner in the app at once. You tune softness in a single place.
Socratic
Discussion prompt
Corner radius is the smallest scale, and it's derived. One variable, --radius (Aster's is 0.625rem), drives the whole set. The classes offset from it.
Suppose that were not true. What is the first thing in Session 2: Tailwind's System, Made Visual in Figma that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Answer:
Because they're all offsets of one variable, dialing --radius up or down re-rounds every corner in the app at once. You tune softness in a single place.
Intuition
Figure (svg): One radius dial driving three cards at once
The radius scale is the clearest example of a token: a named value other things point at. Nothing hard-codes 10px; everything references --radius through rounded-lg.
token — A named design value stored once and referenced by many places (e.g. --radius, --primary). Change the token and every component that references it updates together.
Worked example
Every scale shows up at once on a single element. Read the Aster CTA and name which scale each class comes from.
<button class="px-4 py-2 text-sm rounded-md
bg-primary text-primary-foreground">Get started</button>| class | scale | meaning |
|---|---|---|
| px-4 py-2 | spacing | 16px / 8px padding |
| text-sm | type | 14px label |
| rounded-md | radius | ~8px corners |
| bg-primary | color | role: primary |
Four scales, one button. Composing across all four - fluently, from the panel - is what "thinking in tokens" feels like.
Anomaly
Predict first
A student writes this, and it looks reasonable:
You want a 16px headline, so you reach for a number-4 class like the spacing scale uses.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The type scale isn't numbered like spacing - there's no text-4.
Use the named type step. 16px is text-base.
Why: The type scale isn't numbered like spacing - there's no text-4. Sizes are named: sm, base, lg, xl, 2xl...
Trap
You want a 16px headline, so you reach for a number-4 class like the spacing scale uses.
Write text-4
Why: The type scale isn't numbered like spacing - there's no text-4. Sizes are named: sm, base, lg, xl, 2xl...
See nothing apply
Why: text-4 isn't a real class, so the text falls back to the default size. Two different scales, two different naming systems.
Use the named type step. 16px is text-base.
Write text-base
Why: The type scale uses names (sm/base/lg/xl/2xl...), not the ×4 spacing numbers.
See 16px text
Why: Right scale, right vocabulary. Keep spacing numbers and type names in separate mental buckets.
Break the constraint
Discussion prompt
The rule this trap just fixed:
The type scale uses names (sm/base/lg/xl/2xl...), not the ×4 spacing numbers.
Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?
Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.
Answer:
The type scale isn't numbered like spacing - there's no text-4. Sizes are named: sm, base, lg, xl, 2xl...
Section
Section 3 · the heart of it
Concept
Here is the sentence to internalize: a Figma Auto Layout frame is a CSS flexbox. Every knob in the Auto Layout panel corresponds to a flexbox property, and every flexbox property has a Tailwind class.
Auto Layout — Figma's system for frames that arrange children in a row or column with a set gap, padding, alignment, and resizing behavior - the exact model CSS flexbox uses, control for control.
So when you build with Auto Layout, you're already thinking in flexbox. Reading it off as Tailwind is just looking up each control in one table.
Concept
Figure (svg): Flex row showing main axis maps to justify and cross axis maps to items
Two axes, and mixing them up is the classic error. The main axis runs in the layout direction. The cross axis is perpendicular to it.
justify-* positions children along the main axis. items-* aligns them along the cross axis. In a row: justify-* is horizontal, items-* is vertical. In a column they swap - because the main axis is now vertical.
Socratic
Discussion prompt
Two axes, and mixing them up is the classic error. The main axis runs in the layout direction. The cross axis is perpendicular to it.
Suppose that were not true. What is the first thing in Session 2: Tailwind's System, Made Visual in Figma that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Concept
Both justify-* and items-* take the same set of positions. Figma's alignment picker (the nine-dot grid) is choosing exactly these.
| intent | main axis | cross axis |
|---|---|---|
| pack to the start | justify-start | items-start |
| center | justify-center | items-center |
| pack to the end | justify-end | items-end |
| push apart | justify-between | (n/a) |
The Figma nine-dot alignment grid maps cleanly: top-left is justify-start items-start, dead center is justify-center items-center. Read the dot, write the two classes.
Sorting
Sort into buckets
These are the pieces of Session 2: Tailwind's System, Made Visual in Figma, out of order. Put each one back under the part of the lesson it belongs to.
Concept
This is the table to bookmark. Figma control on the left, Tailwind class on the right.
| Figma Auto Layout control | Tailwind class |
|---|---|
| Direction: Vertical | flex-col |
| Direction: Horizontal | flex-row |
| Gap (item spacing) | gap-* (e.g. gap-4) |
| Padding (all) | p-* (e.g. p-6) |
| Padding horizontal / vertical | px-* / py-* |
| Alignment: space between | justify-between |
| Alignment: main axis | justify-start / center / end |
| Alignment: cross axis | items-start / center / end |
| Resizing: Hug | w-fit / w-auto |
| Resizing: Fill | flex-1 / w-full |
| Resizing: Fixed | a set width, e.g. w-80 |
| Wrap | flex-wrap |
And the always-there prefix: an Auto Layout frame is flex. So every frame starts flex ... then you fill in direction, gap, padding, alignment, size.
Worked example
Figure (svg): Card frame annotated with its Tailwind classes
Read this frame outside-in. The outer card is a vertical Auto Layout with 24px padding and 16px gap.
<div class="flex flex-col gap-4 p-6"> ... </div>The top row is horizontal, pushing title and badge apart, vertically centered.
<div class="flex flex-row justify-between items-center">The button is set to Fill, so it stretches to the card's width.
<button class="flex-1"> ... </button>Anomaly
Predict first
A student writes this, and it looks reasonable:
You want the row's title and badge pushed to opposite ends, so you reach for items-between.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: items-* controls the CROSS axis (vertical, in a row).
Spreading along the row is the main axis, so use justify-*.
Why: items-* controls the CROSS axis (vertical, in a row). There's no items-between, and even items-* wouldn't spread them horizontally.
Trap
You want the row's title and badge pushed to opposite ends, so you reach for items-between.
Write flex-row items-between
Why: items-* controls the CROSS axis (vertical, in a row). There's no items-between, and even items-* wouldn't spread them horizontally.
Result
Why: Nothing spreads apart - the class doesn't exist or targets the wrong axis. The layout ignores your intent.
Spreading along the row is the main axis, so use justify-*.
Write flex-row justify-between items-center
Why: justify-between spreads on the main axis (horizontal); items-center handles cross-axis (vertical) alignment.
Result
Why: Title left, badge right, both vertically centered - exactly "space between" from the Figma panel.
Concept
Figma lets you set one padding for all sides, or separate horizontal and vertical values. Tailwind mirrors that exactly with prefixes.
| Figma padding | Tailwind | sets |
|---|---|---|
| all sides 16 | p-4 | top, right, bottom, left |
| horizontal 24 | px-6 | left + right |
| vertical 16 | py-4 | top + bottom |
| one side (top 32) | pt-8 | top only |
So a navbar with 24px sides and 16px top/bottom is px-6 py-4 - and you can read that straight off the Figma padding fields. Same scale numbers, just split by axis.
Comparison
Comparison matrix
From Padding: all sides, or axis by axis: refill the Tailwind column from what you know. The rest of the table is as it appeared.
| Figma padding | Tailwind | sets |
|---|---|---|
| all sides 16 | p-4 | top, right, bottom, left |
| horizontal 24 | px-6 | left + right |
| vertical 16 | py-4 | top + bottom |
| one side (top 32) | pt-8 | top only |
Worked example
Here's what the Figma Auto Layout panel shows for the Aster hero, and the class each field becomes. This is the panel-to-class translation, field by field.
| panel field | value | class |
|---|---|---|
| Direction | vertical | flex-col |
| Gap between items | 24 | gap-6 |
| Horizontal padding | 24 | px-6 |
| Vertical padding | 48 | py-12 |
| Alignment | top-center | items-center justify-start |
Line the classes up in reading order and the hero is done: flex flex-col gap-6 px-6 py-12 items-center. Every field on the panel had a home.
Concept
Figma's three resizing modes each have a flex meaning. Hug shrinks to content. Fill grows to share the space. Fixed is a hard number.
| Figma resizing | means | Tailwind |
|---|---|---|
| Hug contents | as small as the content | w-fit / w-auto |
| Fill container | stretch to share space | flex-1 / w-full |
| Fixed width | an exact width | w-80 (= 320px) |
flex-1 is the workhorse: it says "take a share of the leftover space." Two flex-1 siblings split the row evenly - the flexible behavior a Fixed width can never give you.
Explain it
Discussion prompt
Explain Resizing: Hug, Fill, Fixed to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Figma's three resizing modes each have a flex meaning. Hug shrinks to content. Fill grows to share the space. Fixed is a hard number.
Anomaly
Predict first
A student writes this, and it looks reasonable:
You want the Aster pricing cards to share the row evenly, but you set each to a Fixed w-80.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: 320px is frozen. On a wider container the cards stay 320px and leave an awkward gap; on a narrower one they overflow.
You meant Fill - each card takes an equal share of the row.
Why: 320px is frozen. On a wider container the cards stay 320px and leave an awkward gap; on a narrower one they overflow.
Trap
You want the Aster pricing cards to share the row evenly, but you set each to a Fixed w-80.
Give each card w-80
Why: 320px is frozen. On a wider container the cards stay 320px and leave an awkward gap; on a narrower one they overflow.
Resize the window
Why: The cards don't respond - Fixed widths ignore available space, so the layout breaks at other sizes.
You meant Fill - each card takes an equal share of the row.
Give each card flex-1
Why: flex-1 tells each card to grow and share the leftover space equally with its siblings.
Resize the window
Why: The cards stretch and shrink together, always filling the row - responsive by construction.
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
0.25rem = 4px. Every step is that unit times the number in the class. So the number is not pixels - it's quarter-rems.gap-2 and assume the number is a pixel count - 2px.Concept
Figma's Auto Layout can let a horizontal frame wrap its children onto a new line when they don't fit. That maps to one class: flex-wrap.
It's how a feature grid stays graceful on narrow screens without a media query: the cards simply flow to the next row. You'll reach for it on Aster's feature grid so three cards become two-plus-one when the viewport shrinks.
Analogy
Discussion prompt
Explain Wrap - when a row runs out of room by analogy to something with no shadcn/ui + Figma in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Figma's Auto Layout can let a horizontal frame wrap its children onto a new line when they don't fit. That maps to one class: flex-wrap.
Intuition
Figure (svg): Nested Auto Layout frames, each its own flex container
Real layouts are frames inside frames. A card is a column; its header is a row; its feature list is another column. Each Auto Layout frame is its own independent flex container.
That's why you read outside-in: give the outer frame its classes, then step into each child frame and start the recipe again. The nesting in Figma is exactly the nesting of your divs.
Counterexample
Discussion prompt
Real layouts are frames inside frames. A card is a column; its header is a row; its feature list is another column. Each Auto Layout frame is its own independent flex container.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Worked example
Let's read a whole Aster pricing card's Auto Layout off as classes, one control at a time. The card is a vertical frame, 24px padding, 24px gap between blocks, medium-large corners.
<div class="flex flex-col gap-6 p-6 rounded-xl">Inside, a header row: plan name on the left, "Popular" badge on the right, centered vertically - the space-between pattern.
<div class="flex flex-row items-center justify-between">
<span class="text-lg">Pro</span>
<span class="text-sm">Popular</span>
</div>The price, big; and a features list stacked with a small gap.
<p class="text-4xl">$55</p>
<div class="flex flex-col gap-2"> ...features... </div>And the CTA button set to Fill, so it spans the card.
<button class="flex-1 px-4 py-2 rounded-md
bg-primary text-primary-foreground">Choose Pro</button>
</div>| frame part | Auto Layout | classes |
|---|---|---|
| card | vertical, pad 24, gap 24 | flex flex-col gap-6 p-6 rounded-xl |
| header | horizontal, space between | flex flex-row items-center justify-between |
| features | vertical, gap 8 | flex flex-col gap-2 |
| CTA | Fill | flex-1 px-4 py-2 rounded-md |
Section
Section 4 · the recipe
Concept
You now have the mapping table. But you don't want to hunt through it every time - you want a fixed order you always walk a frame in, so nothing gets missed and the class string comes out consistent.
Six controls, always the same sequence. Next slide is the recipe; after that we'll drill it until it's automatic.
Ranking
Put in order
These are the steps of Pattern: translate a frame to classes, scrambled. Put them back in order before the next slide shows you.
flex flex-col or flex flex-row.flex-wrap if yes.gap-*.p-* / px-* / py-*.justify-*; cross axis → items-*.w-fit / flex-1 / a set width.Why: This is the order the recipe itself gives. Recalling the sequence without the slide in front of you is the difference between recognising the method and being able to run it — most of what goes wrong in practice is a step done out of turn.
Pattern
Every frame, every time, in the same order. Say each control out loud and write its class before moving on.
flex flex-col or flex flex-row.flex-wrap if yes.gap-*.p-* / px-* / py-*.justify-*; cross axis → items-*.w-fit / flex-1 / a set width.Direction, Wrap, Gap, Padding, Alignment, Size. Do it outside-in: the frame first, then recurse into each child that is itself an Auto Layout frame.
Edge cases
Discussion prompt
Pattern: translate a frame to classes works on the cases you have just seen. Push it to the edge: what is the most degenerate input it still handles — empty, zero, one item, everything equal — and what is the first case where it stops being true? Name the case, not just "it breaks".
Hint: Try the smallest legal input, then the largest, then the one where two things collide. Methods are specified at their edges; the middle takes care of itself.
Answer:
Every frame, every time, in the same order. Say each control out loud and write its class before moving on.
Worked example
The Aster navbar: a horizontal bar, logo on the left, nav links on the right, 16px vertical + 24px horizontal padding, centered. Run the six steps.
| step | reading | class |
|---|---|---|
| Direction | horizontal | flex flex-row |
| Wrap | no | (none) |
| Gap | 24px between groups | gap-6 |
| Padding | 16 vertical, 24 horizontal | py-4 px-6 |
| Alignment | space between, centered | justify-between items-center |
| Size | bar fills width | w-full |
Concatenate the right column and you have the navbar.
<nav class="flex flex-row gap-6 py-4 px-6
justify-between items-center w-full">Worked example
Second rep, harder frame. Aster's feature grid: a horizontal row of three cards, wraps on narrow screens, 24px gap, 24px horizontal padding, each card set to Fill.
| step | reading | class |
|---|---|---|
| Direction | horizontal | flex flex-row |
| Wrap | yes | flex-wrap |
| Gap | 24px | gap-6 |
| Padding | 24 horizontal | px-6 |
| Alignment | top-aligned | items-start |
| Size (each card) | Fill | flex-1 |
The frame, plus each card's size class.
<section class="flex flex-row flex-wrap gap-6 px-6 items-start">
<div class="flex-1"> ...card... </div>
<div class="flex-1"> ...card... </div>
<div class="flex-1"> ...card... </div>
</section>Worked example
Third rep, and notice the footer holds two child groups - a copyright block on the left, a links row on the right - so you recurse once. Outer footer first.
| step | reading | class |
|---|---|---|
| Direction | horizontal | flex flex-row |
| Wrap | no | (none) |
| Gap | handled by space-between | (none) |
| Padding | 24 horizontal, 32 vertical | px-6 py-8 |
| Alignment | space between, centered | justify-between items-center |
| Size | full width | w-full |
Then step into the right-hand links group - itself a horizontal frame with an 8px gap - and run the recipe again.
<footer class="flex flex-row justify-between items-center px-6 py-8 w-full">
<span class="text-sm">© Aster</span>
<div class="flex flex-row gap-2"> ...links... </div>
</footer>Elimination
Eliminate the wrong options
In Tailwind's default spacing scale, how many pixels is gap-2?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: B
Why: The base unit is 0.25rem = 4px, and the class number multiplies it. gap-2 = 2 × 4px = 8px. The number is quarter-rems, not pixels.
Check
The multiply-by-4 rule, straight up.
Check your understanding
In Tailwind's default spacing scale, how many pixels is gap-2?
Answer: B
Why: The base unit is 0.25rem = 4px, and the class number multiplies it. gap-2 = 2 × 4px = 8px. The number is quarter-rems, not pixels.
Prediction
Predict first
In a flex-row, you want the children spread to opposite ends along the row (left and right). Which class does that?
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: justify-between
Why: Spreading along the row is the main axis, and justify-* controls the main axis. justify-between pushes the first and last children to the ends with the space distributed between them.
Check
A horizontal Auto Layout row. Read the prompt carefully - direction matters.
Check your understanding
In a flex-row, you want the children spread to opposite ends along the row (left and right). Which class does that?
Answer: C
Why: Spreading along the row is the main axis, and justify-* controls the main axis. justify-between pushes the first and last children to the ends with the space distributed between them.
Commit first
Predict first
In Figma terms, which resizing mode - and its Tailwind class - makes each button share the row equally?
Commit to an answer, then rate it — certain, fairly sure, or guessing — and write the rating down before you turn the page.
Correct: Fill → flex-1
Why: Fill means 'grow to share the space', which maps to flex-1. Two flex-1 siblings each take an equal share of the row and resize with the container - exactly the behavior asked for.
The rating matters as much as the answer: confident-and-wrong is the combination that survives revision, because nothing about it feels like it needs revisiting.
Check
You have two buttons in a row and want them to split the width evenly, resizing with the container.
Check your understanding
In Figma terms, which resizing mode - and its Tailwind class - makes each button share the row equally?
Answer: B
Why: Fill means 'grow to share the space', which maps to flex-1. Two flex-1 siblings each take an equal share of the row and resize with the container - exactly the behavior asked for.
Prediction
Predict first
Vertical Auto Layout, 16px gap, 24px padding all sides. Which Tailwind string matches?
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: flex flex-col gap-4 p-6
Why: Vertical → flex-col. 16px is step 4 (4 × 4px) → gap-4. 24px is step 6 (6 × 4px) → p-6. So flex flex-col gap-4 p-6.
Check
A frame: vertical Auto Layout, 16px gap, 24px padding all around. Which class string is right?
Check your understanding
Vertical Auto Layout, 16px gap, 24px padding all sides. Which Tailwind string matches?
Answer: A
Why: Vertical → flex-col. 16px is step 4 (4 × 4px) → gap-4. 24px is step 6 (6 × 4px) → p-6. So flex flex-col gap-4 p-6.
Section
Section 5 · Your turn
Concept
Now you drive. Do each milestone in Figma with real Auto Layout frames and variables, then read your frame back as classes using the recipe. Build first, peek at the solution second.
| # | milestone | you produce |
|---|---|---|
| 1 | Aster pricing card | an Auto Layout card on spacing + radius variables |
| 2 | Eyeball vs tokens | two rebuilds, one comparison |
| 3 | Homepage blockout | five section frames on the scale |
Trade off
Comparison matrix
From The build: three milestones: every row here is a choice with a cost. Fill the milestone column, then say which row you would actually pick and what you give up for it.
| # | milestone | you produce |
|---|---|---|
| 1 | Aster pricing card | an Auto Layout card on spacing + radius variables |
| 2 | Eyeball vs tokens | two rebuilds, one comparison |
| 3 | Homepage blockout | five section frames on the scale |
Worked example
Your turn: build the Aster "Pro" pricing card as a vertical Auto Layout frame. Use spacing variables for padding (24) and gap (24), and a radius variable for the corners. Header row: plan name left, badge right. Then price, a features list (8px gap), and a Fill-width CTA.
Hint: outer frame = flex flex-col, padding 24 → p-6, gap 24 → gap-6, corners → rounded-xl. The header row is its own horizontal frame with space-between. The CTA is set to Fill.
<div class="flex flex-col gap-6 p-6 rounded-xl">
<div class="flex flex-row items-center justify-between">
<span class="text-lg">Pro</span>
<span class="text-sm">Popular</span>
</div>
<p class="text-4xl">$55</p>
<div class="flex flex-col gap-2">
<span class="text-sm">Unlimited projects</span>
<span class="text-sm">Priority support</span>
</div>
<button class="flex-1 px-4 py-2 rounded-md
bg-primary text-primary-foreground">Choose Pro</button>
</div>Self-check - your frame should read off as these classes, part by part:
| part | resulting classes |
|---|---|
| card | flex flex-col gap-6 p-6 rounded-xl |
| header row | flex flex-row items-center justify-between |
| price | text-4xl |
| features | flex flex-col gap-2 |
| CTA | flex-1 px-4 py-2 rounded-md bg-primary text-primary-foreground |
Worked example
Your turn, part A: rebuild the same card by eye - nudge padding, gap, and text sizes until they "look right," reading nothing from the scale. Write down the raw px you land on.
Your turn, part B: rebuild it again using only scale tokens - snap every value to a spacing step, a type step, a radius step. Now put the two side by side.
Hint: for each eyeballed value, find the nearest scale step and note whether it maps cleanly. Off-scale values (13, 15, 18, 22...) are the ones with no clean class.
| property | eyeballed | nearest token | clean class? |
|---|---|---|---|
| padding | 15px | 16px | yes → p-4 |
| gap | 10px | 8px | no → drifts to gap-2 |
| price size | 34px | 36px | yes → text-4xl |
| corner | 9px | 10px | yes → rounded-lg |
The lesson lands here: the eyeballed version needs arbitrary values and won't align with the rest of the page; the token version is all clean classes that share rhythm with every other Aster frame. Same look, wildly different maintainability.
Comparison
Comparison matrix
From Milestone 2 - eyeball vs tokens: refill the nearest token column from what you know. The rest of the table is as it appeared.
| property | eyeballed | nearest token | clean class? |
|---|---|---|---|
| padding | 15px | 16px | yes → p-4 |
| gap | 10px | 8px | no → drifts to gap-2 |
| price size | 34px | 36px | yes → text-4xl |
| corner | 9px | 10px | yes → rounded-lg |
Worked example
Your turn: block out the whole Aster homepage as five stacked Auto Layout frames - navbar, hero, feature grid, pricing, footer - using only spacing-scale gaps and padding. Don't style yet; just get the frames and rhythm right.
Hint: the page itself is a vertical frame (flex flex-col) with a large gap between sections. Each section is its own Auto Layout frame with its own padding. The feature grid and pricing are horizontal rows of Fill cards.
<div class="flex flex-col gap-12">
<nav class="flex flex-row justify-between items-center px-6 py-4"> ... </nav>
<header class="flex flex-col gap-6 px-6 py-12"> ... </header>
<section class="flex flex-row gap-6 px-6"> ...feature cards... </section>
<section class="flex flex-row gap-6 px-6"> ...pricing cards... </section>
<footer class="flex flex-row justify-between px-6 py-8"> ... </footer>
</div>Self-check - every gap and padding below is a real scale step. If any of your frames used an off-scale value, snap it to the nearest row here.
| section | direction | gap / padding classes |
|---|---|---|
| page | vertical | flex flex-col gap-12 |
| navbar | horizontal | px-6 py-4 justify-between |
| hero | vertical | flex flex-col gap-6 px-6 py-12 |
| feature grid | horizontal | flex flex-row gap-6 px-6 |
| pricing | horizontal | flex flex-row gap-6 px-6 |
| footer | horizontal | px-6 py-8 justify-between |
Discrimination
Sort into buckets
Sort these by direction, from memory, without looking back at Milestone 3 - block out the five sections. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.
Worked example
Your turn: pick any frame you built and read it out loud using the recipe - "horizontal, no wrap, gap-6, px-6 py-4, justify-between items-center, full width." If you can say it, you can ship it.
This is the whole skill in one sentence: you now look at an Auto Layout frame and hear its Tailwind. That fluency is what makes handoff feel like reading, not translating.
If your hero reads off as flex flex-col gap-6 px-6 py-12 without checking a table - you're thinking in tokens and utilities. That was today's goal.
Elimination
Eliminate the wrong options
Your Figma frame has 15px padding. What should you do before handoff?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: B
Why: 15px isn't on the 4px scale. Snapping to 16px (step 4) gives a clean p-4 that shares rhythm with the rest of the page and survives a theme's spacing changes - the tiny visual difference isn't worth breaking the system.
Check
You eyeballed a section's padding to 15px. What's the right move before handoff?
Check your understanding
Your Figma frame has 15px padding. What should you do before handoff?
Answer: B
Why: 15px isn't on the 4px scale. Snapping to 16px (step 4) gives a clean p-4 that shares rhythm with the rest of the page and survives a theme's spacing changes - the tiny visual difference isn't worth breaking the system.
Section
Wrap-up
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Utility-First Thinking · The Four Scales · Auto Layout = Flexbox · Read Any Frame · Build It: Aster on the Scale · Recap & Homework. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
| Figma | Tailwind |
|---|---|
| vertical / horizontal | flex-col / flex-row |
| item spacing | gap-* (×4px) |
| padding | p-* / px-* / py-* |
| space between | justify-between |
| cross-axis align | items-* |
| Hug / Fill / Fixed | w-fit / flex-1 / set width |
Homework: block out the full Aster homepage - navbar, hero, feature grid, pricing, footer - as Auto Layout frames using only spacing-scale gaps. No off-scale values. Bring the frames next time.
Next session (Session 3): semantic tokens - trade the raw palette for --primary, --muted, and friends, so one variable retints all of Aster at once.
Want this taught 1-on-1? Alexander tutors shadcn/ui + Figma — $55/session, free consultation.