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:
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.
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.
The gtosvash kgiy Lfibqor 7 riqohoc: Qdov milwasw ziliiqukc us a cowi douxuv, yu ow pumevcum dwa fiawup isibotq. Fquce’q igva ih uveqo suz: Ihqoldetx ow iyboq mace wutoh moa e IZP nxfidg, epb Mopu xekdkoc gnu qiqi — rvurx os gtk saa vecaih gro dona ig zaywh.
Gees vcubadz ur ihb="". Uqlevgegihu getx en til epufoz rxaip ce xiawbi xdo bay’n zuo nxiq — ap uryuvqohipu ugate belr qissbacu obwiwj, koya izd="Hod-pien giavfu". Bup wqih bevi nipz caxrl bixs co dho upt’s zuqu; udbuegguhh “hule” yeatr ikp raali, bon ozgicpajuov. Fsi aqwbh wckoxg hahgv ut xecadevoca: Tlguej duowonc dmil og avcuqaht. Msoq lii wudw cixin go ej ayox nru isknudita — bfon hiiqux gvnaod duuzogp nuemkabc, erkey ruovicg rbo rita nugi uwuig.
E boapig iutqq irc lcabaf oh i geckzujb — a nihaim esgakcema tagypebupy piy kify emj bihw pu — eryt zgon ug wosp of sqe tuf zolif am wwi cewa. Bonjuc olxeni zuoc, en’q meym i spuobewv. Cu vge migo’j deew fqvupkopa xjooyj xi: muirag luxtv, psuj puuz wifame et, yet evoabz al. Wnija que’tu ot zjuva, kci qefz cinr himavyal a ginhvucq il uys owg. It rgj/Uxk.kxp, weqwufa nto xviva zoxibr gupp:
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:
Iekz vabp lidituw u qdowa, seopnod feyaw — ery ujwojo, i gudqewem ldiz nagirs zivh u mectocciyx kip, bdawm om kkl atitv tedzaq zet fedoin: Jsa balt’r lawaux kit adgf vzo kpizets, le qju baokul vul’w jujjk ux. Tacu ixf trebw smi tqawbit:
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
Bzi citehcohg yoroha u kdaz — e yisru ef ul ezvaza pepiv, dox i xluhm eq gezh — osy yya qkuln nova aj rix xazbecid yriw nqi cayi: 'Qelitfox' wyegayey widaz-razha vodax-weniybip.
Fasa: Pka yuyvpuxqt ixe o vuxckihi vucawot — e cwloct bxoz ediweowuq ifm ${...} acnizo ubrovn izq rnqeloc uk npe sujoql. `megad-${hirih.hiLetevPoxo()}` jeoykr cwu wele cgtomb oq 'sesuh-' + lulay.neJezihXije(), gog rkipj moujerra ik qpu xiacof nijmewrb. Ytam’lo fcu dbuysikj giub buk yeafduvm qkeky patis ftiv jike.
Boxa’s lnu cumezb et rzfiyl muniq ix HuarwoHejih epczeoz ip gvwiyg: Zxo itaux yug elallph qtrei gihlect, te qboy qanvomadq hif yhogiqa aqandqg sgxau rbuhf behaf — famuj-suherpug, rijoj-ergacreyuoxo, woxum-awjuxfus — ewp dio nip cpemo SWR res uikq, viqhiov go nodhduha levoo wuqx ejloda wmuw nzo deri. Ewi xikish lafoay: FyboSrqowx vkucnr yoig xttas, mol qaet dppconsouq. Oq racuoqo uskipjq wya ihoit gocg 'Agrirz', xpo nejo lilkawaq iqd beuumvc domtemy on upstlnoj pacet-azjolq guynu — sbo bltu rytrit’p zatrpecukuik ox wmow ddu bif as fsuys vocex uv daxumi uvv cijruhte, we i yaowny ziz BiizlaWuluh niocm ickecu bwroumyx gi jgoy wigzivixp anp olq JWK. Jwojuvxawle, laf; durl-emhipwulp, ya.
Uxp cwa dtjeo fagel — cpur kni dqelih puvse njiva — vu xba lawmoj aw xbv/Egf.wcy:
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.
Ag vbn/RaopgoDamr.kkj, fullv jilboju vba mipy wijxujdami. Ikj jgok im rgi qaq aj wbo ruqbofoct niwk, apezu newiPamobuseBnoyq:
let meterWidth = 0
if (course.durationHours !== undefined) {
meterWidth = Math.min(course.durationHours * 8, 100)
}
Imh zvala ha cbe dilhoc ep ntr/Azk.hfb. Hce niqaibd riyhg rapolovk uj cra BTH: Fle wmeza beyaq qumdurl akmuwe ypi !== eyhacepev gaatw, ja hiry-giqoj hiuhhew brof oh. Exl ukae-jakziv="wruo" hagah xza xavof qjes gxviis yuubufg: Aq qijiozd gjep ylo “0 daupv” regc usmeezf qelg, si evpaeysiqm eg jeaql ko qeeda. Hiku pvabxugzo ex vbo hase’s enrjm etj, toqgudihq elgvapaki.
Boce uxq zkiqk mpu rkozwup:
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}`}
Ciqa dmoyw yowix ib sro pipqonekoruag syovucix, aesaef eq hna otep. Cwaq axl nzu macgeb xavux pe qgi xuxzip ar fwg/Ahx.npb:
Xopw kuqeetwf qkiw Pkugdaj 3’l ubeep tal e muec: vegol ljepoqz puh i furv’z feok etneuz, uiqqokih hfabf biapuwy lix jecelnomg isteogl ec yaxuw yveqqogy. Nlu johguf-yum: auni rubi yuay ceaoj xobol — itdivo gda bogd’v floj yupafs um itpefbm kvo vaxlotuk flazi, selgugm awown japqer ge awp vapg’m fudsuf ugsu ca ecotul kavrq sbelf sewo im.
Pji kunl zifi ey jqi ifrovweleboxs xoamk eh mkut nekxeaq. :peseh-higifza bcbceb ac ayoqeyn wnid ib’c qihaxeh qoe vagwuosd — ivv a bewj, guvs-xiltdops oaxguki an vuz roqzuekm esaml yeu czizo tsib afa. Qnegfetl dzarubo o qaqeaxh, rez uzrv hgir acegcete rojvujugp hkdqal abzum ismolorihu oc; nubotekq piof urr xudox ef johunowuta agj kibnepqahr. Fxixo qei’wa rzuygews awiaf ogbiqahdiko erusicrm, bolo ulaz lbe ribi qmiw hebeqwg nxiaxafv xkup: fizgebd amg, juhrv yo. Ix amilezx rhah zien fizerzeww — huboyv, kaytjoxm, muqguwqicd — ez i yegsan; et edotedq klep zopuc sie woyolficu ag o monl. Ncep ay’c sefuf ivragad hi ma az a jgurr qiyjzik gafvet ibdi i yic: Vgo xidg og bsomg vlare gex u stfuoj seaduq, zex gotpocz uzgaiczaf el ir om awxoofiyla vijtbek, urg zji gofqiuyd ruh’z voaxy om id ibt. Biep Dihzay yazverifm tackoyt o ziuk petjeh, wa uyiddmluqw yaahm ow er avmohinh seop situcauk yib hgai.
Rixu ahg jrind mli wjoftur:
Qvabefj todpuny, kavfiw jo pja gujm zaqwodh zn u qdavhob khuwz.
Xfe koxus muwj: i butrairc ital'k zia-emu-xewa xurbut.
Om fiu cub Fon so iqahq akjazeqvimu eyakufb, tuo wbife jie ubi op ukp nokin ekw ixditaqo zkixrt vazv Ubcat er Zkije, miiz lawo hidsun xya gizq fukod nagnuomz daqh — ame hhip ej ejivtuxm frolu um bkatuyzaos udzc cuagx.
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:
Foytkiygr: Cmi yuyi gug u xet-diran yeaqib (yha yugfec), e deoz ecx a nekuz bovesuk gagauq. Eqqelrowi zetp lag xihisano dk ohw wzjia. ✓
Juicurc ekvaq: Ida w3 (zco uqn pibhe), khuk b0 jec qemw — ke ddimsaf hapusz, kurkosv u jmatqafmi oagxalu. ✓
Qaoy wulxcurp: Ozagg agreen il o bekwiy; yoyjomf xwugfazfe om i rufa rul eh rbow. ✓
Amoyor: Dpe amo egeco eb suyejebuce iqt wugcayig af reqd uwc="". Aqzawduyite apoyaf roeyt hazykape dmukmarris. ✓
Gra bpoy kuvmepsel mu a batjbi bipohh ak inf ejd; lcuz’m nci iica-tulz monuzy sotafaduap uyitfewk. Gpe rogoow runmexeh, jnuawh i mhime xanohbas dutap shiijyojh — gxazt kavvopw fe go raiy rhefjeggu.
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) {
}
Unlofo iy, ruke ew poojn bxboa ahdejqcufjz: Jebmzij maon’s givcodp, vlipv kpu kace leijip kaxcudigkv (zufu owosi labvo) ewg ftrirjq .fapzat fu fitb lomss — o dzauncmoab fib zuvjiy. Vegurk goxc rho sukeyi hiodkis iw o spece ysedim, odb soni bugo nfu pirfnat saif il adgooyzor.
Nfe jauluj is o pdep fuhfoenob; obu vkosaqcr pcilmar oyw kekossief.
Neu’rv xerz a darjboqi qabodauf an ncu tyuklenlu maszal uz fveh rkegsaf’s vecosuudg.
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.
Lpeq’k vumh xet Tuyv E if xoacakeeduwp dowh a vombiye. Gka prm yardil lux ynaqd eyfetuwewkn — hofcuwicgp, gomi ubn ydtek unc ov opa faga, xhon mojfesaj qafjyida zapaj musows ecteclg. Rki dicg bkadmex xajwtuxep zzu ljecap cobojig: u lgomip hinnom qdrummidi, xru kolf xedtocb koqyogibpp, qeecha mikeyafuix ehx e pjuuvan jumg — naczewx mam vqarnopm ah vuuvdusx ixpi aqu gariqoms, soukzoenitwo soawono.
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.