# Make It Look Good: Styling, Components, and Themes

Course: Vibe Coding: Build Real Apps with AI Agents — Lesson 4 of 4

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:

```css
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`:

```tsx
<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-6` apart.
- **`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`:

```diff
-  <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](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 `Button` 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 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 a
> `Dialog` later is not a mystery.

**Ask your agent:**

```text
Add the shadcn/ui button and card components to this project.
```

Or run it yourself from inside the web app:

```bash
cd apps/web
bunx shadcn@latest add button card
```

Either way `button.tsx` and `card.tsx` appear in `packages/ui/src/components/`, imported
with the **`@workspace/ui`** shortcut from lesson 2:

```tsx
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:

```diff
 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 as `ShadcnCard` so it
  doesn't clash with your own. Border, padding and corners come built in.
- Your hand-styled link became a real **`<Button>`** with no `className`: the Button ships
  with those classes.
- **`asChild`** tells 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"`:

```tsx
<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:

```css
: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](https://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**:

```text
--preset b3ZzDeHhRo
```

Copy it, then run `apply` in the web app:

```bash
cd apps/web
bunx --bun shadcn@latest apply --preset b3ZzDeHhRo
```

Save, refresh [https://web.localhost](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-500` or `bg-black`, sits there
> **unchanged** and clashes with everything around it. Use `text-muted-foreground` over
> `text-gray-500` and `bg-background` over `bg-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.

## Links

- Lesson page: https://andrey-markin.com/courses/vibe-coding/styling-and-shadcn
- Course: https://andrey-markin.com/courses/vibe-coding.md
