Session 4: Radix & Component Anatomy

Session 4 of the Designing & Shipping with shadcn/ui in Figma series, in which Gordon learns to configure any kit component through its properties and to account for every interaction state. It explains what "headless" means - Radix supplies the behavior and accessibility while shadcn adds the Tailwind skin - and covers Figma component properties, where a variant property swaps a whole style and a boolean property toggles a single feature, demonstrated on the real Button variant and size set. It then works through the five interactive states that matter (default, hover, focus-visible ring, active, and disabled), with particular attention to focus-visible for keyboard accessibility. A scaffolded your-turn build brands the Aster primary Button and Input and documents all their states, alongside the configure-a-component pattern, four checks, and three traps drawn from real designer misconceptions.

Subject: shadcn/ui + Figma · 97 slides · applied lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. Radix & Component Anatomy

Title

Session 4 · shadcn/ui + Figma

What's actually inside a kit component - the headless behavior underneath, the properties on top, and every interaction state you have to design for.

2. What you'll be able to do

Objectives

The big goal: configure any kit component through its properties instead of forking a new one, and account for every state it can be in. Concretely you'll be able to:

3. What survived from Session 3: Design Tokens & Theming — the Heart of It?

Warm-up

Discussion prompt

Before we open Session 4: Radix & Component Anatomy: without looking back, what was the main idea of Session 3: Design Tokens & Theming — the Heart of It, 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 3 of 6, in 60 slides. It covers the three token layers, running from primitive to semantic to mode, and shows how to retheme a whole kit in light and dark by editing a handful of semantic variables. It explains OKLCH and why Tailwind v4 and shadcn adopted it, how one --radius drives the whole corner scale, and how to use tweakcn to dial in Aster's brand theme and export a matching globals.css. Traps cover theming primitives instead of semantics, and forgetting dark mode.

4. Where this sits in the series

Concept

You've already themed Aster with tokens. Now you drop down to the level of a single component - how it's built, how you configure it, and how it behaves under the pointer and the keyboard.

The reason this matters: a component isn't one picture. It's a small machine with inputs (its properties) and modes (its states). You design the inputs and the modes, not one static frame.

5. Break it if you can: Where this sits in the series

Counterexample

Discussion prompt

You've already themed Aster with tokens. Now you drop down to the level of a single component - how it's built, how you configure it, and how it behaves under the pointer and the keyboard.

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.

Answer:

The reason this matters: a component isn't one picture. It's a small machine with inputs (its properties) and modes (its states). You design the inputs and the modes, not one static frame.

6. Today's roadmap

Concept

Three parts, each building on the last, then a hands-on build:

Headless
Radix = behavior + a11y. shadcn = the Tailwind skin.
Properties
Variant props swap a style; boolean props toggle a feature.
States
default · hover · focus-visible · active · disabled.

7. Which is which: Today's roadmap

Matching

Match the pairs

From Today's roadmap — match each one to what it actually does. The descriptions have been shuffled.

  • c1. Headless
  • c2. Properties
  • c3. States
  • b1. Radix = behavior + a11y. shadcn = the Tailwind skin.
  • b2. Variant props swap a style; boolean props toggle a feature.
  • b3. default · hover · focus-visible · active · disabled.

Why: Headless, Properties, States are easy to tell apart while they are sitting next to their descriptions and much harder afterwards, which is what this checks.

8. What "Headless" Means

Section

Part 1

9. Three layers, one component

Concept

Figure (svg): A bare wireframe skeleton labeled Radix, plus a paint swatch labeled Tailwind, equals a finished styled button labeled shadcn

A shadcn component is two things fused together: Radix underneath supplies the behavior, and Tailwind on top supplies the look.

Radix is the bare skeleton - it works, but it has no paint. Tailwind is the paint. shadcn is the finished, painted component you actually drop into Aster.

You own the shadcn file, so you can tweak the paint freely. The skeleton underneath is the part you almost never touch.

10. By analogy: Three layers, one component

Analogy

Discussion prompt

Explain Three layers, one component 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:

A shadcn component is two things fused together: Radix underneath supplies the behavior, and Tailwind on top supplies the look.

11. What "headless" actually gives you

Concept

headless — A component that ships behavior and accessibility with NO styling. It manages focus, keyboard navigation, and ARIA roles/states, but renders no colors, spacing, or fonts - you supply all of that.

Radix Primitives are headless. A Radix Dialog traps focus, closes on Escape, and wires up role="dialog" and aria-modal - all correct - while looking like completely unstyled markup.

So the accessibility is built in for free. Your only job is to style it. That's the whole bargain: they own the hard, invisible behavior; you own the paint.

12. Teach it back: What "headless" actually gives you

Explain it

Discussion prompt

Explain What "headless" actually gives you 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:

Radix Primitives are headless. A Radix Dialog traps focus, closes on Escape, and wires up role="dialog" and aria-modal - all correct - while looking like completely unstyled markup.

13. What behavior actually rides underneath

Concept

"Behavior + accessibility" isn't vague - it's a concrete list of things you'd otherwise hand-build and get wrong:

Radix handlesexample
focus managementfocus is trapped inside an open dialog, returned on close
keyboard navigationarrow keys move through a menu, Escape closes it
ARIA roles + statesrole="dialog", aria-expanded, aria-checked set correctly

None of that shows up as pixels, which is exactly why it's easy to forget it's there - and expensive to rebuild by hand.

14. Fill in: example for What behavior actually rides underneath

Comparison

Comparison matrix

From What behavior actually rides underneath: refill the example column from what you know. The rest of the table is as it appeared.

Radix handlesexample
focus managementfocus is trapped inside an open dialog, returned on close
keyboard navigationarrow keys move through a menu, Escape closes it
ARIA roles + statesrole="dialog", aria-expanded, aria-checked set correctly

15. What you'd have to build without headless

Worked example

Imagine hand-rolling a dropdown menu. The visible part - a styled box with rows - is the easy 20%. The invisible 80% is what Radix gives you:

behavioryou'd hand-codeRadix does it
arrow-key navigationkeydown handlers, index mathyes
Escape / click-outside to closelisteners + cleanupyes
focus return to triggersave + restore focusyes
aria-expanded / rolesset + sync every toggleyes

Each of those is a place teams ship bugs. Headless means you inherit the correct version and spend your time on the paint - which is the part you're actually good at.

16. What each one costs: What you'd have to build without headless

Trade off

Comparison matrix

From What you'd have to build without headless: every row here is a choice with a cost. Fill the you'd hand-code column, then say which row you would actually pick and what you give up for it.

behavioryou'd hand-codeRadix does it
arrow-key navigationkeydown handlers, index mathyes
Escape / click-outside to closelisteners + cleanupyes
focus return to triggersave + restore focusyes
aria-expanded / rolesset + sync every toggleyes

17. Skeleton and skin

Intuition

Figure (svg): Left: a plain dashed rectangle labeled Radix works no style. Right: the same rectangle filled and rounded labeled shadcn works and looks right.

Think of a wireframe versus a mockup. The wireframe already does the right thing - the tab order, the focus, the roles are all correct. It just looks like nothing yet.

You'd never ship the wireframe. But you'd be foolish to rebuild all its behavior from scratch either. shadcn keeps the wireframe's brain and gives it a face.

18. What rests on this: Skeleton and skin

Socratic

Discussion prompt

Think of a wireframe versus a mockup. The wireframe already does the right thing - the tab order, the focus, the roles are all correct. It just looks like nothing yet.

Suppose that were not true. What is the first thing in Session 4: Radix & Component Anatomy that would stop working?

Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.

Answer:

You'd never ship the wireframe. But you'd be foolish to rebuild all its behavior from scratch either. shadcn keeps the wireframe's brain and gives it a face.

19. You own the file - so you can read the skin

Worked example

When you run npx shadcn@latest add button, the component lands in your repo at components/ui/button.tsx. It's not a locked dependency - it's your source.

npx shadcn@latest add button input

Open it and you see the two layers plainly: a Radix (or native) element for behavior, wrapped in a Tailwind class string for the skin.

// the behavior element (Radix Slot / <button>)
// + a class string you fully control:
"bg-primary text-primary-foreground rounded-lg
 hover:bg-primary/90 focus-visible:ring-2 ..."

Because you own the file, branding Aster is editing a class string you can read - not fighting a library's theme API from the outside.

20. Something is wrong here: assuming Radix "does the styling"

Anomaly

Predict first

A student writes this, and it looks reasonable:

You add a Radix primitive expecting it to look like a finished component out of the box.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: It works - focus is trapped, Escape closes it - but it has zero paint: no background, no radius, no spacing.

Treat Radix as behavior-only and add the Tailwind skin yourself (this is exactly what shadcn does).

Why: It works - focus is trapped, Escape closes it - but it has zero paint: no background, no radius, no spacing.

21. Trap: assuming Radix "does the styling"

Trap

The trap

You add a Radix primitive expecting it to look like a finished component out of the box.

Drop in a bare Radix Dialog and render it

Why: It works - focus is trapped, Escape closes it - but it has zero paint: no background, no radius, no spacing.

Conclude "Radix is broken or ugly"

Why: It isn't broken. Radix is headless - styling was never its job. You just haven't added the skin.

The fix

Treat Radix as behavior-only and add the Tailwind skin yourself (this is exactly what shadcn does).

Use the shadcn version, which wraps Radix in Tailwind classes

Why: You keep Radix's focus/keyboard/ARIA and get a themed surface from bg-*, rounded-lg, p-*.

Tweak the classes to brand it

Why: Behavior stays correct; only the paint changes. That separation is the whole point of headless.

22. Component Properties

Section

Part 2

23. Two kinds of property

Concept

In Figma, a component's behavior is exposed through properties in the right panel. Two kinds do most of the work: variant props and boolean props.

variant prop — A property that swaps the component to a whole different named style. One variant prop can have many values - e.g. the Button's variant = default / secondary / destructive / outline / ghost / link.

boolean prop — A true/false property that toggles ONE feature on or off - e.g. showIcon. It doesn't swap the whole style; it just adds or removes a piece.

24. Take the definitions apart: headless vs variant prop

Definition probe

Sort into buckets

Every line below is part of the definition of headless or of variant prop — one or the other, never both. Put each where it belongs.

headless
A component that ships behavior and accessibility with NO styling.; It manages focus, keyboard navigation, and ARIA roles/states, but renders no colors, spacing, or fonts - you supply all of that.
variant prop
A property that swaps the component to a whole different named style.; One variant prop can have many values - e.g.; the Button's variant = default / secondary / destructive / outline / ghost / link.
b1
A component that ships behavior and accessibility with NO styling. It manages focus, keyboard navigation, and ARIA roles/states, but renders no colors, spacing, or fonts - you supply all of that.
b2
A property that swaps the component to a whole different named style. One variant prop can have many values - e.g. the Button's variant = default / secondary / destructive / outline / ghost / link.

25. The real Button property set

Concept

Figure (svg): A Figma-style properties panel listing variant, size, and a showIcon toggle with their values

shadcn's Button really exposes these, and they map straight to the code props:

propertykindvalues
variantvariantdefault, secondary, destructive, outline, ghost, link
sizevariantdefault, sm, lg, icon
showIconbooleantrue / false

Notice: variant and size each swap a whole style. showIcon just adds or removes the leading icon - one feature, independent of which variant you picked.

26. When to use each Button variant

Concept

The variant isn't decoration - it signals intent. Pick by what the button is for:

variantwhen to use
defaultThe primary action on a screen - the one thing you want clicked.
secondaryA supporting action next to the primary one.
destructiveDeletes or irreversibly changes data - warns the user.
outlineA quieter action; sits calmly on busy surfaces.
ghostMinimal, chrome-free - toolbars, menus, icon rows.
linkLooks like a text link but is a real button.

27. What rests on this: When to use each Button variant

Socratic

Discussion prompt

The variant isn't decoration - it signals intent. Pick by what the button is for:

Suppose that were not true. What is the first thing in Session 4: Radix & Component Anatomy that would stop working?

Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.

28. Sizes are just another variant axis

Intuition

Figure (svg): Four buttons showing sm, default, lg widths, and a square icon-only button

size is a second variant prop: default, sm, lg, and icon. It changes height, padding, and text size - not color.

icon is the special one: a square button sized for a single glyph, no label. It's how you get a clean icon button without inventing a new component.

29. The Figma prop IS the code prop

Concept

This isn't a loose analogy. The variant you pick in Figma's panel maps one-to-one to a prop in the code - shadcn builds its variants with a helper called cva (class-variance-authority).

const buttonVariants = cva("base classes...", {
  variants: {
    variant: { default: "...", destructive: "...", outline: "..." },
    size:    { default: "...", sm: "...", lg: "...", icon: "..." },
  },
})

So variant and size in Figma are literally the variant and size keys here. Design and code speak the same vocabulary - that's what makes handoff clean.

30. Toggling the showIcon boolean

Worked example

Watch what a boolean does versus a variant. Flipping showIcon adds or removes exactly one child - it doesn't touch the fill, size, or radius.

// showIcon = false
<Button variant="default">Save</Button>

// showIcon = true (same variant, same size)
<Button variant="default"><Check /> Save</Button>
propertyshowIcon = falseshowIcon = true
fill / size / radiusunchangedunchanged
leading iconhiddenshown

One boolean, orthogonal to the variant. That independence is exactly why you don't need ButtonWithIcon and ButtonNoIcon as separate components.

31. Dissecting the Aster Button

Worked example

Figure (svg): Three Aster buttons: a filled violet default, an outline, and a ghost, plus a small and large size

For Aster's hero, you want the primary call-to-action. In the panel, set variant = default, size = lg, showIcon = true.

That maps to one line of markup - the same three properties, now as class-driving props:

<Button variant="default" size="lg">
  <Sparkles /> Start a project
</Button>

The nav's "Log in", by contrast, is variant = ghost, size = default - same component, different property values. You never made a second button.

usevariantsize
hero CTAdefaultlg
nav Log inghostdefault
delete rowdestructivesm

32. Watch it run: Dissecting the Aster Button

Pattern

Step through it

Step through Dissecting the Aster Button one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: use is hero CTA
  2. Step 2: use is nav Log in
  3. Step 3: use is delete row

33. Name variants by role, not by look

Concept

Notice shadcn's variant values: default, secondary, destructive, ghost. They name a role, not a color. None of them is called violet or red.

That's deliberate. destructive stays destructive even after you rebrand Aster's red to a different red. If you'd named it redButton, the name would lie the moment the theme changed.

Role names + tokens are the same discipline at two levels: the variant says what it's for, the token says what color that role is right now.

34. What rests on this: Name variants by role, not by look

Socratic

Discussion prompt

Notice shadcn's variant values: default, secondary, destructive, ghost. They name a role, not a color. None of them is called violet or red.

Suppose that were not true. What is the first thing in Session 4: Radix & Component Anatomy that would stop working?

Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.

Answer:

Role names + tokens are the same discipline at two levels: the variant says what it's for, the token says what color that role is right now.

35. Why booleans keep the kit lean

Concept

Say you have 2 variants and you want each with and without an icon. You could build 4 separate components. Don't.

Instead: keep the variant prop (2 values) and add ONE boolean showIcon. That's 2 properties, and it covers all 4 combinations - variant multiplied by showIcon.

approachwhat you maintaincovers
4 separate components4 frames to update4 fixed looks
variant + showIcon boolean1 component, 2 propsall 2 x 2 combinations

Fewer components, no drift: fix a padding value once and every combination inherits it. Booleans are how a kit stays small while covering more cases.

36. Something is wrong here: a new component for every style

Anomaly

Predict first

A student writes this, and it looks reasonable:

Aster needs a red delete button. You build ButtonRed as a brand-new component.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: Now there are two components. A radius change has to be made in both - and they'll drift apart.

Add a value to the existing variant prop instead of a new component.

Why: Now there are two components. A radius change has to be made in both - and they'll drift apart.

37. Trap: a new component for every style

Trap

The trap

Aster needs a red delete button. You build ButtonRed as a brand-new component.

Duplicate Button, recolor it, name it ButtonRed

Why: Now there are two components. A radius change has to be made in both - and they'll drift apart.

Next week: ButtonGhost, ButtonIcon, and on it goes

Why: The kit bloats. Every fix multiplies across near-duplicates you have to keep in sync.

The fix

Add a value to the existing variant prop instead of a new component.

Set the Button's variant = destructive

Why: It's already in the set. One component, one more value - no duplicate to maintain.

Toggle showIcon or pick a size as needed

Why: Every combination comes from properties on one source of truth. Fixes land everywhere at once.

38. Break it on purpose: a new component for every style

Break the constraint

Discussion prompt

The rule this trap just fixed:

Every combination comes from properties on one source of truth. Fixes land everywhere at once.

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:

Now there are two components. A radius change has to be made in both - and they'll drift apart.

39. The States That Matter

Section

Part 3

40. A component has five states

Concept

Figure (svg): Five stacked buttons showing default, hover brighter, focus-visible with a ring, active pressed, and disabled dimmed

The same button is never just one picture. It moves through five interactive states, and a finished design shows all of them:

state — A distinct mode a component can be in based on interaction: default (resting), hover (pointer over it), focus-visible (keyboard-focused ring), active (being pressed), and disabled (not interactive).

Design only the default and you've designed one-fifth of the component.

41. States are a little machine

Intuition

Figure (svg): A state diagram: default at center, arrows to hover on pointer, focus-visible on Tab, active on press, and disabled as a separate locked node

Picture the states as nodes. From default, a pointer takes you to hover, Tab to focus-visible, a press to active - then back.

disabled sits off to the side: it's a locked mode where none of the other transitions can fire. That's why disabled has to switch off pointer events, not just dim.

42. What rests on this: States are a little machine

Socratic

Discussion prompt

Picture the states as nodes. From default, a pointer takes you to hover, Tab to focus-visible, a press to active - then back.

Suppose that were not true. What is the first thing in Session 4: Radix & Component Anatomy that would stop working?

Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.

43. Each state, and its Tailwind hook

Concept

Tailwind gives each state a variant prefix. You attach the state's look to that hook:

statevisual changeTailwind hook
defaultresting brand lookbase classes (no prefix)
hoverslightly brighter or darker fillhover:bg-primary/90
focus-visiblea visible ring around itfocus-visible:ring-2
activepressed - a touch darkeractive:bg-primary/80
disableddimmed, not clickabledisabled:opacity-50

44. Where does each piece belong: Session 4: Radix & Component Anatomy

Sorting

Sort into buckets

These are the pieces of Session 4: Radix & Component Anatomy, out of order. Put each one back under the part of the lesson it belongs to.

What "Headless" Means
Three layers, one component; What "headless" actually gives you; What behavior actually rides underneath
Component Properties
Two kinds of property; The real Button property set; When to use each Button variant
The States That Matter
A component has five states; States are a little machine; Each state, and its Tailwind hook
s1
What "Headless" Means is where Session 4: Radix & Component Anatomy puts Three layers, one component, What "headless" actually gives you, What behavior actually rides underneath. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
Component Properties is where Session 4: Radix & Component Anatomy puts Two kinds of property, The real Button property set, When to use each Button variant. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
The States That Matter is where Session 4: Radix & Component Anatomy puts A component has five states, States are a little machine, Each state, and its Tailwind hook. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

45. focus-visible is the important one

Concept

focus-visible — The keyboard-only focus ring. It fires when an element is focused AND the browser judges a ring should show - typically Tab navigation - not on an ordinary mouse click. In Tailwind: focus-visible:ring-*.

Someone navigating Aster with the keyboard needs to see where they are. That ring is how they know the "Start a project" button is focused before they press Enter.

Designers forget it constantly, because with a mouse you never see it. Turn the mouse off and Tab through your page - if nothing lights up, the page is unusable by keyboard.

46. focus-visible vs hover vs :focus

Intuition

Figure (svg): pointer maps to hover, Tab key maps to focus-visible, mouse click maps to plain focus which also fires and is the problem

Three different triggers, easy to conflate:

That last one is why :focus feels ugly: click a button and a ring sticks around. focus-visible shows the ring only when it helps - keyboard users - and stays quiet for mouse clicks.

47. States differ per variant

Worked example

The states aren't one-size-fits-all - each variant expresses them differently. A ghost button has no resting fill, so its hover is where the fill first appears.

<!-- ghost: transparent at rest, fill on hover -->
class="bg-transparent
       hover:bg-accent hover:text-accent-foreground
       focus-visible:ring-2 focus-visible:ring-ring"
variantdefaulthover
defaultsolid --primary fillbg-primary/90 (darker)
outlineborder only, no fillbg-accent appears
ghostfully transparentbg-accent appears

focus-visible, though, stays the same everywhere: the ring is a consistent keyboard signal regardless of variant. Consistency there is a feature, not a bug.

48. Tokens make five states nearly free

Concept

Here's the payoff of the token work from earlier sessions: every state reads a semantic token, so you write the states once and rebranding retints all of them.

state hookreads tokenon rebrand
bg-primary--primarynew brand fill everywhere
hover:bg-primary/90--primary (90%)hover follows automatically
focus-visible:ring-ring--ringring recolors with the theme

Change --primary once in globals.css and the default, hover, and active states all move together. You never hand-recolor five frames.

49. Fill in: on rebrand for Tokens make five states nearly free

Comparison

Comparison matrix

From Tokens make five states nearly free: refill the on rebrand column from what you know. The rest of the table is as it appeared.

state hookreads tokenon rebrand
bg-primary--primarynew brand fill everywhere
hover:bg-primary/90--primary (90%)hover follows automatically
focus-visible:ring-ring--ringring recolors with the theme

50. The Aster primary Button, all states in class

Worked example

Here's the default variant's class string, with one hook per state. Read it as five states, not one look.

class="bg-primary text-primary-foreground rounded-lg
       hover:bg-primary/90
       focus-visible:ring-2 focus-visible:ring-ring
       active:bg-primary/80
       disabled:opacity-50 disabled:pointer-events-none"

Every state reads a token (bg-primary, ring-ring), so rebranding Aster retints all five at once. You wrote the states; the tokens do the coloring.

Note the disabled line does two things: dims it (opacity-50) AND turns off clicks (pointer-events-none).

51. Disabled needs two things, not one

Concept

A disabled button must look and behave unavailable. Reduced affordance alone isn't enough.

jobwhyTailwind hook
reduce affordancesignal you can't use this yetdisabled:opacity-50
stop interactionclicks and hover must not firedisabled:pointer-events-none

Skip pointer-events-none and a dimmed button still responds to hover and clicks - it looks disabled but isn't. Both halves are required.

52. What rests on this: Disabled needs two things, not one

Socratic

Discussion prompt

A disabled button must look and behave unavailable. Reduced affordance alone isn't enough.

Suppose that were not true. What is the first thing in Session 4: Radix & Component Anatomy that would stop working?

Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.

Answer:

Skip pointer-events-none and a dimmed button still responds to hover and clicks - it looks disabled but isn't. Both halves are required.

53. Something is wrong here: using hover (or :focus) for the focus ring

Anomaly

Predict first

A student writes this, and it looks reasonable:

You want a focus ring, so you attach it to hover - or to plain :focus.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: A keyboard user Tabs to the button and gets NO ring - hover only fires for the pointer.

Use focus-visible for the ring - it targets keyboard focus specifically.

Why: A keyboard user Tabs to the button and gets NO ring - hover only fires for the pointer. They can't see where they are.

54. Trap: using hover (or :focus) for the focus ring

Trap

The trap

You want a focus ring, so you attach it to hover - or to plain :focus.

Put the ring on hover:ring-2

Why: A keyboard user Tabs to the button and gets NO ring - hover only fires for the pointer. They can't see where they are.

Or fall back to focus:ring-2

Why: Now the ring also appears on every mouse click and lingers - the exact behavior everyone finds ugly and turns off.

The fix

Use focus-visible for the ring - it targets keyboard focus specifically.

Put the ring on focus-visible:ring-2 focus-visible:ring-ring

Why: Tab shows the ring for keyboard users; an ordinary mouse click doesn't - accessible and clean.

Keep hover for the fill only

Why: hover and focus-visible are different triggers doing different jobs - don't make one stand in for the other.

55. Reading a full state block in the source

Worked example

Because you own button.tsx, you can read every state in one class string. Here's the shape of the real thing - grouped by state so it's legible:

class="inline-flex items-center justify-center rounded-md text-sm
       bg-primary text-primary-foreground
       hover:bg-primary/90
       focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring
       active:bg-primary/80
       disabled:pointer-events-none disabled:opacity-50"

Line 1 is layout + shape (shared by all states). Lines 2-7 are the five states. When you brand Aster, you're editing exactly these lines - nothing hidden.

focus-visible:outline-none removes the browser's default outline so the ring-2 you added is the only ring - a clean, single, on-brand focus indicator.

56. Ship the default only? You shipped a stub

Concept

The most common real failure isn't a wrong hook - it's only designing the default. The button looks great in the mockup and falls apart in use.

No hover means it feels dead under the pointer. No focus-visible means it's invisible to keyboard users. No disabled means a form's submit stays clickable when it shouldn't be.

A component isn't done at the default frame. It's done when all five states are drawn and documented.

57. Beyond the Button

Section

Part 4

58. The same anatomy, every component

Concept

Everything so far - headless behavior, variant/boolean props, five states - isn't a Button quirk. It's the anatomy of every kit component. The Input is the next one Aster needs.

So when you meet a new component, you already know the questions: what behavior is headless underneath, what are its properties, and what does each of its states look like?

59. What rests on this: The same anatomy, every component

Socratic

Discussion prompt

Everything so far - headless behavior, variant/boolean props, five states - isn't a Button quirk. It's the anatomy of every kit component. The Input is the next one Aster needs.

Suppose that were not true. What is the first thing in Session 4: Radix & Component Anatomy that would stop working?

Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.

60. Input anatomy

Concept

Figure (svg): An input field with a resting border, a focused version with a ring, a dimmed disabled version, and a red-bordered error version

The Input's behavior is the browser's native <input> - shadcn styles it rather than wrapping a Radix primitive, but the split still holds: behavior underneath, Tailwind skin on top.

Its resting border reads --input; its focus ring reads --ring; its error treatment reads --destructive. Different tokens, same idea - the component just references roles.

61. The error state is a fourth first-class state

Concept

Text inputs add one state the Button doesn't have: error. It's not decoration - a form field must be able to say "this value is wrong" both visually and to a screen reader.

input statevisualhook
defaultquiet borderborder-input
focus-visibleringfocus-visible:ring-ring
disableddim + no typingdisabled:opacity-50 disabled:pointer-events-none
errordestructive border + ringaria-invalid:border-destructive

The aria-invalid hook does double duty: it drives the red styling AND tells assistive tech the field is invalid. One attribute, both audiences.

62. What rests on this: The error state is a fourth first-class state

Socratic

Discussion prompt

Text inputs add one state the Button doesn't have: error. It's not decoration - a form field must be able to say "this value is wrong" both visually and to a screen reader.

Suppose that were not true. What is the first thing in Session 4: Radix & Component Anatomy that would stop working?

Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.

Answer:

The aria-invalid hook does double duty: it drives the red styling AND tells assistive tech the field is invalid. One attribute, both audiences.

63. Components compose into blocks

Intuition

Figure (svg): A card outline containing a labeled input and a button, showing small components nested inside a larger block

Configured components nest into bigger pieces. Aster's newsletter card is a Card holding a labeled Input and a Button - three configured components, one block.

That's the next session's topic. But notice: because each inner component already handles its own states, the block inherits correct behavior for free. Good anatomy compounds.

64. Teach it back: Components compose into blocks

Explain it

Discussion prompt

Explain Components compose into blocks 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:

Configured components nest into bigger pieces. Aster's newsletter card is a Card holding a labeled Input and a Button - three configured components, one block.

65. Not every component has five states

Concept

A Card is a container - it's not interactive, so it has no hover, focus, active, or disabled. Its 'anatomy' is layout: padding, radius, border, and slots for a header, content, and footer.

The rule isn't 'always draw five states' - it's 'draw the states this component actually has.' Ask what a user can do to it. A Card: nothing. A Button: five things.

componentinteractive?states to draw
Buttonyesall five
Inputyesdefault, focus-visible, disabled, error
Cardnojust its default layout

66. By analogy: Not every component has five states

Analogy

Discussion prompt

Explain Not every component has five states 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:

A Card is a container - it's not interactive, so it has no hover, focus, active, or disabled. Its 'anatomy' is layout: padding, radius, border, and slots for a header, content, and footer.

67. The icon-only button needs a name

Worked example

The size = icon Button has no text label. That's a real accessibility gap: a screen reader would announce nothing. This is a headless-limit you must fill yourself.

// Wrong: no accessible name
<Button size="icon"><Trash /></Button>

// Right: give it an aria-label
<Button size="icon" aria-label="Delete"><Trash /></Button>

Radix gives you focus and keyboard handling, but it can't invent a label for your glyph. The aria-label is the one piece of accessibility that's on you.

68. Something is wrong here: an icon-only button with no label

Anomaly

Predict first

A student writes this, and it looks reasonable:

You ship Aster's delete icon button relying on the glyph alone to convey meaning.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: It looks fine and works with a mouse - the trash glyph reads as 'delete' to sighted users.

Add an aria-label so the button has an accessible name.

Why: It looks fine and works with a mouse - the trash glyph reads as 'delete' to sighted users.

69. Trap: an icon-only button with no label

Trap

The trap

You ship Aster's delete icon button relying on the glyph alone to convey meaning.

Render <Button size="icon"><Trash /></Button>

Why: It looks fine and works with a mouse - the trash glyph reads as 'delete' to sighted users.

A screen-reader user reaches it

Why: It announces as 'button' with no name. There's no text and no label, so its purpose is invisible to them.

The fix

Add an aria-label so the button has an accessible name.

Render <Button size="icon" aria-label="Delete">

Why: Now it announces as 'Delete, button' - the meaning the glyph carried is available to everyone.

Note this in the component's state doc

Why: The icon size always needs a label; document it so no one forgets on the next icon button.

70. Which of these survive contact with Session 4: Radix & Component Anatomy?

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.

Holds up
Three parts, each building on the last, then a hands-on build:; A shadcn component is two things fused together: Radix underneath supplies the behavior, and Tailwind on top supplies the look.; None of that shows up as pixels, which is exactly why it's easy to forget it's there - and expensive to rebuild by hand.
Breaks
You add a Radix primitive expecting it to look like a finished component out of the box.; Aster needs a red delete button. You build ButtonRed as a brand-new component.
sound
These are stated as this lesson states them — each one survives the edge cases Session 4: Radix & Component Anatomy puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

71. The Recipe

Section

Part 5 · Pattern

72. Rebuild the recipe: The configure-a-component recipe

Ranking

Put in order

These are the steps of The configure-a-component recipe, scrambled. Put them back in order before the next slide shows you.

  1. Pick the variant that matches intent (default / secondary / destructive / outline / ghost / link).
  2. Set the booleans for optional features (e.g. showIcon) and pick the size.
  3. Theme via tokens - the component reads --primary, --ring, etc., so branding is a token change, not per-component paint.
  4. Document all five states - default, hover, focus-visible, active, disabled - before you call it done.

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.

73. The configure-a-component recipe

Pattern

Whenever Aster needs a component, configure the kit one - never fork a new component. Four moves, in order:

  1. Pick the variant that matches intent (default / secondary / destructive / outline / ghost / link).
  2. Set the booleans for optional features (e.g. showIcon) and pick the size.
  3. Theme via tokens - the component reads --primary, --ring, etc., so branding is a token change, not per-component paint.
  4. Document all five states - default, hover, focus-visible, active, disabled - before you call it done.
statevisual changeTailwind hook
hoverfill shiftshover:bg-primary/90
focus-visiblekeyboard ring appearsfocus-visible:ring-2
disableddim + no clicksdisabled:opacity-50 disabled:pointer-events-none

74. Where does it stop working: The configure-a-component recipe

Edge cases

Discussion prompt

The configure-a-component recipe 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:

Whenever Aster needs a component, configure the kit one - never fork a new component. Four moves, in order:

75. Rule out three: Check: what headless means

Elimination

Eliminate the wrong options

Radix Primitives are described as "headless." What does that mean for what you get?

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.

  • A. Behavior and accessibility (focus, keyboard, ARIA) but NO styling - you add the look yourself.
  • B. Fully styled components you can't change.
  • C. The styling, but you have to write the accessibility and keyboard handling yourself.
  • D. Nothing until you install a separate theme package.

Survives elimination: A

Why: Headless means the primitive supplies behavior and accessibility - focus management, keyboard navigation, ARIA roles and states - with no visual styling. You (or shadcn on your behalf) add the Tailwind skin.

76. Check: what headless means

Check

A teammate says "let's use Radix so we don't have to style anything." What's wrong with that sentence?

Check your understanding

Radix Primitives are described as "headless." What does that mean for what you get?

  • A. Behavior and accessibility (focus, keyboard, ARIA) but NO styling - you add the look yourself. (correct)
  • B. Fully styled components you can't change.
  • C. The styling, but you have to write the accessibility and keyboard handling yourself.
  • D. Nothing until you install a separate theme package.

Answer: A

Why: Headless means the primitive supplies behavior and accessibility - focus management, keyboard navigation, ARIA roles and states - with no visual styling. You (or shadcn on your behalf) add the Tailwind skin.

Why B tempts people
That describes a locked, styled library like MUI. Radix ships no styling at all, which is the opposite of fully styled and unchangeable.
Why C tempts people
Backwards. Radix hands you the hard accessibility and keyboard work for free; the styling is the part you supply.
Why D tempts people
There's no theme package to wait on. Radix works immediately - it just renders unstyled markup until you add classes.

77. Answer it before you see the options: Check: variant vs boolean

Prediction

Predict first

You want filled/outlined AND icon/no-icon, in any combination. Which property design is right?

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: A variant prop (filled/outline) plus a boolean prop showIcon.

Why: The style (filled vs outline) is a whole-style swap, so it's a variant prop. The icon is one independent feature that's on or off, so it's a boolean. Two props cover all 2x2 combinations from one component.

78. Check: variant vs boolean

Check

Aster's buttons can be filled or outlined, and independently can show or hide a leading icon. How should you model that?

Check your understanding

You want filled/outlined AND icon/no-icon, in any combination. Which property design is right?

  • A. A variant prop (filled/outline) plus a boolean prop showIcon. (correct)
  • B. Four separate components: FilledIcon, FilledNoIcon, OutlineIcon, OutlineNoIcon.
  • C. One variant prop with four values that bake in the icon choice.
  • D. Two boolean props: isFilled and isOutline.

Answer: A

Why: The style (filled vs outline) is a whole-style swap, so it's a variant prop. The icon is one independent feature that's on or off, so it's a boolean. Two props cover all 2x2 combinations from one component.

Why B tempts people
Four components duplicate everything and drift apart. Two orthogonal properties on one component express the same combinations without the maintenance cost.
Why C tempts people
Baking the icon into the variant couples two independent choices, so you'd need a value for every combination and lose the clean toggle.
Why D tempts people
Filled and outline are mutually exclusive styles, so they belong to ONE variant prop with multiple values, not two booleans that could both be true or both false.

79. How sure are you: Check: which state hook?

Commit first

Predict first

You need a ring that shows for keyboard focus but NOT on ordinary mouse clicks. Which Tailwind hook?

Commit to an answer, then rate it — certain, fairly sure, or guessing — and write the rating down before you turn the page.

Correct: focus-visible:ring-2

Why: focus-visible fires when the browser judges a ring should show - keyboard navigation - and stays quiet for ordinary mouse clicks. That's exactly the keyboard-only ring you want.

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.

80. Check: which state hook?

Check

A keyboard user reports they can't tell which button is focused as they Tab through Aster. Which fix is correct?

Check your understanding

You need a ring that shows for keyboard focus but NOT on ordinary mouse clicks. Which Tailwind hook?

  • A. focus-visible:ring-2 (correct)
  • B. hover:ring-2
  • C. focus:ring-2
  • D. active:ring-2

Answer: A

Why: focus-visible fires when the browser judges a ring should show - keyboard navigation - and stays quiet for ordinary mouse clicks. That's exactly the keyboard-only ring you want.

Why B tempts people
hover only responds to the pointer. A keyboard user who never touches the mouse would get no ring, so the problem stays unsolved.
Why C tempts people
Plain focus also fires on mouse click, so the ring appears and lingers after every click - the ugly behavior you're trying to avoid.
Why D tempts people
active fires only while the button is being pressed. It's gone the instant the press ends, so it can't mark the focused element.

81. Answer it before you see the options: Check: the disabled state

Prediction

Predict first

A button has only disabled:opacity-50. It looks dimmed. What's the remaining problem?

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: It still responds to hover and clicks - it needs disabled:pointer-events-none too.

Why: Opacity only changes the look. Without pointer-events-none the dimmed button still fires hover and click. A disabled state needs reduced affordance AND interaction turned off.

82. Check: the disabled state

Check

Gordon styles Aster's disabled submit with disabled:opacity-50 and ships it. What did he miss?

Check your understanding

A button has only disabled:opacity-50. It looks dimmed. What's the remaining problem?

  • A. It still responds to hover and clicks - it needs disabled:pointer-events-none too. (correct)
  • B. Nothing - opacity-50 fully disables the button.
  • C. opacity-50 makes it invisible, so users can't find it.
  • D. It also needs a focus-visible ring while disabled so keyboard users can focus it.

Answer: A

Why: Opacity only changes the look. Without pointer-events-none the dimmed button still fires hover and click. A disabled state needs reduced affordance AND interaction turned off.

Why B tempts people
opacity-50 only affects appearance, not behavior. The button remains clickable and hoverable until you also disable pointer events.
Why C tempts people
opacity-50 is 50% opacity, still clearly visible - it's a dim, not invisible. opacity-0 would hide it, which isn't what's used.
Why D tempts people
A disabled control is intentionally not interactive, so it shouldn't take focus at all - adding a focus ring to it would be wrong, not the fix.

83. Rule out three: Check: the icon-only button

Elimination

Eliminate the wrong options

An icon-only Button (size=icon) renders just a <Trash /> glyph. What's the accessibility gap you must fix?

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.

  • A. It has no accessible name - add aria-label="Delete" so screen readers announce its purpose.
  • B. Nothing - Radix supplies an accessible name automatically from the icon.
  • C. It needs a bigger focus ring since icon buttons are small.
  • D. You must add a visible text label; icon-only buttons aren't allowed.

Survives elimination: A

Why: Radix gives focus and keyboard behavior, but it can't name your glyph. With no text, a screen reader announces only 'button'. An aria-label provides the accessible name the visual glyph was carrying.

84. Check: the icon-only button

Check

Aster has a trash-icon button with no visible text. Radix handles its focus and keyboard. What still needs your attention?

Check your understanding

An icon-only Button (size=icon) renders just a <Trash /> glyph. What's the accessibility gap you must fix?

  • A. It has no accessible name - add aria-label="Delete" so screen readers announce its purpose. (correct)
  • B. Nothing - Radix supplies an accessible name automatically from the icon.
  • C. It needs a bigger focus ring since icon buttons are small.
  • D. You must add a visible text label; icon-only buttons aren't allowed.

Answer: A

Why: Radix gives focus and keyboard behavior, but it can't name your glyph. With no text, a screen reader announces only 'button'. An aria-label provides the accessible name the visual glyph was carrying.

Why B tempts people
Radix can't infer meaning from an arbitrary icon component - it has no idea a Trash glyph means 'delete'. The accessible name is the one part you must supply.
Why C tempts people
Ring size is a nice-to-have, not the gap. The real problem is that assistive tech has no name for the control at all.
Why D tempts people
Icon-only buttons are fine when they have an aria-label. A visible label isn't required - an accessible name is, and aria-label provides it.

85. Your Turn: Brand the Aster Kit

Section

Part 6 · Hands-on

86. The build: brand and document

Concept

You'll brand two Aster components - the Button and the Input - and document all five states for each. Configure the kit components; do not build new ones.

#milestonedeliverable
1Aster primary Buttonvariant + size chosen, on brand
2Document the 5 statesa table: state to visual change
3Aster Inputfocus-visible ring + disabled + error

87. Break it if you can: The build: brand and document

Counterexample

Discussion prompt

You'll brand two Aster components - the Button and the Input - and document all five states for each. Configure the kit components; do not build new ones.

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.

88. Milestone 1 — the Aster primary Button

Worked example

Your turn: configure the kit Button to be Aster's primary hero action. Pick the variant and the size, and make sure it reads the brand primary. Say out loud which variant matches "the one thing I want clicked."

Hint: the primary action is the default variant; a hero CTA wants size = lg. The fill should come from the --primary token (Aster's violet), not a hardcoded color.

<Button variant="default" size="lg">
  Start a project
</Button>
// default variant fill = bg-primary = oklch(0.55 0.24 285)
self-checkshould be
variantdefault (the primary action)
sizelg (hero CTA)
fill source--primary token, not a literal color

89. Milestone 2 — document the five states

Worked example

Your turn: for that Button, write down what changes in each of the five states. Don't ship until every row is filled - especially focus-visible.

Hint: hover shifts the fill, focus-visible adds a ring, active darkens on press, disabled dims and stops clicks. Attach each to its Tailwind hook.

statewhat changesTailwind hook
defaultbrand violet fill, rounded-lgbg-primary text-primary-foreground
hoverfill slightly darkerhover:bg-primary/90
focus-visiblering appears (keyboard)focus-visible:ring-2 focus-visible:ring-ring
activefill darker on pressactive:bg-primary/80
disableddim + no clicksdisabled:opacity-50 disabled:pointer-events-none

Self-check: did you include focus-visible (not hover, not plain focus) for the ring, and did disabled turn off pointer-events? If yes, all five states are covered.

90. What each one costs: Milestone 2 — document the five states

Trade off

Comparison matrix

From Milestone 2 — document the five states: every row here is a choice with a cost. Fill the Tailwind hook column, then say which row you would actually pick and what you give up for it.

statewhat changesTailwind hook
defaultbrand violet fill, rounded-lgbg-primary text-primary-foreground
hoverfill slightly darkerhover:bg-primary/90
focus-visiblering appears (keyboard)focus-visible:ring-2 focus-visible:ring-ring
activefill darker on pressactive:bg-primary/80
disableddim + no clicksdisabled:opacity-50 disabled:pointer-events-none

91. Milestone 3 — brand the Aster Input

Worked example

Your turn: brand the kit Input for Aster's contact form. Give it a visible focus-visible ring, a disabled treatment, and an error treatment using the destructive token. Same rule: configure, don't fork.

Hint: the resting border reads --input; the focus ring reads --ring; the error state swaps border and ring to --destructive. Disabled dims and blocks input.

class="border-input rounded-md
       focus-visible:ring-2 focus-visible:ring-ring
       disabled:opacity-50 disabled:pointer-events-none
       aria-invalid:border-destructive aria-invalid:ring-destructive"
statewhat changestoken / hook
defaultquiet borderborder-input
focus-visiblering appearsfocus-visible:ring-ring
disableddim + no typingdisabled:opacity-50 disabled:pointer-events-none
errorred border + ringaria-invalid:border-destructive

92. Milestone check — verify by keyboard

Worked example

Your turn: before you call the Button and Input done, verify them the way a real user would. Put the mouse down and drive with the keyboard only.

Hint: press Tab to move focus, look for the ring on each control, press Enter on the button, and confirm the disabled control is skipped entirely.

do thisexpect
Tab to the Buttona visible focus-visible ring
Tab to the Inputa visible focus-visible ring
Tab past a disabled controlit is skipped, no ring
click the Button with the mouseNO lingering ring after

If every row matches, your states are real - not just drawn. A ring that lingers after a mouse click means you used :focus instead of focus-visible; go fix that hook.

93. Show it off

Worked example

Lay all your work on one Figma frame: the Button in its five states, then the Input in its four. A reviewer should read every state at a glance.

Figure (svg): A state sheet: a row of five button states across the top and a row of input states below, each labeled

If a teammate can see all nine states without asking you a question, the components are documented - not just designed.

94. Homework — assemble the states Aster needs

Worked example

Homework: walk Aster's real page - navbar, hero, feature grid, pricing, footer - and assemble the states each interactive piece needs.

Cover: the nav links and menu (hover + focus-visible), the buttons across variants (all five states each), and the form fields in pricing/contact (focus-visible ring, disabled, error).

piecestates to document
nav linksdefault, hover, focus-visible
hero + pricing buttonsdefault, hover, focus-visible, active, disabled
contact form fieldsdefault, focus-visible, disabled, error

Rule for all of it: configure the kit components via their properties and tokens. If you catch yourself making ButtonSomething, stop - that's a variant or a boolean.

95. Fill in: states to document for Homework — assemble the states Aster needs

Comparison

Comparison matrix

From Homework — assemble the states Aster needs: refill the states to document column from what you know. The rest of the table is as it appeared.

piecestates to document
nav linksdefault, hover, focus-visible
hero + pricing buttonsdefault, hover, focus-visible, active, disabled
contact form fieldsdefault, focus-visible, disabled, error

96. Connect it up: Session 4: Radix & Component Anatomy

Connect it up

Draw it

One page, no notation unless you need it: draw how these connect — What "Headless" Means · Component Properties · The States That Matter · Beyond the Button · The Recipe · Your Turn: Brand the Aster Kit. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.

97. What you can do now

Recap

statevisual changeTailwind hook
defaultresting brand lookbase classes
hoverfill shiftshover:bg-primary/90
focus-visiblekeyboard ringfocus-visible:ring-2
activepressed, darkeractive:bg-primary/80
disableddim + no clicksdisabled:opacity-50 disabled:pointer-events-none

Next session: composition - assembling these configured components into blocks and pages, and making them reflow responsively.

Sources

  1. Radix Primitives - Introduction (headless, accessible, unstyled primitives)
  2. shadcn/ui - Button component (variants, sizes, cva)
  3. Tailwind CSS - Handling hover, focus, and other states (focus-visible, active, disabled)
  4. MDN - :focus-visible pseudo-class
  5. Figma - Create and use component properties (variant and boolean props)

Want this taught 1-on-1? Alexander tutors shadcn/ui + Figma — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108