Chapters

Hide chapters

React Apprentice

First Edition · web · React 8.0.0 · Visual Studio Code

Section I: Rendering Right

Section 1: 7 chapters
Show chapters Hide chapters

6. Styling & Accessible Markup
Written by Eli Ganim

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the previous chapter, the catalog became data-driven: a typed array in, a filtered, keyed list of cards out, with a featured pick on top and a graceful message when nothing matches. Functionally, it’s the real thing. Visually, it’s a stack of gray paragraphs wearing a browser’s default styles.

This chapter gives the Learning Tracker its face. You’ll lay down a global CSS foundation, arrange the cards in a responsive grid, turn levels into color-coded badges and dress the buttons — including their keyboard focus states. React’s part in this story is small and precise: the className prop you met in Chapter 3, driven by your typed data.

Just as important is what the styling sits on. Looks attract users; semantics and accessibility keep the interface usable by all of them — people navigating by keyboard, by screen reader or on a four-inch screen. You’ll build both together, because that’s the professional habit: Accessibility is much cheaper as a foundation than as a retrofit.

One note before you start: This is a React book, so the CSS itself gets a lighter touch than the React code. You’ll paste style rules in larger chunks and focus on how components select them.

Laying the Global Foundation

Every page inherits from a few global rules: fonts, colors, spacing defaults. Those live in src/index.css — the file main.tsx imports for the whole app. It still contains the Vite template’s styles, which fought you in no way so far only because the app was too plain to notice. Replace the entire contents of src/index.css with:

:root {
  color-scheme: light;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: system-ui, 'Segoe UI', Roboto, sans-serif;
  background: #f4f6f8;
  color: #22272e;
  line-height: 1.5;
}

Four small decisions with app-wide reach. color-scheme: light tells the browser to render the app light regardless of the OS theme — real products often support both themes, but pinning one keeps your screen matching this book’s screenshots. The * rule makes every element size itself by its border box, the sizing model everyone expects. And body sets the typography: a system font stack that uses whatever your OS considers its native font, dark gray text on a soft gray page.

Save and check the browser:

New fonts, new colors — the template's defaults are gone.
New fonts, new colors — the template's defaults are gone.

Different fonts, a softer background and everything crammed against the left edge — the layout is still yours to build.

Building the Page Scaffold

Before decorating, get the bones right. The page’s markup should say what each region is — that’s semantic HTML, and it’s what screen readers, keyboards and search engines navigate by.

import logo from './assets/logo.svg'

function PageHeader() {
  return (
    <header className="page-header">
      <img src={logo} alt="" className="logo" />
      <div>
        <h1>Learning Tracker</h1>
        <p>Your course catalog starts here.</p>
      </div>
    </header>
  )
}

export default PageHeader
return (
  <>
    <PageHeader />
    <main>
      {featuredCourse !== undefined && (
        <p className="featured-banner">
          Featured: {featuredCourse.title}
        </p>
      )}
      <section className="catalog" aria-label="Course catalog">
        {visibleCourses.map((course) => (
          <CourseCard key={course.id} course={course} />
        ))}
      </section>
      {visibleCourses.length === 0 && (
        <p className="empty-note">
          No courses match this level yet.
        </p>
      )}
    </main>
  </>
)
main {
  max-width: 960px;
  margin: 0 auto;
  padding: 0 20px 48px;
}

.page-header {
  max-width: 960px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 24px 20px;
}

.page-header .logo {
  width: 48px;
  height: 48px;
}

.page-header h1 {
  margin: 0;
  font-size: 1.75rem;
}

.page-header p {
  margin: 4px 0 0;
  color: #57606a;
}

.featured-banner {
  background: #fff8e6;
  border: 1px solid #e6d9a8;
  border-radius: 8px;
  padding: 10px 14px;
  font-weight: 600;
}

.catalog {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 20px;
}
A real page: branded header, featured banner and a responsive card grid.
A cauq gohi: fjomxow ceasob, coetuyum wecmuh uvk o marxagvona mafy mnes.

Styling the Cards

The grid cells need to look like cards. All card markup flows through one component — the Card container from Chapter 4 — so the visual shell has exactly one home. In src/Card.tsx, add the class to the article:

return <article className="card">{children}</article>
<p className="course-description">{course.description}</p>
<p className="duration">
{course.isFavorite && (
  <p className="favorite-note">One of your favorites</p>
)}
.card {
  background: #ffffff;
  border: 1px solid #d8dee6;
  border-radius: 12px;
  padding: 20px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

.card h2 {
  margin: 0;
  font-size: 1.2rem;
}

.course-description {
  margin: 0;
  color: #57606a;
}

.duration {
  margin: 0;
  font-size: 0.9rem;
  color: #57606a;
}

.favorite-note {
  margin: 0;
  font-size: 0.9rem;
  font-weight: 600;
}

.empty-note {
  color: #57606a;
}
White cards on a soft grid — the catalog starts looking intentional.
Srowo luwzf ut u hazl xcop — fsu pejazog yvebfh feinubt uhpeyfuaban.

Turning Levels Into Badges

Level: Beginner as plain text works; a color-coded badge communicates faster. This is where your typed data starts driving CSS. Replace the contents of src/LevelBadge.tsx with:

import type { CourseLevel } from './courses.ts'

type LevelBadgeProps = {
  level: CourseLevel
}

function LevelBadge({ level }: LevelBadgeProps) {
  const badgeClass = `level-badge level-${level.toLowerCase()}`
  return <span className={badgeClass}>{level}</span>
}

export default LevelBadge
.level-badge {
  display: inline-block;
  border-radius: 999px;
  padding: 2px 12px;
  font-size: 0.85rem;
  font-weight: 600;
  border: 1px solid transparent;
}

.level-beginner {
  background: #dcf5e3;
  border-color: #9fd8ae;
  color: #0f5426;
}

.level-intermediate {
  background: #fff1d6;
  border-color: #e6c37a;
  color: #6a4a00;
}

.level-advanced {
  background: #ece4fb;
  border-color: #c3aef0;
  color: #45278c;
}
Green, amber and violet badges — with the level spelled out in every one.
Gyeis, uwruh ozf moecev nawzum — zarx kje nahun qcodyur uew up ofavp asa.

Visualizing Duration With an Inline Style

Everything so far used classes, and that should be your default. But one styling job genuinely can’t live in a stylesheet: a value computed from data at render time. The card is about to get one — a small meter under the duration line, filled in proportion to the course’s length.

let meterWidth = 0
if (course.durationHours !== undefined) {
  meterWidth = Math.min(course.durationHours * 8, 100)
}
{course.durationHours !== undefined && (
  <div className="duration-meter" aria-hidden="true">
    <div
      className="duration-fill"
      style={{ width: meterWidth + '%' }}
    />
  </div>
)}
.duration-meter {
  width: 100%;
  height: 6px;
  border-radius: 3px;
  background: #e6eaef;
}

.duration-fill {
  height: 100%;
  border-radius: 3px;
  background: #4a5568;
}
Data-driven meters: the only inline style in the app, and rightly so.
Coni-pgezoz funakg: jne iwxq ibyiva dnwlu eg mro ehk, ozv ceyptxs du.

Dressing the Buttons

The buttons still wear their browser defaults. Their component already computes a class from the variant prop — with string concatenation, which you can now upgrade. In src/Button.tsx, change the className line to a template literal:

className={`button ${variant}`}
.button {
  margin-top: auto;
  border-radius: 8px;
  padding: 8px 16px;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
}

.button.primary {
  background: #24467c;
  border: 1px solid #24467c;
  color: #ffffff;
}

.button.ghost {
  background: transparent;
  border: 1px solid #24467c;
  color: #24467c;
}

.button:focus-visible,
a:focus-visible {
  outline: 3px solid #2f6fde;
  outline-offset: 2px;
}
Primary buttons, pinned to the card bottoms by a flexbox trick.
Qvabefj todpuny, kavfiw jo pja gujm zaqwodh zn u qdavhob khuwz.

The focus ring: a keyboard user's you-are-here marker.
Xfe koxus muwj: i butrairc ital'k zia-emu-xewa xurbut.

Auditing the Page

Styling sprint done. Before calling the catalog polished, walk through a quick audit — the same checks worth running on any page you ship:

One-column catalog at phone width — auto-fill doing its quiet work.
Odo-torawz giwojif ob nyoni yosgj — uuxu-biwc jiisp ajj liaak xacq.

Challenge: Polish the Phone Layout

The narrow view works, but it spends space like a desktop: roomy paddings, a wide header, buttons sized for cursors. Add a small-screen refinement pass to the bottom of src/App.css using a media query — CSS that applies only below a width you choose:

@media (max-width: 480px) {
}

Key Points

  • Global rules — fonts, colors, box-sizing — live in index.css; component and layout styles live in App.css, selected via className.
  • Semantic HTML is the foundation: header and main landmarks, ordered headings, real button elements. Buttons act, links go — and neither is ever a clickable div.
  • A grid with repeat(auto-fill, minmax(280px, 1fr)) is a complete responsive layout in one declaration.
  • Style shared shells once: One class on the Card component styles every card in the app.
  • Build class names from typed data with template literals; a union-typed variant keeps the set of class names finite and findable — though TypeScript can’t check that the CSS exists.
  • Inline styles are for genuinely dynamic, data-computed values — like a width percentage — and nothing else.
  • Decorative images take alt=""; informative images describe themselves; redundant visuals take aria-hidden="true". Never omit alt.
  • Style :focus-visible boldly — keyboard users navigate by it — and Tab through your own page as a routine test.
  • Meet 4.5:1 contrast, and never communicate by color alone: The badges carry their level as text.

Where to Go From Here?

The Learning Tracker finally looks like something you’d show a friend — and, better, it holds up for users who never touch a mouse. The styling system is small but principled: semantic bones, classes driven by typed data and exactly one justified inline style.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now