Session 5 of the shadcn/ui and Figma series, in which Gordon composes a complete, responsive Aster homepage entirely from his themed components. It teaches the hierarchy that runs from components to blocks to pages, "blocks" being shadcn's own term, then covers responsive design in Figma - the Tailwind breakpoints sm, md, lg, and xl, constraints, minimum and maximum width, and wrapping Auto Layout - and maps all of it onto responsive Tailwind such as grid-cols-1 md:grid-cols-3, flex-col md:flex-row, hidden md:flex, max-w-*, and container. The usual suspects (navbar, hero, feature grid, pricing, and footer) are built as worked examples that read off their own responsive classes, and spacing and type stay on the scale at every breakpoint. The session includes a compose-a-responsive-page pattern, a breakpoint table, four checks, three traps, and a fully scaffolded your-turn build of the Aster homepage at desktop and mobile with a per-section self-check.
Subject: shadcn/ui + Figma · 91 slides · applied lesson
Open the interactive version of this deck · Homework for this lesson
Title
Session 5 · shadcn/ui + Figma
Stack your themed components into blocks, blocks into a page, and make the whole thing reflow from desktop to mobile — Figma constraints and wrapping Auto Layout mapped straight onto Tailwind's breakpoints.
Objectives
You have themed components and states. Today you assemble them into the real Aster homepage — one that survives a phone. By the end you can:
sm 640, md 768, lg 1024, xl 1280 — and what changes at each.grid-cols-1 md:grid-cols-3, flex-col md:flex-row, hidden md:flex, max-w-*, container.Warm-up
Discussion prompt
Before we open Session 5: Composition — Responsive Layouts, Blocks & Pages: without looking back, what was the main idea of Session 4: Radix & Component Anatomy, 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 4 of 6 (60 slides): what 'headless' means (Radix supplies behavior + accessibility, shadcn adds the skin), Figma component properties — variants (default/secondary/outline/ghost/destructive) vs boolean props like showIcon and why booleans keep a kit lean, and every interactive state that matters (default, hover, focus-visible ring, active, disabled). Your-turn: brand an Aster button/input and document all its states.
Concept
You already have the pieces: a theme (Session 2), components and variants (Session 3), and states (Session 4). What's missing is the thing a visitor actually loads — a whole page that works at any width.
The mental shift for today: stop thinking in single components and start thinking in arrangements. shadcn ships whole arrangements — it calls them blocks — and a page is just blocks stacked. Figma and Tailwind both give you tools to make those arrangements reflow.
One rule threads through everything: base = mobile, larger widths add overrides. Tailwind is mobile-first, and once you internalise that, responsive design stops being guesswork.
Counterexample
Discussion prompt
One rule threads through everything: base = mobile, larger widths add overrides. Tailwind is mobile-first, and once you internalise that, responsive design stops being guesswork.
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 stops, each one leaning on the last — ending with the full Aster homepage at two breakpoints.
Matching
Match the pairs
From Today's roadmap — match each one to what it actually does. The descriptions have been shuffled.
Why: Blocks & pages, Responsive in Figma, The usual suspects are easy to tell apart while they are sitting next to their descriptions and much harder afterwards, which is what this checks.
Section
Section 1
Concept
Figure (svg): Hierarchy of components inside blocks inside a page
Component: one reusable piece — a Button, a Card, an Input. Block: a themed arrangement of components that does one job — a hero, a pricing section. Page: blocks stacked top to bottom.
block — A themed arrangement of components forming one section of a page — a navbar, hero, or pricing section. shadcn ships ready-made blocks; you compose your own from your themed components.
The whole trick is that a block is built from components that already read your theme. Retint one semantic token and every block retints at once — you never restyle the page.
Analogy
Discussion prompt
Explain The three levels of composition 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:
The whole trick is that a block is built from components that already read your theme. Retint one semantic token and every block retints at once — you never restyle the page.
Concept
On ui.shadcn.com the Blocks page is a gallery of full sections — dashboards, login screens, sidebars — built entirely from the components you already own. They're not a new primitive; they're recipes for arranging the primitives.
In Figma you mirror that idea: a block is a frame with Auto Layout that contains component instances. In code it's a section of JSX that composes <Button>, <Card>, etc. Same arrangement, two representations.
So the Aster page you'll build is five blocks — navbar, hero, feature grid, pricing, footer — each a frame, stacked in one vertical Auto Layout.
Intuition
Think of the page as a vertical shelf. Each block is a tray that slides onto the shelf. You don't reach inside a tray to rearrange forks — you slot the whole tray in, and the tray knows how to arrange its own contents.
That's why composition scales: to add a testimonials section you build one more tray and slot it in. Nothing above or below it needs touching, because each block owns its own internal Auto Layout.
Worked example
Here's the whole homepage expressed as one vertical stack. Read it top to bottom — that's exactly the order a visitor scrolls.
export default function Home() {
return (
<main className="flex flex-col">
<Navbar />
<Hero />
<FeatureGrid />
<Pricing />
<Footer />
</main>
)
}flex flex-col is the vertical shelf — the page's own Auto Layout. Each child is a block you'll build next.
| block | Figma frame | job |
|---|---|---|
| Navbar | top bar, horizontal Auto Layout | logo + links + CTA |
| Hero | centered column | headline + subcopy + button |
| FeatureGrid | 3-across grid | three feature cards |
| Pricing | row of price cards | plans side by side |
| Footer | multi-column | links + legal |
Comparison
Comparison matrix
From The Aster page as a stack of blocks: refill the Figma frame column from what you know. The rest of the table is as it appeared.
| block | Figma frame | job |
|---|---|---|
| Navbar | top bar, horizontal Auto Layout | logo + links + CTA |
| Hero | centered column | headline + subcopy + button |
| FeatureGrid | 3-across grid | three feature cards |
| Pricing | row of price cards | plans side by side |
| Footer | multi-column | links + legal |
Concept
Composing in blocks pays off three ways. Reuse: a themed Card built once appears in the feature grid and the pricing row. Isolation: each block owns its own layout, so editing the hero can't disturb the footer.
Consistency: every block reads the same tokens and the same scale, so the page reads as one design. And because responsiveness lives inside each block, making the page responsive is just making five blocks responsive — a tractable, checkable list.
Worked example
Gordon says: "I'll add a testimonials block to Aster." Placing that sentence on the ladder tells you exactly what he's building — one page section, made of components he already owns.
| level | on Aster | in code |
|---|---|---|
| component | one Card, one Button | <Card />, <Button /> |
| block | the testimonials section | <Testimonials /> composing Cards |
| page | the whole homepage | <main> of stacked blocks |
So a testimonials block is level two: a themed arrangement of Cards, not a new component and not a whole page. Adding it is slotting one more tray onto the shelf.
Trade off
Comparison matrix
From Naming the three levels on Aster: every row here is a choice with a cost. Fill the on Aster column, then say which row you would actually pick and what you give up for it.
| level | on Aster | in code |
|---|---|---|
| component | one Card, one Button | <Card />, <Button /> |
| block | the testimonials section | <Testimonials /> composing Cards |
| page | the whole homepage | <main> of stacked blocks |
Concept
A shadcn block is a recipe you copy into your repo and own — not an installed, locked package like a pre-built theme. That's the same ownership model as the components underneath it, and it's what makes composition safe to edit.
Because you own the block's source, tweaking its layout — a wider hero, a fourth pricing card — is just editing your own file. Nothing upstream fights you, and your theme tokens still flow through it.
Keep that in mind as you build: every block is yours to shape, and every block reads the same tokens, so the page stays one coherent design.
Explain it
Discussion prompt
Explain Why own your blocks (vs a locked library) 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:
Because you own the block's source, tweaking its layout — a wider hero, a fourth pricing card — is just editing your own file. Nothing upstream fights you, and your theme tokens still flow through it.
Section
Section 2 · the core cycle
Concept
A breakpoint is a screen width at which you change the layout. Tailwind ships four, and you'll design against them so your Figma frames and your code agree.
breakpoint — A minimum screen width at which a responsive rule turns on. Tailwind's are sm 640px, md 768px, lg 1024px, xl 1280px. A prefix like md: means 'at this width and wider'.
| breakpoint | min width | what typically changes on Aster |
|---|---|---|
| (base) | 0px (mobile) | everything stacked in one column |
| sm | 640px | small tablets; wider max-w on hero copy |
| md | 768px | nav links appear, feature grid goes 3-across |
| lg | 1024px | pricing sits in a comfortable row |
| xl | 1280px | page hits its max container width |
Definition probe
Sort into buckets
Every line below is part of the definition of block or of breakpoint — one or the other, never both. Put each where it belongs.
Concept
This is the single fact people get backwards. In Tailwind, an unprefixed class is the mobile style, and a prefixed class like md: is an override that turns on at that width and up. There is no mobile: prefix — mobile is the default.
So grid-cols-1 md:grid-cols-3 reads: one column by default (mobile), three columns from 768px up. It does NOT mean 'three columns on mobile'. Get the direction right and every other responsive class becomes obvious.
In Figma you design the same way: sketch the mobile frame first (narrow, everything stacked), then a wider frame that adds the overrides. Designing desktop-first and cramming it down later is how layouts break.
Intuition
Mobile-first has a feel to it: you build the simplest, narrowest version that works, then add capability as the screen grows — links reappear, columns fan out, type gets bigger. You're always enhancing upward.
The opposite — starting rich and desktop-y, then patching downward to cram it onto a phone — is where the off-scale sizes and overflow bugs creep in. Every md: you write should read as 'and on bigger screens, additionally…'.
Concept
Figure (svg): Constraints keep a logo left, a title centered, and a button right as the frame widens
Constraints tell a layer what to do when its parent frame resizes: pin left/right/top/bottom, center, or scale. A logo pinned left hugs the left edge; a button pinned right rides the right edge as the frame widens.
constraint — A rule on a Figma layer that fixes its position or size relative to a parent edge (left, right, top, bottom), the center, or scales it — controlling how it responds when the frame is resized.
Constraints are the manual tool for plain frames. But most Aster blocks use Auto Layout, where reflow is handled by direction, resizing, and wrapping instead — which is closer to how flexbox and grid actually behave in the browser.
Worked example
Picture the navbar as a plain frame for a moment, to see what constraints buy you. Three layers, three constraints — and the bar behaves at any width.
| layer | constraint | behaviour when frame widens |
|---|---|---|
| logo | pin left | stays anchored to the left edge |
| nav links | center | stay centered as the bar grows |
| CTA button | pin right | rides the right edge outward |
With Auto Layout you'd get the same result more directly: justify-between pushes logo and CTA to the edges, no per-layer pinning. Constraints are the fallback when a layer isn't in an Auto Layout flow.
Concept
A block that should span the screen must be Fill (w-full), not a Fixed width. Fixed pins the pixels and refuses to reflow — the fastest way to break responsiveness. Reach for Fixed only for genuinely fixed things like an icon.
To stop text lines getting uncomfortably long on huge screens, cap width with max-width: in Figma set a max on the Auto Layout frame; in Tailwind that's max-w-2xl (or max-w-md, max-w-4xl). The element fills up to that ceiling, then stops.
The container utility bundles this pattern: it centers content and applies sensible max-widths that step up per breakpoint. Wrap your whole page in container mx-auto px-4 and the outer margins take care of themselves.
Worked example
Every Figma resizing choice has a Tailwind class. You already met these for single components — here they govern whole blocks.
| Figma resizing | means | Tailwind |
|---|---|---|
| Fill container | grow to fill the parent | w-full or flex-1 |
| Hug contents | shrink to fit children | w-auto / w-fit |
| Fixed width | a set pixel width | w-[360px] (avoid on blocks) |
| Max width on frame | cap how wide it grows | max-w-2xl |
| Centered, capped | centered column with a ceiling | container mx-auto |
Rule of thumb for blocks: Fill the width, cap with max-w, center with mx-auto. Fixed widths are for icons and avatars, not sections.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Gordon builds the hero at a comfortable Fixed width of 1200px because that's how it looks on his monitor.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: It pins 1200 pixels. On a 375px phone the frame overflows the screen and the page scrolls sideways.
Gordon sets the hero to Fill and caps its inner content with a max-width.
Why: It pins 1200 pixels. On a 375px phone the frame overflows the screen and the page scrolls sideways.
Trap
Gordon builds the hero at a comfortable Fixed width of 1200px because that's how it looks on his monitor.
Sets the hero frame to Fixed 1200px
Why: It pins 1200 pixels. On a 375px phone the frame overflows the screen and the page scrolls sideways.
In code this is w-[1200px]
Why: A hard pixel width can't shrink; the browser shows a horizontal scrollbar and the content is cut off on mobile.
Gordon sets the hero to Fill and caps its inner content with a max-width.
Sets the hero frame to Fill container
Why: It grows to the screen width at every size — 375px, 768px, 1280px — with no overflow.
In code this is w-full max-w-6xl mx-auto
Why: Fills up to a 1152px ceiling, then centers. Wide on desktop, edge-to-edge and readable on mobile.
Concept
Figure (svg): A 3-across card row reflowing to a single stacked column on mobile
Turn on wrap on a horizontal Auto Layout and its children flow onto new lines when they run out of room — a row of three cards becomes two-and-one, then a single column, as the frame narrows. In Tailwind flexbox that's flex-wrap.
For a fixed count per breakpoint, a grid is cleaner: grid grid-cols-1 md:grid-cols-3 gives exactly one column on mobile and three from md up. Use wrap when the count can be fluid; use grid when you want a definite layout at each breakpoint.
Either way the reflow is built into the layout, not something you position by hand. That's what makes it survive a screen size you didn't test.
Socratic
Discussion prompt
Either way the reflow is built into the layout, not something you position by hand. That's what makes it survive a screen size you didn't test.
Suppose that were not true. What is the first thing in Session 5: Composition — Responsive Layouts, Blocks & Pages that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Intuition
You already know Auto Layout is flexbox: vertical/horizontal → flex-col/flex-row, gap → gap-*, wrap → flex-wrap. Responsive just means swapping that direction at a breakpoint: flex-col md:flex-row is 'stacked on mobile, side by side from md up'.
So the entire responsive toolkit is two moves you already have — change direction (flex-col md:flex-row) or change column count (grid-cols-1 md:grid-cols-3) — gated by a breakpoint prefix. Nothing new to learn, just where to put the md:.
Concept
You can stack more than one override. Prefixes cascade upward: each applies from its width until a larger one takes over. grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 walks one → two → three columns as the screen grows.
| width | active rule | columns |
|---|---|---|
| <640px | grid-cols-1 | 1 |
| 640–1023px | sm:grid-cols-2 | 2 |
| ≥1024px | lg:grid-cols-3 | 3 |
You rarely need all four breakpoints. For Aster, md: alone carries most blocks — reach for sm:/lg: only where a two-step transition genuinely reads better.
Pattern
Step through it
Step through Layering breakpoints: base then sm then lg one row at a time. What is driving the change, and what would the row after the last one be?
Worked example
The core responsive class of the whole session. Read it left to right: mobile first, then the md: override.
<section className="grid grid-cols-1 gap-6 md:grid-cols-3">
<FeatureCard />
<FeatureCard />
<FeatureCard />
</section>grid-cols-1 is the mobile default; md:grid-cols-3 overrides it from 768px up. gap-6 (24px) is unprefixed, so the gap holds at every width.
| screen width | active grid rule | layout |
|---|---|---|
| 375px (phone) | grid-cols-1 | cards stacked, one column |
| 700px (small tablet) | grid-cols-1 | still one column (below md) |
| 768px+ (md and up) | md:grid-cols-3 | three cards across |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Gordon wants three columns on desktop and one on mobile, and writes the prefix on the mobile intention.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: This reads: three columns by default (so on mobile too), collapsing to one from 768px up — the exact opposite of what he wants.
Remember mobile-first: the base class is the mobile layout, md: adds the desktop override.
Why: This reads: three columns by default (so on mobile too), collapsing to one from 768px up — the exact opposite of what he wants.
Trap
Gordon wants three columns on desktop and one on mobile, and writes the prefix on the mobile intention.
Writes grid-cols-3 md:grid-cols-1
Why: This reads: three columns by default (so on mobile too), collapsing to one from 768px up — the exact opposite of what he wants.
Result on a phone
Why: Three squished columns on a 375px screen; the cards are unreadable. Desktop shows a single lonely column.
Remember mobile-first: the base class is the mobile layout, md: adds the desktop override.
Writes grid-cols-1 md:grid-cols-3
Why: One column by default (mobile), three from md up. Base describes the small screen; the prefix upgrades the big one.
Result on a phone
Why: A clean single column on mobile, three tidy cards on desktop — because the unprefixed class is always the mobile case.
Break the constraint
Discussion prompt
The rule this trap just fixed:
One column by default (mobile), three from md up. Base describes the small screen; the prefix upgrades the big one.
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:
This reads: three columns by default (so on mobile too), collapsing to one from 768px up — the exact opposite of what he wants.
Ranking
Put in order
These are the steps of How to compose a responsive page, scrambled. Put them back in order before the next slide shows you.
md:/lg: overrides for wider screens — change direction (md:flex-row) or column count (md:grid-cols-3).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 block you build today — and the whole Aster page — follows the same four moves:
md:/lg: overrides for wider screens — change direction (md:flex-row) or column count (md:grid-cols-3).Say it as a sentence: base is mobile, md: upgrades to desktop, values stay on the scale, verify at each width.
Edge cases
Discussion prompt
How to compose a responsive page 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 block you build today — and the whole Aster page — follows the same four moves:
Elimination
Eliminate the wrong options
A section has className="flex flex-col md:flex-row gap-4". How does it lay out on a 375px phone?
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: Tailwind is mobile-first: the unprefixed flex-col is the base style and applies at every width unless overridden. md:flex-row only turns on at 768px and up. At 375px (below md) the base wins, so the section is a vertical column.
Check
A visitor opens Aster on a 375px-wide phone. Apply the mobile-first rule.
Check your understanding
A section has className="flex flex-col md:flex-row gap-4". How does it lay out on a 375px phone?
Answer: B
Why: Tailwind is mobile-first: the unprefixed flex-col is the base style and applies at every width unless overridden. md:flex-row only turns on at 768px and up. At 375px (below md) the base wins, so the section is a vertical column.
Prediction
Predict first
A grid has className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3". How many columns show at 800px wide?
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: 2 columns — sm applies (≥640) but lg (≥1024) does not.
Why: Prefixes apply from their width upward until a larger one takes over. At 800px, sm (≥640) is active and lg (≥1024) is not yet, so sm:grid-cols-2 governs — two columns show.
Check
Apply the cascade rule to a specific width.
Check your understanding
A grid has className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3". How many columns show at 800px wide?
Answer: B
Why: Prefixes apply from their width upward until a larger one takes over. At 800px, sm (≥640) is active and lg (≥1024) is not yet, so sm:grid-cols-2 governs — two columns show.
Section
Section 3
Concept
Almost every marketing page is the same five blocks in the same order: navbar, hero, feature grid, pricing, footer. Learn the responsive move for each once and you can compose any landing page.
For each block we'll do a short worked example and read the responsive classes off it — the class string tells you the whole story of how it behaves from phone to desktop.
Explain it
Discussion prompt
Explain Five blocks every landing page has 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:
Almost every marketing page is the same five blocks in the same order: navbar, hero, feature grid, pricing, footer. Learn the responsive move for each once and you can compose any landing page.
Concept
Every block shares the same horizontal breathing room — the gutter between content and the screen edge. Rather than repeat max-w-* mx-auto px-4 on each block, wrap the page or each section in a container and let it center and cap the content.
In Tailwind, container mx-auto px-4 centers the content and applies a max-width that steps up per breakpoint — comfortable on a phone, capped on a huge monitor. In Figma this is a fixed-max-width Auto Layout frame with horizontal padding.
Set the gutter once and every block inherits it. Consistent gutters are half of what makes a page feel 'designed' rather than assembled.
Analogy
Discussion prompt
Explain The container: consistent page gutters 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:
Set the gutter once and every block inherits it. Consistent gutters are half of what makes a page feel 'designed' rather than assembled.
Worked example
The navbar is a horizontal Auto Layout: logo pinned left, links in the middle, CTA pinned right. On mobile the links collapse into a menu button.
<nav className="flex items-center justify-between px-4 py-3">
<Logo />
<div className="hidden md:flex gap-6">
<a>Work</a><a>About</a><a>Pricing</a>
</div>
<Button className="md:hidden">Menu</Button>
</nav>hidden md:flex — the links are hidden by default (mobile) and appear as a flex row from md up. md:hidden — the Menu button shows on mobile and hides on desktop. They're mirror images.
| element | class | phone (<768) | desktop (≥768) |
|---|---|---|---|
| links row | hidden md:flex | hidden | visible row |
| Menu button | md:hidden | visible | hidden |
Sorting
Sort into buckets
These are the pieces of Session 5: Composition — Responsive Layouts, Blocks & Pages, out of order. Put each one back under the part of the lesson it belongs to.
Worked example
The hero is a centered column — heading, subcopy, CTA — capped with a max-width so the copy doesn't sprawl. The headline steps down the type scale on smaller screens.
<section className="flex flex-col items-center text-center gap-4 max-w-2xl mx-auto px-4 py-16">
<h1 className="text-4xl md:text-6xl font-semibold">Design that ships.</h1>
<p className="text-base text-muted-foreground">Aster builds themed, coded sites.</p>
<Button size="lg">Start a project</Button>
</section>text-4xl md:text-6xl — 2.25rem on mobile, 3.75rem from md up. Both are real steps on the scale, not made-up sizes. max-w-2xl mx-auto keeps the column centered and readable.
| screen | active heading size | rem |
|---|---|---|
| phone (<768) | text-4xl | 2.25rem |
| desktop (≥768) | text-6xl | 3.75rem |
Worked example
Three feature Cards, each with an icon, title, and line of copy. The grid is the reflow you've already seen — the block just fills it with themed Cards.
<section className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto px-4 py-12">
<FeatureCard title="Themed" />
<FeatureCard title="Coded" />
<FeatureCard title="Responsive" />
</section>grid-cols-1 md:grid-cols-3 is the whole responsive story; gap-6 (24px) holds the rhythm at every width. In Figma: a wrapping horizontal Auto Layout, or a fixed-count grid frame.
Worked example
Three plan Cards side by side on desktop; on a phone they stack. Here we use flex with wrap so the cards break naturally as space runs out.
<section className="flex flex-wrap justify-center gap-6 max-w-5xl mx-auto px-4 py-12">
<PriceCard plan="Starter" className="w-full md:w-72" />
<PriceCard plan="Studio" className="w-full md:w-72" />
<PriceCard plan="Agency" className="w-full md:w-72" />
</section>flex-wrap lets the row break onto new lines. Each card is w-full on mobile (one per line) and a fixed md:w-72 on desktop, so three fit across. In Figma: horizontal Auto Layout with wrap on.
| screen | card width | result |
|---|---|---|
| phone (<768) | w-full | one card per row, stacked |
| desktop (≥768) | md:w-72 | three cards wrap into one row |
Comparison
Comparison matrix
From Pricing: a row of cards that wraps: refill the result column from what you know. The rest of the table is as it appeared.
| screen | card width | result |
|---|---|---|
| phone (<768) | w-full | one card per row, stacked |
| desktop (≥768) | md:w-72 | three cards wrap into one row |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Gordon builds the pricing row in Figma as a horizontal Auto Layout, sizes the cards to Fill, but leaves wrap off.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: With wrap off, the cards can never move to a second line — they just squeeze thinner and thinner as the frame narrows.
Turn wrap on in Auto Layout — the reflow is a property, not something width alone triggers.
Why: With wrap off, the cards can never move to a second line — they just squeeze thinner and thinner as the frame narrows.
Trap
Gordon builds the pricing row in Figma as a horizontal Auto Layout, sizes the cards to Fill, but leaves wrap off.
Three Fill cards in a no-wrap horizontal Auto Layout
Why: With wrap off, the cards can never move to a second line — they just squeeze thinner and thinner as the frame narrows.
In code: flex with no flex-wrap
Why: On a phone the three cards cram into one line, each far too narrow to read. Nothing breaks onto a new row.
Turn wrap on in Auto Layout — the reflow is a property, not something width alone triggers.
Enable wrap on the horizontal Auto Layout
Why: Now, when a card can't fit, it moves to the next line — the row becomes a column on narrow screens.
In code: flex flex-wrap
Why: flex-wrap is what lets the line break; with w-full cards on mobile, each takes its own row cleanly.
Worked example
The footer is several link columns plus legal text. On desktop the columns sit in a row; on mobile they stack into one column.
<footer className="flex flex-col md:flex-row justify-between gap-8 px-4 py-10 border-t">
<FooterCol title="Product" />
<FooterCol title="Company" />
<FooterCol title="Legal" />
</footer>flex-col md:flex-row — the classic direction swap: stacked on mobile, a row from md up. justify-between spreads the columns across the width on desktop.
Worked example
Every landing block reduces to one responsive move. Learn this table and you can read — or write — any of them at a glance.
| block | responsive move | the class |
|---|---|---|
| Navbar | hide links, show menu | hidden md:flex / md:hidden |
| Hero | step type down | text-4xl md:text-6xl |
| Feature grid | 1 col → 3 cols | grid-cols-1 md:grid-cols-3 |
| Pricing | row that wraps | flex flex-wrap |
| Footer | stack → row | flex-col md:flex-row |
Notice the pattern under the pattern: it's always direction, count, or visibility, gated by md:. Three levers cover the whole page.
Commit first
Predict first
Which class string makes a links row hidden on phones and a visible flex row on desktop (≥768px)?
Commit to an answer, then rate it — certain, fairly sure, or guessing — and write the rating down before you turn the page.
Correct: hidden md:flex
Why: hidden is the base (mobile) state, so the row is hidden on phones. md:flex overrides it from 768px up, showing the links as a flex row on desktop. That's exactly the desktop-links / mobile-menu pattern.
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
Gordon wants the nav links visible on desktop, hidden on mobile, replaced by a menu button.
Check your understanding
Which class string makes a links row hidden on phones and a visible flex row on desktop (≥768px)?
Answer: B
Why: hidden is the base (mobile) state, so the row is hidden on phones. md:flex overrides it from 768px up, showing the links as a flex row on desktop. That's exactly the desktop-links / mobile-menu pattern.
Section
Section 4
Concept
The temptation on mobile is to nudge a value to a random number that 'looks right' — a text-[19px] heading, a gap-[13px]. Every off-scale value is a small debt that makes the page feel subtly inconsistent.
Instead, step down the same scale. A text-2xl heading becomes text-xl, not text-[1.15rem]. A gap-8 (32px) section becomes gap-6 (24px), never gap-7px. You're moving to the next rung, not building a new ladder.
This is why the token system pays off: mobile and desktop share one scale, so the page reads as one design at every width — just tighter on small screens.
Intuition
Picture your type sizes and spacing as a ladder with fixed rungs. Going responsive is stepping down a rung or two for mobile — not sawing a new custom rung into the middle. Every element still lands on the same shared ladder.
That's why a well-built responsive page looks like one design breathing in and out, not two designs bolted together: mobile and desktop are the same ladder viewed at two heights.
Concept
Spacing rides the same discipline. Tailwind's base unit is 0.25rem = 4px, so gap-2 = 8px, gap-4 = 16px, gap-6 = 24px, gap-8 = 32px. Every gap and padding on the page should be one of these steps — at mobile and at desktop.
When you tighten spacing for a phone, drop to a lower step on the same 4px system: gap-8 → gap-6, py-16 → py-8. Never reach for gap-[13px] — that's a rung that exists nowhere else on the page.
| step | px | typical use |
|---|---|---|
| gap-2 | 8px | tight inline spacing |
| gap-4 | 16px | default block gap |
| gap-6 | 24px | between cards |
| gap-8 | 32px | between sections' inner parts |
Worked example
Take the hero heading and section padding. The mobile values are lower rungs of the same scale, chosen with a breakpoint prefix on the desktop step.
<h1 className="text-3xl md:text-5xl">Aster</h1>
<section className="py-8 md:py-16 gap-4 md:gap-8">Base = mobile step (text-3xl, py-8, gap-4); md: = desktop step (text-5xl, py-16, gap-8). No custom brackets anywhere.
| property | mobile (base) | desktop (md:) |
|---|---|---|
| heading | text-3xl (1.875rem) | text-5xl (3rem) |
| section padding | py-8 (32px) | py-16 (64px) |
| inner gap | gap-4 (16px) | gap-8 (32px) |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Gordon designs the whole Aster page at 1440px, ships it, and assumes the browser will 'figure out' mobile.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: With no responsive classes, the desktop layout applies at every width.
Gordon designs the mobile frame first, then adds md: overrides for desktop — two frames, both intentional.
Why: With no responsive classes, the desktop layout applies at every width. The 3-across grid stays 3-across on a 375px phone.
Trap
Gordon designs the whole Aster page at 1440px, ships it, and assumes the browser will 'figure out' mobile.
Only a desktop frame exists; no md/base decisions made
Why: With no responsive classes, the desktop layout applies at every width. The 3-across grid stays 3-across on a 375px phone.
On a real phone
Why: Cramped columns, an overflowing hero, links spilling off the edge. 'Mobile' was never actually designed — it just inherited desktop.
Gordon designs the mobile frame first, then adds md: overrides for desktop — two frames, both intentional.
Sketch the 375px frame: everything stacked, base classes
Why: Mobile is the default the code actually ships, so it's the layout you should design first, not last.
Add md: overrides for the 1440px frame
Why: Desktop is the enhancement layered on top. Now both breakpoints are deliberate and verified.
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.
Prediction
Predict first
The desktop heading is text-2xl (1.5rem) and needs to be smaller on mobile. Best practice is:
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: text-xl md:text-2xl — step down to the next rung on the scale for mobile.
Why: Keep values on the type scale: step the base (mobile) down one rung to text-xl (1.25rem) and let md:text-2xl restore 1.5rem on desktop. Same scale, mobile-first — no one-off sizes to maintain.
Check
On mobile a text-2xl heading feels a touch large. What's the on-system fix?
Check your understanding
The desktop heading is text-2xl (1.5rem) and needs to be smaller on mobile. Best practice is:
Answer: B
Why: Keep values on the type scale: step the base (mobile) down one rung to text-xl (1.25rem) and let md:text-2xl restore 1.5rem on desktop. Same scale, mobile-first — no one-off sizes to maintain.
Section
Section 5 · Your turn
Concept
You'll compose the whole Aster homepage from your themed components at two breakpoints — desktop and mobile. Five milestones, one per block. For each: do the task, peek at the hint only if stuck, then check your responsive classes against the solution and the self-check table.
Golden rule for the build: base class = mobile, md: = desktop, every value on the scale. Say it before each block.
| # | block | responsive move |
|---|---|---|
| 1 | Navbar | hidden md:flex links + md:hidden menu |
| 2 | Hero | text-4xl md:text-6xl, centered max-w |
| 3 | Feature grid | grid-cols-1 md:grid-cols-3 |
| 4 | Pricing | flex-wrap row of cards |
| 5 | Footer | flex-col md:flex-row |
Counterexample
Discussion prompt
Golden rule for the build: base class = mobile, md: = desktop, every value on the scale. Say it before each block.
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
Before Milestone 1, create two frames: a 375px mobile frame and a 1440px desktop frame. Build each block in the mobile frame first, then widen your thinking to the desktop frame — mirroring how the base and md: classes work in code.
Give both frames a vertical Auto Layout so blocks stack automatically, and set a container-style horizontal padding (px-4) so every block shares the same gutter. Now you're building the same page at two widths, not two pages.
Worked example
Your turn: build the navbar as a horizontal Auto Layout — logo left, links center, CTA right. The links must hide on mobile and a Menu button take their place.
Hint: put hidden md:flex on the links row, and md:hidden on the Menu button. Space the outer items with justify-between.
<nav className="flex items-center justify-between px-4 py-3">
<Logo />
<div className="hidden md:flex gap-6">
<a>Work</a><a>About</a><a>Pricing</a>
</div>
<Button variant="outline" className="md:hidden">Menu</Button>
</nav>| element | class | self-check |
|---|---|---|
| links row | hidden md:flex | hidden <768, row ≥768 ✓ |
| Menu button | md:hidden | shown <768, hidden ≥768 ✓ |
| outer layout | justify-between | logo left, CTA right ✓ |
Worked example
Your turn: build a centered hero — heading, subcopy, CTA — capped with a max-width. The heading should be smaller on mobile, larger on desktop, both on the scale.
Hint: text-4xl md:text-6xl on the h1; wrap the column in max-w-2xl mx-auto text-center.
<section className="flex flex-col items-center text-center gap-4 max-w-2xl mx-auto px-4 py-12 md:py-20">
<h1 className="text-4xl md:text-6xl font-semibold">Design that ships.</h1>
<p className="text-muted-foreground">Themed, coded sites — built by Aster.</p>
<Button size="lg">Start a project</Button>
</section>| property | mobile | desktop | self-check |
|---|---|---|---|
| heading | text-4xl (2.25rem) | text-6xl (3.75rem) | both on scale ✓ |
| section padding | py-12 (48px) | py-20 (80px) | stepped on scale ✓ |
| width | max-w-2xl mx-auto | same | centered + capped ✓ |
Worked example
Your turn: lay out three feature Cards. One column on mobile, three across on desktop, with a consistent gap held at every width.
Hint: grid grid-cols-1 md:grid-cols-3 gap-6. Keep the gap unprefixed so it never changes.
<section className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto px-4 py-12">
<FeatureCard title="Themed" />
<FeatureCard title="Coded" />
<FeatureCard title="Responsive" />
</section>| screen | columns | self-check |
|---|---|---|
| phone (<768) | 1 | grid-cols-1 base ✓ |
| desktop (≥768) | 3 | md:grid-cols-3 override ✓ |
| gap | 24px both | gap-6 unprefixed ✓ |
Trade off
Comparison matrix
From Milestone 3 — Feature grid 1 → 3: every row here is a choice with a cost. Fill the self-check column, then say which row you would actually pick and what you give up for it.
| screen | columns | self-check |
|---|---|---|
| phone (<768) | 1 | grid-cols-1 base ✓ |
| desktop (≥768) | 3 | md:grid-cols-3 override ✓ |
| gap | 24px both | gap-6 unprefixed ✓ |
Worked example
Your turn: three plan Cards. Stacked on mobile, wrapping into one row on desktop. Each card full-width on mobile, a fixed width on desktop.
Hint: flex flex-wrap justify-center gap-6 on the section; w-full md:w-72 on each card. In Figma this is horizontal Auto Layout with wrap on.
<section className="flex flex-wrap justify-center gap-6 max-w-5xl mx-auto px-4 py-12">
<PriceCard plan="Starter" className="w-full md:w-72" />
<PriceCard plan="Studio" className="w-full md:w-72" />
<PriceCard plan="Agency" className="w-full md:w-72" />
</section>| screen | card width | self-check |
|---|---|---|
| phone (<768) | w-full | one per row, stacked ✓ |
| desktop (≥768) | md:w-72 | three wrap into a row ✓ |
| container | flex-wrap | row can break ✓ |
Comparison
Comparison matrix
From Milestone 4 — Pricing row that wraps: refill the self-check column from what you know. The rest of the table is as it appeared.
| screen | card width | self-check |
|---|---|---|
| phone (<768) | w-full | one per row, stacked ✓ |
| desktop (≥768) | md:w-72 | three wrap into a row ✓ |
| container | flex-wrap | row can break ✓ |
Worked example
Your turn: a footer with three link columns plus a border on top. Stacked column on mobile, spread into a row on desktop.
Hint: flex flex-col md:flex-row justify-between gap-8 — the classic direction swap. Add border-t for the divider.
<footer className="flex flex-col md:flex-row justify-between gap-8 px-4 py-10 border-t">
<FooterCol title="Product" />
<FooterCol title="Company" />
<FooterCol title="Legal" />
</footer>| screen | direction | self-check |
|---|---|---|
| phone (<768) | flex-col | columns stacked ✓ |
| desktop (≥768) | md:flex-row | columns in a row ✓ |
| spacing | gap-8 (32px) | on the scale ✓ |
Worked example
Stack all five blocks in the page's vertical Auto Layout. Each block owns its own responsive behaviour, so the page just lists them.
export default function Home() {
return (
<main className="flex flex-col min-h-screen">
<Navbar />
<Hero />
<FeatureGrid />
<Pricing />
<Footer />
</main>
)
}Resize the browser from 1440px down to 375px and watch: nav links fold into a menu, the hero shrinks, the grid goes 3 → 1, pricing stacks, the footer stacks. Nothing was positioned by hand — the classes did it.
| block | 1440px (desktop) | 375px (phone) |
|---|---|---|
| Navbar | links visible | menu button |
| Hero | text-6xl | text-4xl |
| Feature grid | 3 columns | 1 column |
| Pricing | row of 3 | stacked |
| Footer | row | stacked |
Worked example
Put your 375px and 1440px Figma frames next to each other. They should read as the same design at two sizes — same colors, same type scale, same spacing rhythm — not two different pages.
Figure (svg): Phone frame with stacked blocks beside a desktop frame with rows and a 3-column grid
If they look like siblings, you nailed it: you designed a complete, responsive page entirely from themed shadcn components — the goal of this session.
Concept
Homework: finish any sections you sketched but didn't build out — a testimonials block, an FAQ, a secondary CTA — each as one more block slotted into the vertical stack, at both breakpoints.
Then audit every value against the token system: is each font size a real text-* step? Is every gap and padding on the 4px scale? Any custom text-[..] or w-[..]px you can pull back onto the scale? Any Fixed-width block that should be Fill + max-w?
Bring the two frames — 375px and 1440px — to next session. In Session 6 you'll take this exact page to code and handoff: npx shadcn@latest init, Dev Mode, and honest Figma-variable → Tailwind-class mapping.
Elimination
Eliminate the wrong options
The single most important reason the Aster page reflows correctly across breakpoints is:
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: A
Why: Reflow is built into each block: widths are Fill (w-full) capped with max-w rather than fixed, and breakpoint classes like grid-cols-1 md:grid-cols-3 and flex-col md:flex-row change layout at defined widths. The mobile-first base plus md: overrides make one page adapt itself.
Check
Gordon's page reflows perfectly from 1440px to 375px without him repositioning anything. What made that possible?
Check your understanding
The single most important reason the Aster page reflows correctly across breakpoints is:
Answer: A
Why: Reflow is built into each block: widths are Fill (w-full) capped with max-w rather than fixed, and breakpoint classes like grid-cols-1 md:grid-cols-3 and flex-col md:flex-row change layout at defined widths. The mobile-first base plus md: overrides make one page adapt itself.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Components → Blocks → Pages · Responsive Design in Figma · Building the Usual Suspects · Consistency Across Breakpoints · Build It: The Aster Homepage. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
md:/lg: = overrides that turn on at that width and up.max-w-* instead of fixed widths.hidden md:flex), hero (text-4xl md:text-6xl), grid (grid-cols-1 md:grid-cols-3), pricing (flex-wrap), footer (flex-col md:flex-row).| want | Tailwind move |
|---|---|
| one col → three | grid-cols-1 md:grid-cols-3 |
| stack → row | flex-col md:flex-row |
| hide on mobile | hidden md:flex |
| fill but capped | w-full max-w-* mx-auto |
| row that wraps | flex flex-wrap |
Next time (Session 6): take this exact responsive Aster page to code and handoff — npx shadcn@latest init, adding components, Dev Mode & Code Connect, and honest Figma-variable → Tailwind-class mapping.
Want this taught 1-on-1? Alexander tutors shadcn/ui + Figma — $55/session, free consultation.