Session 1 of the "Designing & Shipping with shadcn/ui in Figma" series, written for Gordon, a designer building the Aster studio landing page. It closes the design-to-code gap by teaching the three layers: Tailwind supplies utility-first CSS, Radix supplies headless accessible behavior, and shadcn/ui supplies styled components that you copy into your repo and then own. The deck's key idea is the difference between copying components in and depending on a closed library. It includes a guided tour of the Figma shadcn kit - the Pages, the Assets panel, and the Variables panel - mapped one-to-one onto code, a "which layer owns this?" decision recipe, four checks, three traps, and a scaffolded your-turn build in which Gordon recreates an Aster profile card from the kit's Card, Avatar, Badge, and Button parts. The homework is to browse ui.shadcn.com and list five components the Aster site will lean on.
Subject: shadcn/ui + Figma · 97 slides · applied lesson
Open the interactive version of this deck · Homework for this lesson
Title
Session 1 · shadcn/ui + Figma
Designers hand off pixels; developers rebuild in a system. This series closes that gap - starting with how Tailwind, Radix and shadcn actually relate.
Objectives
You already know Figma. By the end of this session you can speak the three words developers use all day - Tailwind, Radix, shadcn - and know exactly which one does what. Concretely, you can:
Concept
Figure (svg): A design artboard on the left and a code file on the right with a jagged gap between them
The classic handoff: a designer exports a pixel-perfect frame, a developer eyeballs it and rebuilds it from scratch in their own system. Spacing drifts, colors get re-picked, and the two artifacts slowly diverge.
The fix isn't a better export - it's a shared system on both sides. When your Figma kit and the code are built from the same components and the same tokens, handoff becomes translation, not reconstruction.
This session gives you the vocabulary for that shared system. Everything after builds your Aster studio landing page on top of it.
Concept
The running project across all six sessions is Aster - a landing page for your own freelance design studio. Brand: a violet primary oklch(0.55 0.24 285), base radius 0.625rem, project folder aster-site.
The page has five sections we'll return to constantly: navbar, hero, feature grid, pricing, footer. Today stays entirely in Figma - no terminal yet - but every example pushes Aster one step forward.
Section
Part 1
Concept
Tailwind is a utility-first CSS system. Instead of writing a stylesheet with named classes like .card { padding: 16px }, you compose the style directly in the markup out of tiny single-purpose classes.
Tailwind — A utility-first CSS system: you style an element by composing many small, single-purpose classes (p-4, flex, gap-2, bg-primary) right on the element, instead of writing separate CSS rules.
Each class maps to one declaration. p-4 sets padding: 1rem, flex sets display: flex, gap-2 sets gap: 0.5rem. Nothing is prebuilt - Tailwind gives you the paint, not the furniture.
Counterexample
Discussion prompt
Each class maps to one declaration. p-4 sets padding: 1rem, flex sets display: flex, gap-2 sets gap: 0.5rem. Nothing is prebuilt - Tailwind gives you the paint, not the furniture.
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
Here's a small chunk of the Aster hero's call-to-action row. Every class is one CSS declaration - read it left to right.
<div class="flex items-center gap-3 p-4">
<!-- flex row, vertically centered, 12px gap, 16px padding -->
</div>The spacing scale is fixed and predictable: base unit is 0.25rem = 4px, so gap-3 = 12px and p-4 = 16px. No magic numbers - the scale keeps every gap on the grid.
| class | CSS it produces |
|---|---|
| flex | display: flex |
| items-center | align-items: center |
| gap-3 | gap: 0.75rem (12px) |
| p-4 | padding: 1rem (16px) |
Comparison
Comparison matrix
From Tailwind: a class string, decoded: refill the CSS it produces column from what you know. The rest of the table is as it appeared.
| class | CSS it produces |
|---|---|
| flex | display: flex |
| items-center | align-items: center |
| gap-3 | gap: 0.75rem (12px) |
| p-4 | padding: 1rem (16px) |
Concept
Radix (Radix Primitives) is a set of headless behavior primitives. It ships the hard, invisible parts of an interactive component - focus management, keyboard navigation, ARIA roles - and no styling at all.
Radix — Unstyled ('headless') accessible UI primitives. Radix provides behavior and accessibility - focus trapping, keyboard nav, correct ARIA roles - but ships zero visual styling; you bring the skin.
Think of a dialog: Radix guarantees focus is trapped inside it, Esc closes it, focus returns to the trigger, and screen readers announce it correctly. What it looks like is entirely up to you.
Analogy
Discussion prompt
Explain Layer 2 - Radix 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:
Think of a dialog: Radix guarantees focus is trapped inside it, Esc closes it, focus returns to the trigger, and screen readers announce it correctly. What it looks like is entirely up to you.
Worked example
A raw Radix primitive renders correct structure and behavior but is visually unstyled - it looks like nothing until you add classes.
// Radix gives the behavior + ARIA, no styling:
<Dialog.Root>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Content>...</Dialog.Content>
</Dialog.Root>Out of the box this traps focus, wires aria-* attributes, and handles Esc and Tab. But it has no padding, no background, no rounded corners. Radix is the skeleton.
Getting accessibility right by hand is genuinely hard. Radix is why shadcn components are accessible without you having to think about ARIA on day one.
Concept
shadcn/ui is the assembled layer: styled components built by putting a Tailwind skin on Radix behavior. A shadcn Button is Radix-grade behavior plus a class string that gives it Aster's look.
shadcn/ui — A collection of styled UI components built on Radix primitives and Tailwind. You copy the component source into your own repo (components/ui/) and own it - it is not an installed, locked dependency.
Crucially, shadcn is not a package you install and hide behind. You copy each component's source into your project and it becomes your code - the single most important idea in this deck, and the whole of Part 2.
Definition probe
Sort into buckets
Every line below is part of the definition of Radix or of shadcn/ui — one or the other, never both. Put each where it belongs.
Intuition
Figure (svg): Three stacked layers: Radix skeleton at the bottom, Tailwind paint in the middle, shadcn assembled component on top
Read the stack bottom-up. Radix is the skeleton: it moves correctly. Tailwind is the paint: it composes the look. shadcn/ui is the assembled, finished component that combines both - and it lives in your repo.
Each layer has one job. When something's wrong, this stack tells you where to look: broken keyboard focus is a Radix concern; a wrong color is a Tailwind/token concern; a component's whole shape is shadcn.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Assuming Radix is what makes a shadcn Button look good.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: There's nothing to change - Radix ships zero styling.
Radix owns behavior; Tailwind and shadcn own the skin.
Why: There's nothing to change - Radix ships zero styling. You'd find no colors, no padding, no radius in it.
Trap
Assuming Radix is what makes a shadcn Button look good.
Reach for Radix to change the button's color and radius
Why: There's nothing to change - Radix ships zero styling. You'd find no colors, no padding, no radius in it.
Conclude 'Radix must be broken - it has no styles'
Why: It's working as designed. Headless means behavior only; the look was never Radix's job.
Radix owns behavior; Tailwind and shadcn own the skin.
Let Radix handle focus, keyboard, and ARIA
Why: That's the invisible, hard-to-get-right part - exactly what a headless primitive is for.
Change the color and radius via Tailwind classes / tokens in the shadcn component
Why: The look lives in the class string and the CSS variables - the layer you actually edit.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Expecting Tailwind to hand you prebuilt, styled components.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Those don't exist. Bootstrap ships components; Tailwind ships only utilities.
Tailwind gives utilities; the component comes from shadcn (or you).
Why: Those don't exist. Bootstrap ships components; Tailwind ships only utilities.
Trap
Expecting Tailwind to hand you prebuilt, styled components.
Search Tailwind for a ready-made .card or .btn-primary
Why: Those don't exist. Bootstrap ships components; Tailwind ships only utilities.
Assume class="card" will render a styled card
Why: card isn't a Tailwind class. You get an unstyled div - nothing prebuilt appears.
Tailwind gives utilities; the component comes from shadcn (or you).
Compose the look from utilities: rounded-lg border bg-card p-6
Why: You build the card by stacking single-purpose classes - Tailwind is paint, not furniture.
Or drop in the shadcn Card component
Why: The prebuilt, styled component is shadcn's job - built on top of Tailwind utilities.
Break the constraint
Discussion prompt
The rule this trap just fixed:
You build the card by stacking single-purpose classes - Tailwind is paint, not furniture.
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:
Those don't exist. Bootstrap ships components; Tailwind ships only utilities.
Concept
One sentence to keep: shadcn = Radix (behavior) + Tailwind (styling), copied into your repo. Two dependencies do the heavy lifting; the visible component is yours.
| Layer | Owns | Aster example |
|---|---|---|
| Tailwind | utility-first styling (paint) | gap-3 p-4 bg-primary on the hero CTA |
| Radix | behavior + accessibility (skeleton) | focus trap on the pricing FAQ accordion |
| shadcn/ui | assembled styled component you own | the <Button> in the navbar |
Trade off
Comparison matrix
From How the three fit together: every row here is a choice with a cost. Fill the Owns column, then say which row you would actually pick and what you give up for it.
| Layer | Owns | Aster example |
|---|---|---|
| Tailwind | utility-first styling (paint) | gap-3 p-4 bg-primary on the hero CTA |
| Radix | behavior + accessibility (skeleton) | focus trap on the pricing FAQ accordion |
| shadcn/ui | assembled styled component you own | the <Button> in the navbar |
Intuition
Figure (svg): A ruler showing Tailwind spacing steps 1 through 8 mapping to 4, 8, 12, 16, 24, 32 pixels
Tailwind's spacing isn't arbitrary. One base unit is 0.25rem = 4px, and every step is a multiple of it. So the number in the class is the multiplier: p-4 = 4 units = 16px.
This is why designs stay on a grid. If your Figma Auto-Layout gaps are 8 and 16, they land exactly on gap-2 and gap-4 - no odd 13px values to reverse-engineer at handoff.
Explain it
Discussion prompt
Explain Tailwind's spacing scale is a grid 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:
Tailwind's spacing isn't arbitrary. One base unit is 0.25rem = 4px, and every step is a multiple of it. So the number in the class is the multiplier: p-4 = 4 units = 16px.
Concept
A shadcn component isn't a black box - it exposes a small, deliberate set of knobs. The Button, which Aster's navbar and hero both use, has two axes you'll set constantly.
Variants swap the whole look: default, secondary, destructive, outline, ghost, link. Sizes change the footprint: default, sm, lg, icon.
| prop | values | Aster use |
|---|---|---|
| variant | default / secondary / outline / ghost / link | default for the hero CTA, ghost in the navbar |
| size | default / sm / lg / icon | lg for the hero, icon for the menu toggle |
Socratic
Discussion prompt
A shadcn component isn't a black box - it exposes a small, deliberate set of knobs. The Button, which Aster's navbar and hero both use, has two axes you'll set constantly.
Suppose that were not true. What is the first thing in Session 1: Foundations & the Design-to-Code Mental Model that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Worked example
In code, you set variant and size as props. In Figma, the exact same choices are the Button instance's variant properties - the twin relationship you'll rely on all series.
// Aster hero CTA
<Button variant="default" size="lg">Start a project</Button>
// Aster navbar link-style button
<Button variant="ghost" size="sm">Work</Button>Setting variant="ghost" in Figma's properties panel picks the same style set that variant="ghost" picks in code. You are choosing among the component's built-in options, not restyling by hand.
| Figma choice | Code prop |
|---|---|
| Variant = default, Size = lg | variant="default" size="lg" |
| Variant = ghost, Size = sm | variant="ghost" size="sm" |
| Variant = outline, Size = default | variant="outline" |
Concept
A real component has more than one look. Design each meaningful interactive state, because the code will render all of them - and Radix drives when each applies.
focus-visible:ring-*), not mouse clicks.The focus-visible ring is the one designers most often forget. It's an accessibility feature Radix and Tailwind wire up; design it so keyboard users can see where they are on the Aster page.
Worked example
Here is a single Aster CTA with each layer's contribution labeled. Read it as: Radix behavior, Tailwind classes, all wrapped in a shadcn component you own.
// shadcn <Button> (your file) = Radix behavior + Tailwind skin
<Button
className="gap-2 px-4 rounded-lg focus-visible:ring-2"
>
Start a project
</Button>| Piece | Layer | What it does |
|---|---|---|
| <Button> component | shadcn/ui | the assembled component in components/ui/ |
| gap-2 px-4 rounded-lg | Tailwind | spacing and radius (radius via --radius token) |
| focus-visible ring, click/keyboard | Radix | accessible focus + interaction behavior |
Change the look? Edit the classes. Change the behavior? That's Radix underneath. Need a whole new variant? Edit the component file - it's yours. The three-layer map tells you where to go every time.
Section
Part 2 · The key idea
Concept
There are two ways a component library can reach your project, and the difference decides how much control you have.
Closed / installed (MUI, Ant Design): you npm install a package. The component source lives in node_modules/ - code you don't own and shouldn't edit. You style it only from the outside, through the props and theme options the authors exposed.
Copy-in / owned (shadcn/ui): the CLI copies the component's actual source into your repo, at components/ui/. It's now your file. You read it, edit any line, and it updates only when you choose to re-pull it.
Intuition
Figure (svg): Left: a locked box labeled node_modules holding a closed library. Right: an open editable file labeled components ui button tsx inside your repo
Left: a closed library is a locked box in node_modules/. You can pass it props, but you can't open it and change how it's built.
Right: a shadcn component is an open file in your own source tree. Same as any other file you wrote - fully readable, fully editable, versioned in your git history.
Socratic
Discussion prompt
Left: a closed library is a locked box in node_modules/. You can pass it props, but you can't open it and change how it's built.
Suppose that were not true. What is the first thing in Session 1: Foundations & the Design-to-Code Mental Model that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Answer:
Right: a shadcn component is an open file in your own source tree. Same as any other file you wrote - fully readable, fully editable, versioned in your git history.
Worked example
Say Aster needs a special 'brand' button that isn't one of the built-in variants. With shadcn, button.tsx is your file, so you just add a line to the variant map.
// components/ui/button.tsx - your own file
const buttonVariants = cva("...", {
variants: { variant: {
default: "bg-primary text-primary-foreground",
brand: "bg-violet-600 text-white shadow-lg", // added
} },
})Now <Button variant="brand"> works everywhere. You didn't fork a package or fight the library - you edited a file you own.
With a closed library, a variant the authors didn't ship simply isn't available - you'd be stuck overriding styles from the outside.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Treating shadcn like MUI: install it, then wrestle its internals from outside.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: You're fighting a library from the outside because you assume you can't touch its source.
The component's source is already in your repo - just open and edit it.
Why: You're fighting a library from the outside because you assume you can't touch its source.
Trap
Treating shadcn like MUI: install it, then wrestle its internals from outside.
Try to override a component's internal styles with !important in your CSS
Why: You're fighting a library from the outside because you assume you can't touch its source.
Hit a wall: the internal markup you need to change lives in node_modules and won't budge
Why: For a truly closed library that's genuinely impossible - but shadcn was never closed.
The component's source is already in your repo - just open and edit it.
Open components/ui/button.tsx and change the actual line
Why: It's your file. No override war, no !important - you edit the source directly.
Commit the change like any other code
Why: You own it and it's in your git history; updates happen only when you choose to re-pull.
Concept
For a bespoke brand like Aster, ownership is the whole point: no lock-in, full control, and updates on your terms.
Worked example
Here's what ownership saves you from. With a closed library you can't reach the internal element, so you stack ever-more-specific selectors and !important to force a change from outside.
/* fighting a closed library from outside */
.MuiButton-root .MuiButton-label {
color: var(--primary) !important; /* please win */
}It's brittle: a library update can rename that inner class and silently break your override. You never touched the real markup - you just shouted louder than it.
// shadcn: edit the one line in YOUR file instead
// components/ui/button.tsx
"text-primary" // done - no !important, no selector warConcept
'You own it' cuts both ways: nothing auto-updates behind your back. When shadcn improves a component, you choose whether and when to pull the new version in.
Because both your version and the new one are just files, you diff them - see exactly what changed, keep your Aster edits, and take the upstream improvements you want. No surprise breakage on a routine npm update.
Section
Part 3 · Pattern
Ranking
Put in order
These are the steps of Which layer owns this decision?, scrambled. Put them back in order before the next slide shows you.
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
When you hit any UI decision, route it to the layer that owns it. Ask these in order:
One nuance: colors and radius flow through design tokens (CSS variables). So 'change the primary color' is a token change that Tailwind classes then read - not a hand-edit on every component.
Edge cases
Discussion prompt
Which layer owns this decision? 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:
When you hit any UI decision, route it to the layer that owns it. Ask these in order:
Worked example
Four real decisions from the Aster build. For each, ask the recipe's questions and route it to the owning layer before you read the answer.
| Decision | Question it answers | Owner |
|---|---|---|
| Make the hero CTA violet | utility styling, via a token | Tailwind (--primary token) |
| Pricing FAQ closes on Esc | behavior + accessibility | Radix |
| Add an Aster-only 'brand' button style | the assembled component you own | shadcn (edit button.tsx) |
| Bump gap between feature cards to 24px | utility styling (spacing scale) | Tailwind (gap-6) |
Notice the first and last are both Tailwind, but the color one goes through a token so it stays consistent everywhere. The recipe scales to every choice you'll make this series.
Sorting
Sort into buckets
These are the pieces of Session 1: Foundations & the Design-to-Code Mental Model, out of order. Put each one back under the part of the lesson it belongs to.
Elimination
Eliminate the wrong options
Keyboard/Esc handling and focus management for the Aster FAQ accordion - which layer owns it?
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: Focus management, keyboard navigation, and Esc-to-close are behavior and accessibility concerns - exactly what Radix primitives provide. shadcn's Accordion is built on the Radix Accordion, so you get this for free.
Check
Aster's pricing section has an accordion of FAQs. Pressing Esc should close an open item, and focus should stay managed correctly. Which layer owns that?
Check your understanding
Keyboard/Esc handling and focus management for the Aster FAQ accordion - which layer owns it?
Answer: A
Why: Focus management, keyboard navigation, and Esc-to-close are behavior and accessibility concerns - exactly what Radix primitives provide. shadcn's Accordion is built on the Radix Accordion, so you get this for free.
Prediction
Predict first
Why can you change a shadcn Button's internal markup but not a closed library's?
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: shadcn's source is copied into your repo (components/ui/), so it's your editable file; the closed library's source lives in node_modules and is off-limits
Why: The core distinction: shadcn copies the actual component source into your project so you own and edit it directly, while an installed closed library keeps its source in node_modules where you can only configure it from the outside via props/theme.
Check
You need to change the internal markup of a Button - not just its props - so it renders an extra wrapper element. Compare a closed library (MUI) with shadcn.
Check your understanding
Why can you change a shadcn Button's internal markup but not a closed library's?
Answer: C
Why: The core distinction: shadcn copies the actual component source into your project so you own and edit it directly, while an installed closed library keeps its source in node_modules where you can only configure it from the outside via props/theme.
Section
Part 4
Concept
The Figma shadcn kit isn't decoration - it's the design-side twin of the codebase. Three panels do the work, and each maps to something concrete in code.
Matching
Match the pairs
From The kit mirrors the code 1:1 — match each one to what it actually does. The descriptions have been shuffled.
Why: Pages, Assets panel, Variables panel are easy to tell apart while they are sitting next to their descriptions and much harder afterwards, which is what this checks.
Concept
Pages (top-left of the Figma file) split a large kit into navigable sections - for example a cover page, a components page, and an examples page. They're organization only; they don't change how anything renders.
Loosely, a page is like a folder in the project - a way to keep the button gallery separate from the assembled example screens. You'll live mostly on the components and examples pages.
Concept
One mapping unlocks most of Figma-to-Tailwind: Auto Layout is flexbox. The controls you already use in Figma have direct Tailwind twins - so the way you lay out Aster's navbar is the way it's coded.
| Auto Layout setting | Tailwind class |
|---|---|
| Vertical / Horizontal | flex-col / flex-row |
| Gap between items | gap-* (on the scale) |
| Padding | p-* / px-* / py-* |
| Space between | justify-between |
| Resizing: Hug / Fill / Fixed | w-fit / flex-1 (or w-full) / a set width |
Intuition
Figure (svg): A horizontal navbar with a logo on the left and links plus a button pushed to the right by space-between
Aster's navbar is a horizontal Auto-Layout frame with space between the logo and the links. In code that's exactly flex flex-row items-center justify-between - the logo hugs left, the links and Hire button sit right.
You didn't guess the CSS. You read it off the Auto-Layout panel: direction, alignment, and the space-between setting each name their own class.
Socratic
Discussion prompt
You didn't guess the CSS. You read it off the Auto-Layout panel: direction, alignment, and the space-between setting each name their own class.
Suppose that were not true. What is the first thing in Session 1: Foundations & the Design-to-Code Mental Model that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Concept
Small parts compose upward. A component (Button) sits inside a block (a whole navbar or pricing section - shadcn calls these 'blocks'), and blocks compose into a page (the full Aster landing page).
Figma mirrors this: instances nest inside a section frame, sections stack into a screen. Same shape on both sides - which is why the kit's example screens read like real page code.
Concept
Figure (svg): The Assets panel listing components Button, Card, Avatar, Badge, with an arrow dragging a Button instance onto a canvas
The Assets panel is the kit's component library. Every main component - Button, Card, Avatar, Badge - appears here. You drag one onto the canvas to place an instance of it.
Assets panel — The Figma panel that lists all published/available components in a file. Dragging one onto the canvas creates an instance - a live copy that stays linked to the main component.
An instance is the design-side twin of an imported component in code. Placing a Button instance in Figma is the same act as writing <Button /> in a file.
Socratic
Discussion prompt
The Assets panel is the kit's component library. Every main component - Button, Card, Avatar, Badge - appears here. You drag one onto the canvas to place an instance of it.
Suppose that were not true. What is the first thing in Session 1: Foundations & the Design-to-Code Mental Model that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Answer:
An instance is the design-side twin of an imported component in code. Placing a Button instance in Figma is the same act as writing <Button /> in a file.
Concept
The Variables panel holds the kit's design tokens: named values like a primary color or a base radius. Bind a layer's fill to the primary variable and it now references that token instead of a raw hex.
design token — A named, reusable design value (a color, spacing, or radius) stored once and referenced everywhere. In Figma it's a Variable; in code it's a CSS variable like --primary. Change it once, everything bound to it updates.
A bound Figma variable is the exact twin of a CSS variable in globals.css. Set Aster's primary to oklch(0.55 0.24 285) once, and every instance bound to it retints - same as editing --primary in code.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Treating a bound variable as a one-time color pick, like grabbing a saved swatch.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: A saved swatch copies a value in and forgets it - no living link remains.
A bound variable is a living reference - a token, the twin of a CSS variable.
Why: A saved swatch copies a value in and forgets it - no living link remains.
Trap
Treating a bound variable as a one-time color pick, like grabbing a saved swatch.
Think 'the variable just set this fill to violet once'
Why: A saved swatch copies a value in and forgets it - no living link remains.
Expect nothing to change elsewhere when you edit the variable
Why: If it were only a swatch, editing it wouldn't ripple - but that's not what a variable is.
A bound variable is a living reference - a token, the twin of a CSS variable.
Bind the fill to the primary variable
Why: The layer now points at the token; it doesn't copy the value, it references it.
Edit primary once
Why: Every layer bound to it retints at once - exactly like changing --primary in globals.css.
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.
Concept
Component properties in Figma come in kinds, and two matter today. A variant property swaps a whole style set; a boolean property toggles one feature on or off.
Use them the way code does. variant="outline" is a variant (many exclusive options). showIcon is a boolean (a single on/off). Booleans keep the variant set lean - you don't create a separate variant for 'with icon' and 'without icon'.
| Figma property | Kind | Code twin |
|---|---|---|
| variant: default/outline/ghost | variant (pick one) | variant="outline" |
| showIcon: on/off | boolean (toggle) | showIcon or the icon child |
Comparison
Comparison matrix
From Variant vs boolean properties: refill the Code twin column from what you know. The rest of the table is as it appeared.
| Figma property | Kind | Code twin |
|---|---|---|
| variant: default/outline/ghost | variant (pick one) | variant="outline" |
| showIcon: on/off | boolean (toggle) | showIcon or the icon child |
Concept
Here is the whole mental model in one table. Each Figma concept has exactly one code twin - that's what makes handoff a translation.
| Figma | Code twin | Aster example |
|---|---|---|
| Assets instance | imported component | <Button /> in the navbar |
| Bound variable | CSS variable (token) | --primary in globals.css |
| Component property | prop | variant="outline", size="lg" |
| Page | loosely, a folder/section | the 'examples' page |
Ranking
Put in order
Put the moves of Live in Figma: inspect a Button into the order they have to happen.
primary variableWhy: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. This creates an instance - the twin of writing <Button /> in code.
Worked example
Let's read a Button the way its code twin reads. Drop a Button instance on the canvas and open its properties on the right.
Drag Button from the Assets panel onto the canvas
Why: This creates an instance - the twin of writing <Button /> in code.
Open the variant dropdown in the properties panel
Why: You'll see the real shadcn variants: default, secondary, destructive, outline, ghost, link - each maps to a prop value.
Check the boolean property (e.g. showIcon)
Why: A boolean prop toggles one feature on/off, keeping the variant set lean instead of exploding it.
Inspect the fill - it's bound to the primary variable
Why: The bound token is the twin of bg-primary reading --primary; the button follows Aster's brand automatically.
Reverse engineer
Discussion prompt
Work backwards. The example finished here:
Inspect the fill - it's bound to the primary variable
What was it asked to do, and what must it have been given? Reconstruct the problem from its answer.
Hint: Every quantity in the result had to enter somewhere. Account for each one.
Answer:
Let's read a Button the way its code twin reads. Drop a Button instance on the canvas and open its properties on the right.
Prediction
Predict first
You edit the single primary variable from violet to teal in Figma's Variables panel. What happens?
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: Every layer bound to primary retints to teal at once - like changing --primary in globals.css
Why: A bound variable is a living reference, not a copied value. Editing the token updates every layer that references it simultaneously - the exact twin of changing the --primary CSS variable, where every bg-primary re-tints at once.
Check
Every Aster button, badge, and link in the kit has its fill bound to the primary variable. You change primary from violet to teal, once, in the Variables panel.
Check your understanding
You edit the single primary variable from violet to teal in Figma's Variables panel. What happens?
primary retints to teal at once - like changing --primary in globals.css (correct)Answer: A
Why: A bound variable is a living reference, not a copied value. Editing the token updates every layer that references it simultaneously - the exact twin of changing the --primary CSS variable, where every bg-primary re-tints at once.
Elimination
Eliminate the wrong options
Dragging a Card instance from the Assets panel onto the canvas is the design-side twin of what in code?
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: An Assets-panel instance maps 1:1 to an imported component being used. Dragging a Card onto the canvas is the design equivalent of writing <Card /> - you're placing a linked instance of a defined component.
Check
You drag a Card from the Assets panel onto the Aster canvas. In code, what did you just do?
Check your understanding
Dragging a Card instance from the Assets panel onto the canvas is the design-side twin of what in code?
Answer: B
Why: An Assets-panel instance maps 1:1 to an imported component being used. Dragging a Card onto the canvas is the design equivalent of writing <Card /> - you're placing a linked instance of a defined component.
Section
Part 5 · The bridge
Concept
Aster's feature grid is one column on a phone and three on a desktop. In Tailwind that's grid-cols-1 md:grid-cols-3 - the md: prefix applies a class only at that breakpoint and up.
| prefix | kicks in at | Aster use |
|---|---|---|
| (none) | all sizes | grid-cols-1 - stacked on mobile |
| md: | 768px+ | md:grid-cols-3 - three feature cards |
| lg: | 1024px+ | lg:px-8 - roomier page gutters |
In Figma you plan this with breakpoints and constraints (pin / center / scale). The key discipline: keep spacing and type on the same scale across breakpoints, so the grid stays coherent.
Explain it
Discussion prompt
Explain Responsive: same scale, more breakpoints 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:
Aster's feature grid is one column on a phone and three on a desktop. In Tailwind that's grid-cols-1 md:grid-cols-3 - the md: prefix applies a class only at that breakpoint and up.
Concept
You won't hand-write OKLCH values from memory. tweakcn.com is a free visual theme editor for shadcn/ui: tune colors and radius, preview live, and export both a globals.css and a Figma-compatible theme.
That's the round trip you'll use for Aster next session: pick the violet and 0.625rem radius in tweakcn, export the tokens, and the same values drop into :root/.dark in code and into the Figma Variables panel. One source, both sides.
Concept
No terminal today - but here's where this all lands. Next sessions you'll scaffold aster-site and pull components in; each command copies real source into components/ui/.
npx shadcn@latest init # sets up config + globals.css
npx shadcn@latest add button card avatar badge inputEvery part you place from the Figma kit today has a twin you'll add then own. The design work you're about to do IS the plan for that code.
Analogy
Discussion prompt
Explain The scaffold you'll run later 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:
No terminal today - but here's where this all lands. Next sessions you'll scaffold aster-site and pull components in; each command copies real source into components/ui/.
Section
Part 6 · Hands-on
Concept
Time to build. You'll recreate a small Aster profile card entirely from kit parts - no terminal this session. The card shows a team member's avatar, name, an 'Available' badge, and a 'View work' button.
Four kit parts, assembled from the Assets panel. Do each milestone in Figma, then read the self-check to confirm you see the right thing before moving on.
| # | kit part | role in the card |
|---|---|---|
| 1 | Card | the container frame |
| 2 | Avatar | the circular profile image |
| 3 | Badge | the 'Available' status pill |
| 4 | Button | the 'View work' action |
Counterexample
Discussion prompt
Four kit parts, assembled from the Assets panel. Do each milestone in Figma, then read the self-check to confirm you see the right thing before moving on.
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.
Estimation
Predict first
Your turn: place a Card from the kit as the container for everything else. Give it a moment - where in Figma do the kit's components live?
Commit before you compute: what does Milestone 1 - drop a Card frame come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Leave its inner Auto Layout as-is for now
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. The Card's vertical Auto Layout is the twin of a flex-col container; the other parts will stack inside it.
Worked example
Your turn: place a Card from the kit as the container for everything else. Give it a moment - where in Figma do the kit's components live?
Hint: open the Assets panel (the component library) and find Card. You drag from there onto the canvas - you don't draw a rectangle by hand.
Open the Assets panel, drag Card onto the canvas
Why: This places a Card instance - the twin of <Card /> - already styled with Aster's border, radius, and padding tokens.
Leave its inner Auto Layout as-is for now
Why: The Card's vertical Auto Layout is the twin of a flex-col container; the other parts will stack inside it.
| you should see | meaning |
|---|---|
| A rounded, bordered frame | the Card's --radius and --border tokens are applied |
| 'Card' listed as an instance | it's linked to the main component, not a loose shape |
Reverse engineer
Discussion prompt
Work backwards. The example finished here:
Leave its inner Auto Layout as-is for now
What was it asked to do, and what must it have been given? Reconstruct the problem from its answer.
Hint: Every quantity in the result had to enter somewhere. Account for each one.
Answer:
Your turn: place a Card from the kit as the container for everything else. Give it a moment - where in Figma do the kit's components live?
Estimation
Predict first
Your turn: put an Avatar at the top of the card for the profile image. Which panel, again, holds the components?
Commit before you compute: what does Milestone 2 - add the Avatar come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Confirm it renders as a circle
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. The Avatar component ships its own rounded-full styling; you don't set the radius yourself.
Worked example
Your turn: put an Avatar at the top of the card for the profile image. Which panel, again, holds the components?
Hint: back to the Assets panel - find Avatar and drag it inside the Card so it joins the card's Auto Layout.
Drag Avatar from Assets into the Card
Why: Dropping it inside makes it a child of the Card's Auto Layout - the twin of nesting <Avatar /> inside <Card />.
Confirm it renders as a circle
Why: The Avatar component ships its own rounded-full styling; you don't set the radius yourself.
| you should see | meaning |
|---|---|
| A circular avatar at the top | the Avatar instance is placed and styled |
| It sits inside the card, spaced by the gap | it joined the Card's Auto Layout (flex-col + gap) |
Trade off
Comparison matrix
From Milestone 2 - add the Avatar: every row here is a choice with a cost. Fill the meaning column, then say which row you would actually pick and what you give up for it.
| you should see | meaning |
|---|---|
| A circular avatar at the top | the Avatar instance is placed and styled |
| It sits inside the card, spaced by the gap | it joined the Card's Auto Layout (flex-col + gap) |
Estimation
Predict first
Your turn: add a Badge that reads 'Available' as a small status pill under the name. Same source as before.
Commit before you compute: what does Milestone 3 - add the Badge come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Choose a variant (e.g. secondary) from its properties
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. The variant property maps 1:1 to the variant prop - swapping a whole style set, not restyling by hand.
Worked example
Your turn: add a Badge that reads 'Available' as a small status pill under the name. Same source as before.
Hint: drag Badge from the Assets panel, then use its variant property to pick the style, and edit its text to 'Available'.
Drag Badge into the card and set its text to 'Available'
Why: The Badge instance is the twin of <Badge>Available</Badge>; the text is editable like a prop/child.
Choose a variant (e.g. secondary) from its properties
Why: The variant property maps 1:1 to the variant prop - swapping a whole style set, not restyling by hand.
| you should see | meaning |
|---|---|
| A small pill reading 'Available' | the Badge instance is placed with your text |
| Its color follows the chosen variant | the variant property picked a token-driven style |
Worked example
Your turn: finish with a 'View work' Button at the bottom of the card. You know where to get it.
Hint: drag Button from the Assets panel; set its text to 'View work' and confirm its fill is bound to the primary variable so it wears Aster's violet.
Drag Button into the card, set label to 'View work'
Why: The Button instance is the twin of <Button>View work</Button> at the bottom of the card.
Check the fill is bound to primary
Why: The bound token means it already matches Aster's brand - the twin of bg-primary reading --primary.
| you should see | meaning |
|---|---|
| A violet 'View work' button | the fill is bound to the primary token |
| Card, Avatar, Badge, Button all stacked | four instances composed in one Auto Layout |
Worked example
Step back and look at the whole card. Four kit instances, stacked in the Card's Auto Layout, every color and radius coming from Aster's tokens.
Figure (svg): An assembled profile card: a circular avatar at top, a name, an Available badge, and a violet View work button, inside a rounded bordered card
If yours looks like this - a bordered card holding a circular avatar, a name, an 'Available' badge, and a violet button - you assembled a real UI from kit parts, and each part is the twin of a coded component.
Worked example
Your turn: without touching code, explain how each kit part maps to a real coded component. Say it out loud, part by part - this is the fluency the whole series is building.
Hint: each Assets instance is an imported component; each variant/text you set is a prop; each bound fill is a token. Walk the card top to bottom.
| Kit part | Its coded twin |
|---|---|
| Card instance | <Card> - the container component |
| Avatar instance | <Avatar> - imported, renders the circle |
| Badge + variant | <Badge variant="secondary">Available</Badge> |
| Button + primary fill | <Button> reading bg-primary from --primary |
That mapping - instance to component, property to prop, bound variable to token - is the design-to-code model in miniature. Everything in the coming sessions extends it.
Comparison
Comparison matrix
From Show it off - name the code twins: refill the Its coded twin column from what you know. The rest of the table is as it appeared.
| Kit part | Its coded twin |
|---|---|
| Card instance | <Card> - the container component |
| Avatar instance | <Avatar> - imported, renders the circle |
| Badge + variant | <Badge variant="secondary">Available</Badge> |
| Button + primary fill | <Button> reading bg-primary from --primary |
Concept
Homework: browse the component gallery at ui.shadcn.com and get a feel for what's available - Button, Card, Input, Badge, Navigation Menu, and more.
Then write down the five components the Aster site will lean on most across its navbar, hero, feature grid, pricing, and footer. Think about what each section needs - for example a nav/navigation menu, a button, a card, an input, and a badge.
Bring that list to Session 2. It's the shopping list we'll use when we start theming and, eventually, scaffolding the real aster-site project.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — The Three Layers · Copy-In, Own the Code · A 'Which Layer Owns This?' Recipe · Touring the Figma shadcn Kit · From Kit to Real Code · Your Turn: The Aster Profile Card. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
| Figma | Code twin |
|---|---|
| Assets instance | imported component (<Button />) |
| Bound variable | CSS variable / token (--primary) |
| Component property | prop (variant, size) |
| Page | a folder/section, loosely |
Next time (Session 2): design tokens in depth - primitive to semantic to mode, OKLCH, and wiring Aster's violet through :root, .dark, and @theme inline so one variable retints the whole site.
Want this taught 1-on-1? Alexander tutors shadcn/ui + Figma — $55/session, free consultation.