Andrey Markin
  • home
  • services
  • projects
  • blog
  • directory
  • courses
    • Vibe Coding: Build Real Apps with AI Agents
  • resume
  • about
  • contact
  • meet

Mark Life Ltd

  1. Home
  2. Courses
  3. Vibe Coding
  4. Styling And Shadcn
Meet

Mark Life Ltd

BG208147965

HomeContactPrivacyLLM-friendlyBlog RSSDirectory RSS

Vibe Coding: Build Real Apps with AI Agents

  1. 01Set Up Your Environment
  2. 02Find Your Way Around Your Project
  3. 03Pages and Components: Make It Yours
  4. 04Make It Look Good: Styling, Components, and Themes
  5. 05Deploying Your App to the Web
  6. 06Apps and Interfaces: Bots, Desktop, and Mobile
  7. 07Where Apps Store Data: Files, Key-Value, and Databases
  8. 08Fast, Temporary Storage with Redis
  9. 09Adding a Database
  10. 10Adding Authentication
  1. Courses
  2. Vibe Coding
  3. Make It Look Good
Lesson 4 of 4

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:

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

ClassWhat it does
text-lglarger text
font-boldbold text
p-6padding on all sides
gap-3space between stacked children
flexlay children out in a row or column
rounded-xlround the corners
text-centercenter 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:

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 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 a gap-6 of 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 that text-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:

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, 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 — a 1px border, 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 Button isn'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 a Dialog or a Calendar later.

Add two components. The easiest way is to ask your agent:

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

Or run the command yourself from inside the web app:

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

Either 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):

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

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 the name of your own component, which is still Card). All the border, padding, and corner styling you wrote by hand now comes built in.
  • Your hand-styled link became a real <Button> — with no className at 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.
  • asChild is 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 — asChild lets 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":

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

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 you'll reach for:

Token classUsed for
bg-background / text-foregroundthe page's base surface and text
bg-primary / text-primary-foregroundprimary buttons and accents
text-muted-foregroundsecondary, lower-emphasis text
bg-card / text-card-foregroundcard surfaces
borderborders (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:

text
--preset b3ZzDeHhRo

Copy it, then run apply from inside the web app:

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

Save, 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-500 or bg-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 for text-muted-foreground over text-gray-500, bg-background over bg-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.

Previous