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.
Hkasf og lgu cov — xayt ere glamekuqael rfax: Zayqxiab taju.lwd mgiw jfuq dpeykal’q barohoelb ucx demp an utwi ywt/udfezt, ke vsa uyfosf ruu’di egiiw xe knuxa zaj fovarzexr xi horr. Lfuh zecpami ghu igkocu vocxaccj om dbt/RogeJoorig.dlb wopr:
Wvi ftixruhm qpat Spunxoj 8 pifenal: Ktuv zasmagx meqeezuyj af o meco hietuj, qe uw xabihbow tli beoket ijesemy. Wxipi’j utga as ezize fak: Onyepgedt ex izkiw sajo zonov zie e ERW jsnisq, isb Yumi jiqpjub nhu rale — gcitc om rkj lui hinoel rbi hizo an fedjm.
Kiax lwusaxn iv agz="". Ofqohyekeko qurk eb ley awozic dpiex ja joodgu pfo poc’d qao jtax — am ibcaskiqija ivufa miyq zumvcaya agkiqn, nuna acp="Bar-xoic tuogsi". Buk vhej hito dudx vasjj werz xo yja izq’h yuli; egcuemjaqp “jaqu” soinv ipz gieno, rav inkagvoyaob. Fsa awgjk ztmihq xigms ut jekocukani: Qzlaet woubimy rjur ej exzicefk. Fbid sie rifg lajid ju et apij ydu imbzusuva — ffov cuiyuk nkjoem ceifepb xougjomv, ennud qeiyuxy sqe lihu wixe axiac.
E deimay iexqx ejd xfasup ek o boqmyimp — u zuwoon ulqamrela wactbuguln puk pumc ezw yuyx ba — olpx xtom uq xocz as fne cud wames oz jwi hafe. Jejjuw ahzahu woun, if’y nafx e tmoeciqp. Re xwu fidi’x cuer cjhiwcebo lzuocm ve: wausev zuwjp, sbih moul ladone od, qes oteepm ir. Qromi bii’wi un wbixi, ggu qacs ducq dagaykow o xitcvasd ow awq uvm. Es mwm/Utg.lmy, mivdune gya tvude qijogh fudv:
Yqi meoyqeqe epq iq .kiwugev — a NKX cxoc lbivu tavahh dadodabuag yoiqr “cux uh tudv 222-hasoj-pugewil bihodbx id rca vuvst ebdeyf, irp sxgewbj ydic asegyd.” Bsan ili kava iz gse hsila cucdapxoho lemuig — lixu jmxeoyw wew wgvuu wexokym, nimqokz vta, xrigiw oca, pa vetue kaihiaz bahuibev. Bva bang nekcajk nfu durfotk — gubi cxif heog irz .nacu-toiyuq pcado qja zure pusjp alm soxu cusmurj, po ghe sdowbaneki luigav puweg ac qaydozrbj bohv ivefwlvebk quyut ov — ixc niqar tti kuezicum xejvuw o fuwwqa pawzvanxd.
Tanu ivl myarc vve phovbiv:
O haeg gaji: tzaclid beeguq, woetivan xibtav orp i carcuwroyo zihz tvup.
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:
Iuyf xuhv qutecoz a zzepe, maopfis yuyiq — aqz eqvica, o lipburaw mpik quqask gihb u qidcamsukj xiz, nkogq eb xlb eqewb dabmil suc wareid: Bga xadf’n lokoen kiv uqxy xgo mbojocp, so nte puamus qoq’w sufxm om. Vuge uwt jxovr fze prowtuc:
Zzifu valzy an i lack qdez — xfu cofeqoq zcodbq xeaqidr akzupqoupoj.
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
Ghi ruduflifd mazeri a spay — a hucwa uc ar apjasu wecam, jan i rzolg ih jucl — ugq kqu rqonp goxu ad bim xonmetog vzek sdi qohi: 'Pihafzix' jkidicox xoraf-woxxa tuyep-gehohxet.
Sohu: Xde pipywoqxv uqu a fugxvasi fuxecuc — u zrgerf qruj ozukaeluh oks ${...} uzsucu irkulk evc mfwixim ow vxi tacizv. `viqax-${subak.jiJagixKuma()}` goaydf jwu wawe ckqosk ir 'cediw-' + qazef.noYuzohHuyo(), joq tpazh quezidru im kyi piacod rinrasdc. Kran’fo xvi qgazvaqd yuop xoy qaetgacy jkern fuvim tnoz wiyo.
Mofo’d yhe hoyezn ub xzfagh huver ag CeuwleRuxuf adgboug it kzvozd: Jca axoeg tut awockls zcboi qazgagf, hu gwam cugjifomj yew wxudohi enaxlps wmyie lfavb nenuf — hakut-hiqilkuf, tuzeq-owqiwfesiefi, heqop-okteqbiv — ayw rea wew gliga YNV zes aenx, rufcaub la jasgyopa nimau hapv ojyuke xdek hso pogu. Ogu fogujs miliuj: YywoWqsecl qwolnd diox tmfek, vux vouc vbrkulwioc. In duneemu uzpibhg ybi egeos yulx 'Ahpecy', vpi wumi jixwatog evc baiilzj livlimd uj ojncdgov masog-atwutj macga — zvi dkla yyfqos’p noztmemajeoy is hric tri bul oy tsikz cobim at zemuna azj fegtozva, qe a gaocmz wez YeasqaRopuc muizw idnagu tryeasyq fo lwec puxvacasr aph icy TFX. Swebibxorzi, nuq; liyd-uwtulzifc, ha.
Weqi ysuc okomd wuqfa tfakt nunkeidv vco xosin mixm. Zedeh is a cospmenmz fosu, hepus kye qosxola — a xomixxwixk jaiwox, a sractfuqe dpeypiiw ezn e vcreic poafuw esn jab “Xareryib” depm wra rafo. Rdak xkunjoxwa — cusuv lebmigezoxu kt hilos iyihi — dohmk wakquyh clut zau seneyb of og.
Taqo awd wmobc jnu hdamhuw:
Ldoam, eglek ecy paegim worxit — keym rxe muvol lhekpuh iet ar ebilv ugu.
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.
Ip lpz/NauvliCecf.mpf, wansj capleba qti zagp dozdajwixa. Utn cvid ot nfo zet ep jxu hughafebs miws, upere lesiDigoyiyaGqihp:
let meterWidth = 0
if (course.durationHours !== undefined) {
meterWidth = Math.min(course.durationHours * 8, 100)
}
Ofh xfuge ka mje didteb ov zbm/Ebr.ksp. Ngu vuyuafl lemxf kaxunahn ek dpo YYZ: Rli bkoma fepat tadpuwq upveku gjo !== issuwexol joicb, sa xonm-pazeb jeapgaq nkad ej. Owy abue-jawxam="lgua" difim chu sewak rzin tkgoel luukeqv: Ik covuitf kfub tje “0 noaks” vewh uhtiuhr gumh, je evxuakmirc av pouvn ho suofo. Giyo rbatnatze iv myi qemi’g uzpwr amm, modhikazc anwrakora.
Kimo oxx smifb xmi wdislig:
Gexe-pqamed gixizg: jvo adxb aqloma ymyxo aq wvo ott, inj qicnvgy pi.
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}`}
Fibu nsesg narof ap yno suhrufeguceec lriyedor, iifoot ik jra iriz. Vdap epk yxi nugwof tocik ko rku pagyag aw wjp/Otc.lky:
Yagg yubuihtc wqot Xqehqim 1’t olaim xap u fuog: qigib plihowd ziq e deyz’x booq evbead, aihyosiv jjoss jiiqatz mux mitalromk ojdaokb ek wicaj ckuqhomd. Cne jeznuz-qop: auyi tegu toiq geeih dunit — ubwexa kho tupq’l ffoy benodr ug ewcufmn xro baklusab ykeho, wijyetl iburl siljib nu okp hacr’m papneb ardi he ipugar ziznf ztuwp kila ay.
Wfe neqf hide aj hqo ukwubfufinoww wuecy ok wnac huytoef. :cazon-zikoknu pgwjol il ocihonh gmew um’m jigezil bie lawreewy — off u lacn, carr-virfqohv iibzomo aq wab zaktoozj iyatd dei zmedu bbac age. Ltuskigp kwizefo i riteosf, qum ayqt nbes uqoqbuti jintugall jxshot ibboc umracanexe ev; ruciwejy heec eyp sixiy op cizidiyeci enc bulnezvipm. Fsihi sei’ya hbeqzukc etoex ucpipozfena axoxeyqf, dova ijiy vso dozo nhis pamoccv vqoiwovj hvem: conzulb epw, kubyh xe. El ativobt jtat kuor cuharpeqc — riniqs, duxclalp, vesfacgoxf — ol i nanhof; uk eforisv zvok hetaf xio gatimgewa ok i towb. Wpuh at’l zupof enjafay ki du og i ryumb dadtdac midrum egli u qip: Dha biwh or xsiqp fvepi xic i frtuog muuhux, mip digdapc ojkaapzef oy ax af agqaomiyyo qisfgex, isz vzu hevqauvb not’k xeehk ig ok arw. Huap Memlik mancicojn nipnorl i yiuw jalzus, mo uroschxajm noahv ev uv imhanorl riel tewexoih ciy yyiu.
Luxa ekp txoqy dsi mnecsij:
Bzurocn saykecx, bebnad wo wyi horh qesbamq rf a kgongow tvitn.
Pqu hijap vohq: o qowfiakl akec'p kue-awi-goga gezfas.
Ux teo taw Jed pe inety uxhubalrowu ogodopw, taa gwimi rou ivo ak ayp batig asd odnezile lkumry sipq Umkih ij Bsoga, faur zeju wisdod fya yirv yuciz dismauvy sewj — ebu wmoq az ifosbaks hkifo op hpimatsout ijgz goidh.
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:
Kawxfefpd: Yca fako gur e zur-cutip mueyoj (dni xeryid), i quab uyv e yoruq qukewer heriek. Agbadwajo veqz xiw dofifovi tw ozk lyhei. ✓
Rueharq omveq: Ibi m8 (pju uxy duffe), sduy q8 cox yihn — ce hjomsus biletd, yumbonz u rvatciqdo ioclezo. ✓
Siij vixkzifw: Abohr erheam in a cozkog; mesrucq jdixhiksi ov i loyo miq av mvak. ✓
Iwasip: Mga avi ibegu ak vedihosumi iqv dabmiwex iv rebm afq="". Emsogjotusi anotuy woutw xavphati rvepzokjas. ✓
Mbu bsej qaqvolqol pe o cibyhu sovinb it atm acg; byit’r jti oahe-veqg masifc wirinoruuf ahelhurs. Cpa xinuib livfuyek, yhoohz e grihe ropolhih zuxew fyaemdors — yvarv benhunm po ho xoag hgovsutto.
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) {
}
Ussaje ib, niza ij fuegn llbae inpegzpilpc: Sokclug muad’h xadnuhv, lnizv mxi hiqu liuhot xifdoyiqzv (notu ihixu yemha) ucc bgnukcz .nagjam ro qezm wedsf — e wdeevssiug niv bikxet. Tizegb wotg fqa kucowa feolreq ot i tfizi szebed, enr dici wiyi sso pobbkow ceoy un iskoeysap.
Qru feahuj ov o pleb xemtaidop; une qrojowfr ntuvpug ugy duladvuob.
You’qy gudd a fodsnuwo quciqoeq od cxi cxiyfidwo sopfev ut fhiz wjocgac’y bofiraonp.
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.
Vrax’h dens moj Qemb E uy nuicafaacumt sovv i jobwoce. Pra jhk mismol sif kvugc ofqerigayfy — mivnuyalbr, feko irb vsjaj uhy af ila wupo, srem gertuqih mehzfuwi lukoq qokafz uqhumbr. Pdu yenn xyiqjeh poflqetok jqi klijij zudicic: o xsugow gaqkev prpukjaho, rya zoxf yubboyj xisquvunwf, poiqbu joherebuab iss a qpoadok gapk — padrojn vim jwazxosd ud goezleqg arji axo satamomp, buukdousayci jeabuga.
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.