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 looks plain. Every className="…" you pasted into Hero and Card
last lesson was Tailwind. Now you learn to read it, swap your Card for a shadcn/ui
one, and restyle the whole site from one file.
CSS in one minute
CSS (Cascading Style Sheets) controls color, size, spacing and fonts. It says what things look like; the markup says what things are. A rule:
p {
color: gray;
font-size: 18px;
}"Every <p> is gray and 18 pixels." You'll rarely hand-write CSS here, but the vocabulary
lets you direct the agent:
- Color: text and background.
- Spacing: padding (space inside a box), margin (space outside it).
- Layout: a row, a column, centered.
- Typography: font, size, weight (bold), letter spacing.
Say "more spacing between the cards and a bolder heading" and the agent knows what to change.
Tailwind: styling without leaving your component
Instead of CSS rules in a separate file you'll use Tailwind: tiny utility classes you drop right onto the element. Each 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 |
Snap them together like components: text-lg font-bold is big and bold, sitting next to
the markup it styles. You already wrote a pile in your Hero:
<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:
flex flex-col items-center gap-6 text-center: children stacked in a centered column,gap-6apart.font-bold text-4xl tracking-tight: bold heading, extra-large, tightened letter spacing.text-lg text-muted-foreground: large subtitle in a muted gray. More on that token below.
Why Tailwind and not plain CSS files? You style without leaving the component or naming things. And agents write Tailwind extremely well, so "make this card bigger with more breathing room" lands on the first try far more often than hand-rolled CSS.
Feel it: change one thing
Bump the heading in your Hero:
<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 and watch https://web.localhost update: your name jumps in
size. Try text-2xl, text-5xl, gap-10.
Don't know the class? Ask the agent: "What Tailwind class makes text a softer gray?" or "make the subtitle smaller and lighter." Recognize what comes back, don't memorize it.
One trap: don't hardcode pixels
Tailwind lets you write an exact value in square brackets: text-[16px], p-[20px].
Resist it. Use the named scale (text-base, text-lg, p-5, gap-3) and tell the
agent to as well.
Some readers set a bigger default text size in their browser. text-[16px] ignores it and
stays 16 pixels for everyone; the scale classes use rem, a multiple of that base
size, so your layout grows with them. Exceptions like a 1px border are rare and obvious.
Meet shadcn/ui
The wall of buttons and cards from lesson 2, the demo you deleted, was shadcn/ui: ready-made blocks (Button, Card, Input, Dialog and dozens more) that handle focus rings, hover states, dark mode and accessibility for you.
shadcn/ui differs from a normal component library:
- The source lives in your project. Adding a component drops its code into
packages/ui/src/components/, yours to read and edit. - Components ship with variants. A
Buttonhas built-in styles (default,outline,ghost,destructive) and sizes (sm,lg), so you pick a flavor instead of restyling from scratch. These four are live shadcn buttons from this site:
One variant prop gives the same component four looks.
Add the Button and Card
Your template already ships the full set. The starter has the entire shadcn/ui suite in
packages/ui/src/components/, so Button and Card are already there. The steps below are how you add a component the starter does not ship, worth doing once so aDialoglater is not a mystery.
Ask your agent:
Add the shadcn/ui button and card components to this project.Or run it yourself from inside the web app:
cd apps/web
bunx shadcn@latest add button cardEither way button.tsx and card.tsx appear in packages/ui/src/components/, imported
with the @workspace/ui shortcut from lesson 2:
import { Button } from "@workspace/ui/components/button";
import { Card, CardContent, CardFooter } from "@workspace/ui/components/card";The shadcn Card comes in pieces: Card, CardContent, CardFooter, with
CardHeader, CardTitle and CardDescription there if you want them. Compose the parts
you need.
Swap your Card for the shadcn one
Open apps/web/src/components/card.tsx and rebuild its insides from 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 your own. Border, padding and corners come built in. - Your hand-styled link became a real
<Button>with noclassName: the Button ships with those classes. asChildtells the Button to wrap your child instead of rendering its own<button>, so its looks land on a real Next.js<Link>.
The props are identical (text, buttonLabel, href), so your Hero still does
<Card text={…} buttonLabel={…} href={…} /> with zero edits.
Save and look at both pages: a real surface, softer corners, a button with a hover state,
in less code. For a different look try variant="outline" or size="lg":
<Button asChild size="lg" variant="outline">
<Link href={href}>{buttonLabel}</Link>
</Button>Design tokens: the colors with names
Back to bg-foreground and text-background from your original card, and your
subtitle's text-muted-foreground. These aren't colors like bg-black. They're
design tokens: named slots defined in one place, packages/ui/src/styles/globals.css,
where each 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:
| 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) |
Reach for the named token, not the literal color: text-black stays black under every
theme, while text-foreground goes near-white in dark mode and your brand color under a
custom one.
Theming: restyle the whole site at once
Because every component asks for bg-card, text-foreground and friends, swapping the
variables in globals.css changes every color, corner radius and font across the entire
site, with no component touched.
shadcn/ui turns that into one command. Open ui.shadcn.com/create and design a look with the pickers:
- Style and base color: the palette and neutral tone
- Theme: the accent/brand color
- Chart colors, icon library and radius: the rounding on corners
- Heading font and regular font
The preview updates live. When you like it, the page gives you a preset id:
--preset b3ZzDeHhRoCopy it, then run apply in the web app:
cd apps/web
bunx --bun shadcn@latest apply --preset b3ZzDeHhRoSave, refresh https://web.localhost, and the whole site shifts at
once: your Hero, your Card, the button, every page and every component in
packages/ui. The command rewrites the token variables in globals.css, and your
components already point at them. (No terminal? Tell the agent: "apply the shadcn preset
b3ZzDeHhRo to this project.")
The catch: anything hardcoded, a stray
text-gray-500orbg-black, sits there unchanged and clashes with everything around it. Usetext-muted-foregroundovertext-gray-500andbg-backgroundoverbg-white, and a new design is one preset away.
What's next
You can read the Tailwind you write, you swapped a hand-built brick for a shadcn/ui one without breaking anything plugged into it, and you know the tokens that restyle the site from one file.
It's still only on your laptop. Next lesson you put it on the internet.