100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Tailwind CSS & Modern CSS
50 minbeginner

Foundations Practice — Styling a Card Component

What You'll Build

You will build a polished, responsive player card component for a cricket statistics app — the kind of card that appears in a grid of players, showing a name, role, a key stat, and an action button. This single component exercises every foundation from Module 1: the utility-first mindset, on-scale spacing, the type and color systems, and borders/shadows for elevation. By the end you will have a reusable card that looks designed rather than assembled, flexes from phone to desktop, and uses a clean focus ring on its interactive elements. Crucially, you will build it twice — first as raw repeated utilities, then extracted into a reusable component — so you feel exactly when and why extraction earns its place, which is the core judgement utility-first demands.

Analogy🏏Cricket
🏏 Think of it like cricket: This exercise is like a fielding-and-celebration drill session — rehearsing the dive-and-throw, the run-out relay, the choreographed wicket celebration — until each is smooth, repeatable, and perfectly timed. Just as those drills turn raw athleticism into crisp, reliable match-day moments, this exercise turns raw animation utilities into crisp, reliable interface moments. Just as a celebration that is mistimed or overdone looks worse than none, an effect that is janky or gratuitous looks worse than none. The insight is that interactive polish, like fielding flair, is drilled into something smooth and purposeful — practiced motion that lands cleanly every time.

Prerequisites

  • A working Tailwind project (CLI, Vite, or any framework starter) with the content array configured to scan your component files, as set up in Lesson 02.
  • Comfort with the spacing scale — padding, margin, and gap — and the constrain-not-fix sizing principle from Lesson 03.
  • Familiarity with the type scale, hue-shade color system, and slash-opacity from Lesson 04, including pairing foreground and background for contrast.
  • Understanding of the shadow elevation scale and ring-based focus indicators from Lesson 05, including why rings avoid layout shift.
  • A code editor with a browser preview and Node.js installed if you are using a build tool rather than the standalone CLI.

Setup & Project Structure

Create a minimal Vite project so hot-reload shows your card update as you type. The structure stays deliberately small: a single entry HTML, a CSS file containing the three Tailwind directives, and a components folder where the card will live. Make sure the content glob in your config covers that components folder, or your classes will silently fail to generate — the exact scanner-contract issue from Lesson 02. The dependencies are just Tailwind and its peer build tooling; nothing else is required for the foundations.

Analogy🏏Cricket
🏏 Think of it like cricket: Set-piece effects are best choreographed in the playbook before you use them, just as a team drills its celebrations and fielding routines in advance. Just as the moves are defined and named in the playbook first so every player can call on them cleanly, you define your custom keyframes, slide-in, slide-out, fade, in the config up front, since several effects depend on them. Just as good coaching offers a calmer version of a routine for players who need it, you confirm motion-safe and motion-reduce work so users who prefer less motion are respected. Just as each drill is rehearsed on its own before being strung into a match routine, you keep each effect a small component so you can test it in isolation, then assemble the demo. The payoff: a clean foundation where every effect is pre-defined, considerate of motion preferences, and testable on its own before it goes live.
bash
# Create and enter the project
npm create vite@latest cricket-cards -- --template vanilla
cd cricket-cards
npm install
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# Project structure
# cricket-cards/
# ├─ index.html
# ├─ tailwind.config.js      <- ensure content covers ./*.html and ./src/**/*
# ├─ src/
# │  ├─ main.js
# │  ├─ style.css            <- holds @tailwind directives
# │  └─ components/
# │     └─ playerCard.js     <- the reusable card lives here
npm run dev

Step 1 — Foundation

Begin by hand-writing the card as raw utilities directly in index.html, with no abstraction at all. The goal of this step is to compose the box model, type, color, and elevation foundations into one coherent card and see them work together. You will deliberately repeat the markup for two players so the repetition becomes visible — this sets up the extraction decision in Step 3. Focus on staying on-scale: padding from the spacing scale, colors from the same hue ramp, a single shadow level and radius.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 1 grooves the fundamental fielding move — the clean pick-up and lift — the single action everything fancier builds on. Just as the basic clean pick-up must be flawless before attempting the diving relay, the basic hover-lift transition must be smooth before layering on more. Just as a fielder drills the simple action until it is second nature, you drill the transform-plus-transition pattern until it is reflexive. The insight is that the foundational motion must be clean first, because every richer effect is composed from it.
html
<!-- src/style.css -->
<!-- @tailwind base; @tailwind components; @tailwind utilities; -->

<!-- index.html : raw, repeated card markup (intentionally not yet abstracted) -->
<main class="min-h-screen bg-slate-100 p-6 flex flex-wrap gap-6 justify-center">

  <!-- Card 1 -->
  <article class="w-full max-w-xs bg-white rounded-xl shadow-md border-t-4 border-blue-700
                  p-5 space-y-2">
    <p class="text-xs font-semibold tracking-wide text-blue-700 uppercase">Batsman</p>
    <h3 class="text-xl font-bold text-slate-900">Rohit Sharma</h3>
    <p class="text-sm text-slate-500">Highest ODI score: 264</p>
    <button class="mt-3 w-full py-2 rounded-lg bg-blue-700 text-white font-semibold
                   shadow-lg hover:shadow-xl transition-shadow
                   focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:outline-none">
      View Scorecard
    </button>
  </article>

  <!-- Card 2 : SAME markup repeated (note the duplication) -->
  <article class="w-full max-w-xs bg-white rounded-xl shadow-md border-t-4 border-blue-700
                  p-5 space-y-2">
    <p class="text-xs font-semibold tracking-wide text-blue-700 uppercase">Bowler</p>
    <h3 class="text-xl font-bold text-slate-900">Jasprit Bumrah</h3>
    <p class="text-sm text-slate-500">Career economy: 4.6</p>
    <button class="mt-3 w-full py-2 rounded-lg bg-blue-700 text-white font-semibold
                   shadow-lg hover:shadow-xl transition-shadow
                   focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:outline-none">
      View Scorecard
    </button>
  </article>

</main>

Step 2 — Core Logic

Now make the layout genuinely responsive and the card content robust. The wrapper already uses flex flex-wrap with gap, so cards reflow naturally; in this step you add responsive sizing so cards are full-width on phones but settle into a tidy fixed-ish width on larger screens, and you add a stat-emphasis treatment so the key number stands out. This is where the constrain-not-fix principle pays off: max-w-xs caps the card while w-full lets it shrink, so the same markup works from 320px to a wide grid without media-query juggling.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 2 is the warm-up-to-play transition — the team moving from net practice into the actual innings, a deliberate handover from preparation to performance. Just as the warm-up gives way smoothly to live play, the skeleton gives way smoothly to real content. Just as a jarring switch from nets straight to facing the new ball would unsettle a batsman, a jarring pop from skeleton to content unsettles the user. The insight is that the transition between a holding state and the real state should be as considered as the states themselves — the warm-up handover and the skeleton-to-content swap both reward a smooth changeover.
html
<!-- The card body, now with responsive sizing and an emphasised stat -->
<article class="w-full sm:w-72 max-w-xs bg-white rounded-xl shadow-md
                border-t-4 border-blue-700 p-5 space-y-2">
  <p class="text-xs font-semibold tracking-wide text-blue-700 uppercase">Batsman</p>
  <h3 class="text-xl font-bold text-slate-900">Virat Kohli</h3>

  <!-- Emphasised key stat: large number, muted label -->
  <div class="pt-1">
    <span class="text-3xl font-extrabold text-slate-900">50</span>
    <span class="text-sm text-slate-500 ml-1">ODI centuries</span>
  </div>

  <button class="mt-3 w-full py-2 rounded-lg bg-blue-700 text-white font-semibold
                 shadow-lg hover:shadow-xl transition-shadow
                 focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:outline-none">
    View Scorecard
  </button>
</article>
<!-- w-full sm:w-72 : full width on phones, ~18rem from the sm breakpoint up -->

Step 3 — Integration & Enhancement

The repetition from Step 1 is now a proven, stable pattern, so this is the moment extraction earns its place — the exact judgement call from Lesson 01. Extract the card into a reusable function that takes data and returns the markup, so each card is one call with its values, not a copy-pasted block. This keeps the styling in one place: change the shadow level once and every card updates. Note we extract into a component (a JS function here), not an @apply class, because the component can also hold the data shaping and, later, behaviour — styling and structure travelling together.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 3 is assembling the rehearsed pieces into the full match-day routine — the entrance, the play, the celebration, the walk-off — each transition choreographed to flow into the next. Just as the day's moments are stitched into one seamless production, your effects are stitched into one coherent interface. Just as the walk-off must be timed so players do not leave before the moment completes, the toast must stay mounted until its exit animation finishes. The insight is that integration is about timing the handovers between pieces — the match-day flow and the assembled UI both depend on each transition completing before the next begins.
javascript
// src/components/playerCard.js
export function playerCard({ role, name, statValue, statLabel }) {
  // Styling lives here ONCE; every card shares it.
  return `
    <article class="w-full sm:w-72 max-w-xs bg-white rounded-xl shadow-md
                    border-t-4 border-blue-700 p-5 space-y-2">
      <p class="text-xs font-semibold tracking-wide text-blue-700 uppercase">${role}</p>
      <h3 class="text-xl font-bold text-slate-900">${name}</h3>
      <div class="pt-1">
        <span class="text-3xl font-extrabold text-slate-900">${statValue}</span>
        <span class="text-sm text-slate-500 ml-1">${statLabel}</span>
      </div>
      <button class="mt-3 w-full py-2 rounded-lg bg-blue-700 text-white font-semibold
                     shadow-lg hover:shadow-xl transition-shadow
                     focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:outline-none">
        View Scorecard
      </button>
    </article>`;
}

// src/main.js — data-driven: each player is one object, not repeated markup
import { playerCard } from './components/playerCard.js';

const squad = [
  { role: 'Batsman', name: 'Rohit Sharma',  statValue: '264', statLabel: 'highest ODI' },
  { role: 'Bowler',  name: 'Jasprit Bumrah', statValue: '4.6', statLabel: 'economy' },
  { role: 'Batsman', name: 'Virat Kohli',   statValue: '50',  statLabel: 'ODI tons' },
];

document.querySelector('#app').innerHTML = `
  <main class="min-h-screen bg-slate-100 p-6 flex flex-wrap gap-6 justify-center">
    ${squad.map(playerCard).join('')}
  </main>`;

Step 4 — Testing & Verification

Run the dev server and verify three things: the cards render with consistent elevation and spacing, they reflow from a single column on a narrow viewport to a wrapped row on a wide one, and tabbing with the keyboard shows a clear focus ring on each button with no layout shift. Resize the browser from roughly 320px upward and watch the cards transition from full-width to the capped sm:w-72 width.

Analogy🏏Cricket
🏏 Think of it like cricket: You prove each effect by running it in isolation and then under real conditions, exactly like rehearsing a drill and then testing it match-day. Just as you check each set-piece plays out smoothly, you verify the stat card lifts and reveals its detail on hover, the loading skeleton swaps cleanly into content, the command bar shows its frosted-glass effect, and the toast fully completes its slide-out before disappearing. Just as you offer a calmer routine for players who need one, you enable reduced-motion emulation and confirm animations are disabled or calmed. Just as a shot must hold up on a slow, tiring pitch and not just a fast true one, you throttle the CPU to confirm the effects stay smooth on weaker hardware. The payoff: verified proof that every effect looks right, respects motion preferences, and performs even under poor conditions, not just on your fast machine.
bash
# Start the dev server
npm run dev

# Expected: open the printed localhost URL. You should see:
#  - Three white cards on a slate-100 background, evenly gapped and centered
#  - Each with a blue top accent border, soft shadow, bold name, large stat
#  - On a narrow window: cards stack full-width, one per row
#  - On a wide window: cards sit ~18rem wide and wrap into a row
#  - Pressing Tab: a 2px blue ring appears around the focused button,
#    and NOTHING around it moves (ring is outside the box model)
#  - Hovering a button: its shadow grows (lifts); content stays put

Warning: The most common error here is that classes render in index.html but not in the extracted playerCard.js — the cards appear unstyled after Step 3. The cause is almost always the content array: src/components/*.js is not matched by a glob, so the scanner never reads the extracted file. Add ./src/**/*.{js,html} to content and restart the dev server; the classes will generate immediately.

Extension Challenge: Add a subtle colored glow on hover using a colored shadow with slash-opacity (hover:shadow-blue-700/30), give the card itself a hover:shadow-lg lift, and add a small rounded badge in the top-right corner using absolute positioning within a relative card — combining elevation, color opacity, and per-corner radius from across Module 1.

  • Compose foundations together: a single card exercises the box model, spacing scale, type scale, color ramp, and shadow elevation as one coherent unit.
  • Stay on-scale throughout — one shadow level, one radius, ramp-based colors — so the card reads as intentionally designed rather than assembled from arbitrary values.
  • Use w-full with a max-width or sm: width cap so one markup adapts from phone to desktop without per-device layouts, applying the constrain-not-fix principle.
  • Extract into a reusable component only after repetition proves the pattern stable, keeping styling in one place and letting the component also hold data and behaviour.
  • Prefer focus-visible:ring for interactive elements so keyboard users get a clear, layout-stable focus indicator without flashing on mouse clicks.
  • Remember the scanner contract — when extracted files render unstyled, the content array glob is the first and usually the only thing to check.
Lesson 6 of 35
0% complete