Make It Look Good: Styling, Components, and Themes
Decode the Tailwind classes you already wrote, meet shadcn/ui's ready-made components and swap your hand-built Card for one, then learn the design tokens and themes that let you restyle the whole site at once.
Your page works, but it's plain — your name, a subtitle, a link. Time to make it look
like something. The good news: you already wrote styling last lesson. Every
className="…" you pasted into the Hero and Card and were told to ignore? That was
Tailwind. This lesson you stop ignoring it.
By the end you'll read those classes at a glance, swap your hand-built Card for a
polished, ready-made one from shadcn/ui, and flip your whole site to a new look by
changing a handful of lines. Editor open, bun dev running — let's style.
CSS in one minute
Everything you see on a web page — color, size, spacing, fonts — is controlled by CSS (Cascading Style Sheets). It's the layer that says what things look like, separate from the markup that says what things are.
A single CSS rule looks like this:
p {
color: gray;
font-size: 18px;
}"Every <p> is gray and 18 pixels." That's the whole idea — pick something on the page,
list what it should look like.
You will almost never hand-write CSS in this course. But knowing the vocabulary is what lets you direct the agent precisely. Four words cover most of it:
- Color — text color, background color.
- Spacing — padding (space inside a box) and margin (space outside it).
- Layout — how boxes sit relative to each other: in a row, in a column, centered.
- Typography — font, size, weight (bold), letter spacing.
Say "more spacing between the cards and a bolder heading" and the agent knows exactly which knobs to turn. That's the payoff of knowing the words.
Tailwind: styling without leaving your component
Instead of writing CSS rules in a separate file, you'll use Tailwind — a set of tiny utility classes you drop right onto the element. Each class is one small style:
| Class | What it does |
|---|---|
text-lg | larger text |
font-bold | bold text |
p-6 | padding on all sides |
gap-3 | space between stacked children |
flex | lay children out in a row or column |
rounded-xl | round the corners |
text-center | center the text |
You snap them together like Lego, same as components. text-lg font-bold = big and bold.
No separate file, no switching context — the styling lives right next to the markup it
styles.
You already wrote a pile of these. Look back at your Hero from last lesson:
<div className="flex flex-col items-center gap-6 text-center">
<h1 className="font-bold text-4xl tracking-tight">{name}</h1>
<p className="text-lg text-muted-foreground">{subtitle}</p>
<Card />
</div>Now you can read it line by line:
flex flex-col items-center gap-6 text-center— stack the children in a column (flex flex-col), center them (items-center,text-center), with agap-6of space between each.font-bold text-4xl tracking-tight— the heading is bold, extra-large (text-4xl), with slightly tightened letter spacing (tracking-tight).text-lg text-muted-foreground— the subtitle is large and a muted gray. (Hold thattext-muted-foreground— it's special, and we get to it below.)
It's not magic markup anymore. It's a list of small, readable adjustments.
Why Tailwind, and not plain CSS files? Two reasons. It's faster — you style without leaving the component or inventing names for things. And — this matters for you — AI agents write Tailwind extremely well. It's compact and unambiguous, so when you say "make this card bigger with more breathing room," the agent lands it on the first try far more often than with hand-rolled CSS. Reading Tailwind is a prompting superpower.
Feel it: change one thing
Don't just read — turn a knob. In your Hero, bump the heading bigger and loosen the
spacing:
<h1 className="font-bold text-4xl tracking-tight">{name}</h1> <h1 className="font-bold text-6xl tracking-tight">{name}</h1> <p className="text-lg text-muted-foreground">{subtitle}</p> <Card /></div>Save, watch https://web.localhost update. text-4xl →
text-6xl and your name jumps in size. That instant loop — change a class, see it move —
is how you'll learn what each one does without memorizing a list. Try text-2xl,
text-5xl, gap-10. Poke at it.
Don't know the class for something? Ask the agent: "What Tailwind class makes text a softer gray?" or just "make the subtitle smaller and lighter." You don't need the reference memorized — you need to recognize what comes back.
One trap: don't hardcode pixels
Tailwind lets you write an exact value in square brackets — text-[16px], p-[20px],
gap-[13px]. It's tempting when you want just that size. Resist it. Reach for the
named scale (text-base, text-lg, p-5, gap-3) instead, and let the agent know you
want that too.
Here's why it matters. Some people set a larger default text size in their browser —
because they have trouble reading small text, or just prefer it bigger. A size written in
pixels ignores that setting completely: text-[16px] is locked at 16 pixels for
everyone, forever. For a reader who bumped their font up, your text stays stubbornly tiny.
That's an accessibility problem — you've shut some people out.
The scale classes avoid it because they're built on a unit called rem instead of
pixels. 1rem means "one times the browser's base font size." If the base is the usual
16px, 1rem is 16px — but if a reader sets their base to 20px, that same 1rem becomes
20px, and your whole layout scales up to meet them. rem is relative; pixels are
fixed. Tailwind's text-lg, p-6, and friends all use rem under the hood, so they
scale with the reader. Stick to the named scale and you get this for free — drop to
text-[16px] and you throw it away.
Rule of thumb: if you catch yourself (or the agent) writing
[…px]for text or spacing, swap it for a scale class. The handful of cases where an exact pixel really is right — a1pxborder, a hairline divider — are rare and obvious. Everything else should scale.
Meet shadcn/ui
Remember the wall of buttons, cards, and sliders from lesson 2 — the demo you deleted? Those were shadcn/ui components: a set of polished, ready-made building blocks (Button, Card, Input, Dialog, and dozens more) that already look good and handle the fiddly details — focus rings, hover states, dark mode, accessibility — that you'd otherwise sweat over by hand.
Two things make shadcn/ui different from a normal component library:
- The source lives in your project. When you add a component, its actual code drops
into
packages/ui/src/components/— yours to read and edit, not hidden inside some package you can't touch. - Components ship with variants. A
Buttonisn't one look — it has built-in styles (default,outline,ghost,destructive) and sizes (sm,lg), so you pick a flavor instead of restyling from scratch. These four are the live shadcn buttons from this very site:
Same component, one variant prop — four looks, no styling written.
You hand-built a Card last lesson precisely so this next part clicks: you know exactly
what a card is now, so a ready-made one is an upgrade, not a mystery.
Add the Button and Card
Heads up — your template already ships the full set. The starter you're on comes with the entire shadcn/ui suite already sitting in
packages/ui/src/components/(that's what the demo in lesson 2 was showing off). So Button and Card are already there — you can skip straight to importing them. The steps below are how you'd add any component that isn't included yet — worth doing once so the move isn't a mystery when you need aDialogor aCalendarlater.
Add two components. The easiest way is to ask your agent:
Add the shadcn/ui button and card components to this project.Or run the command yourself from inside the web app:
cd apps/web
bunx shadcn@latest add button cardEither way, the source files appear in packages/ui/src/components/ — button.tsx and
card.tsx — and you import them with the @workspace/ui shortcut (the shared toolbox,
from lesson 2):
import { Button } from "@workspace/ui/components/button";
import { Card, CardContent, CardFooter } from "@workspace/ui/components/card";Notice the shadcn Card comes in pieces — Card, CardContent, CardFooter (and
CardHeader, CardTitle, CardDescription if you want them). That's so you can compose
the parts you need. Same brick idea, just pre-cut.
Swap your Card for the shadcn one
Here's the payoff. Open apps/web/src/components/card.tsx — your hand-built card — and
rebuild its insides out of the shadcn pieces. The - lines go, the + lines come in:
import Link from "next/link";import { Button } from "@workspace/ui/components/button";import { Card as ShadcnCard, CardContent, CardFooter } from "@workspace/ui/components/card";interface CardProps { text: string; buttonLabel: string; href: string;}export const Card = ({ text, buttonLabel, href }: CardProps) => ( <div className="flex flex-col items-start gap-3 rounded-xl border p-6"> <p>{text}</p> <Link className="rounded-md bg-foreground px-4 py-2 text-background" href={href}> {buttonLabel} </Link> </div>);export const Card = ({ text, buttonLabel, href }: CardProps) => ( <ShadcnCard> <CardContent> <p>{text}</p> </CardContent> <CardFooter> <Button asChild> <Link href={href}>{buttonLabel}</Link> </Button> </CardFooter> </ShadcnCard>);What just happened:
- Your bordered
<div>became shadcn's<Card>(imported asShadcnCardso it doesn't clash with the name of your own component, which is stillCard). All the border, padding, and corner styling you wrote by hand now comes built in. - Your hand-styled link became a real
<Button>— with noclassNameat all. It's already styled. The classes you wrote for it (rounded-md bg-foreground px-4 py-2 text-background) are gone because the Button ships with them. asChildis the clever bit: it tells the Button "don't render your own<button>— wrap my child instead." So you get the Button's looks on a real Next.js<Link>. Remember from last lesson why links need to stay links —asChildlets you keep that and still look like a button.
The part that didn't change is the part that matters: the props are identical
(text, buttonLabel, href), so your Hero still does <Card text={…} buttonLabel={…} href={…} /> with zero edits. You replaced the brick's insides; everything plugged
into it never noticed. That's components paying off.
Save and look at both pages. Same words, but the card now has a real surface, softer
corners, a proper button with a hover state. Less code than you wrote by hand, and it
looks better. Want a different button look? Try variant="outline" or size="lg":
<Button asChild size="lg" variant="outline">
<Link href={href}>{buttonLabel}</Link>
</Button>That's variants — a whole different button without touching a single style.
Design tokens: the colors with names
Look back at one class from your original card: bg-foreground and
text-background. And your subtitle's text-muted-foreground. These aren't
colors like bg-black or text-gray-500 — they're design tokens: named slots like
"the foreground color," "the muted text color," whose actual value is defined in one
place.
That one place is the global stylesheet at packages/ui/src/styles/globals.css
(the theme file from lesson 2's cheat sheet). Inside it, each token is a variable:
:root {
--background: oklch(1 0 0); /* near-white */
--foreground: oklch(0.14 0 0); /* near-black */
--muted-foreground: oklch(0.55 0 0); /* soft gray */
--primary: oklch(0.21 0.01 285);
/* …and a dozen more */
}The common ones you'll reach for:
| Token class | Used for |
|---|---|
bg-background / text-foreground | the page's base surface and text |
bg-primary / text-primary-foreground | primary buttons and accents |
text-muted-foreground | secondary, lower-emphasis text |
bg-card / text-card-foreground | card surfaces |
border | borders (uses the --border token) |
Why this matters more than it looks: tokens are the seam your theme plugs into. Write
text-black and that text is black forever — in light mode, in dark mode, under any
theme. Write text-foreground and it becomes whatever "foreground" is right now — black
on a light page, near-white in dark mode, your brand color under a custom theme. One
hardcoded color is a thing you'll have to hunt down and fix later. A token follows along
on its own.
The rule of thumb: reach for the named token, not the literal color. It's the habit that makes the next section possible.
Theming: restyle the whole site at once
Here's the reward for using tokens everywhere. Because every component asks for
bg-card, text-foreground, and friends — never a hardcoded color — you can change every
color, corner radius, and font across the entire site by swapping only the variables
in globals.css. Not one component gets touched.
And shadcn/ui turns that into a single command. Open ui.shadcn.com/create and design a look with the pickers:
- Style and base color — the overall palette and neutral tone
- Theme — the accent/brand color
- Chart colors, icon library, and radius — the rounding on corners
- Heading font and regular font
As you pick, the preview updates live. When you like it, the page gives you a preset id — a short code like:
--preset b3ZzDeHhRoCopy it, then run apply from inside the web app:
cd apps/web
bunx --bun shadcn@latest apply --preset b3ZzDeHhRoSave, refresh https://web.localhost, and watch the whole site
shift at once — your Hero, your Card, the button, every page you've already built
and every component in packages/ui — new accent color, new fonts, rounder (or sharper)
corners, all of it. The command rewrites the token variables in globals.css; your
components were already pointed at those tokens, so they all swing into line together. (No
terminal handy? Tell the agent: "apply the shadcn preset b3ZzDeHhRo to this project.")
The contrast that teaches the lesson: every element built on tokens re-themes for free — that's the magic you just saw. But anything you'd hardcoded — a stray
text-gray-500orbg-black— sits there unchanged, now clashing with everything around it: an obvious wrong note in a freshly retuned site. That's why the earlier rule matters. Reach fortext-muted-foregroundovertext-gray-500,bg-backgroundoverbg-white, and a whole new design is one preset away. Hardcode colors and you sign up to hand-fix every one of them.
What's next
You took a plain page and gave it a real look: you can now read the Tailwind you write, you swapped a hand-built brick for a polished shadcn/ui one without breaking anything plugged into it, and you learned the token-and-theme system that restyles the entire site from one file. Your site looks like something now.
It's still only running on your laptop, though — nobody else can see it. Next lesson, you put it on the internet so anyone with the link can visit.