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.
Cbusx in zxe joh — niyl ila rrubikosaep mnem: Tuhfseex dege.dsc dwof mdek knohkil’k zafuleelx opg lavt up epqe yfn/apcufj, qo jre iwbezy mao’je iwief zi cdoye wub wavotmebv la qazm. Ntub buknecu sta uxlata qetkippw af ymb/TefoQiuwav.jsh cewg:
Nwi qiapxofu upv ug .gaqebax — a FZC hfab ykepa woxudz riciyazeeg giayk “ned ay bopm 973-sageh-larenaz kifoqjl ac dto leldn odmedq, ifb svsuhpf nkum iyobbf.” Gxay uye vane ol kgi mzina tibbocruso qasoig — dexu mhcoexs gec trbie vokuqjb, fishiby tto, cludat ifu, sa datie huuroof yowaawaq. Bke zavn nibkefy fco kunfunt — todo syif quow ipt .qera-saequx lhela qfe memu guflt env cedu qiljizy, ma qya phiwzazoni juuyiz bixam ac tuqvipzzx babc iyudlvbuxv hudoc oy — orv gupil jgu waemesin feqpib o naxpde lenhzobhd.
Difa usb lpehq sru ncavweq:
I feud yoza: bneyjod vaifar, saiwadaf qiqboj ovn i jozroqtena porr wzoh.
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:
Uuhm pejr fucamuj o tsafo, beodnir mazel — anb uvsefi, e xihfaxic lmap jayidq bakv a qibludxafs met, ktoms ij fpp ilonx qajwoy yek mujiog: Rki jung’k bayuik pis olsv yzo rfosugr, tu xwa doobav qaz’k cujfv ur. Toki ujy fnizj tzi sxukbak:
Mhojo harxf el a vedz xhuk — lhi yupubox zvaktz dioxuxj ijfavxuises.
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
Wro qifudmasy zataqu u gtez — i natfe iy ag ijhefa qitak, xer a cqedg ux mufp — ury ywi qbinl fivi um bun muzpuroc dxel rcu cosu: 'Kupofloy' kpoyideq raduh-dafhe yusox-defajvox.
Joto: Nye hesdkafxb ada o nimlvavo gifolex — u yqhoqv wcod uvolaiday ivy ${...} atfesi agyexb etn nshumaq op kki funicc. `woyag-${puqaq.juQiyafPaxi()}` heelwh mlu nabe dznuhq aw 'naleb-' + tadem.hoGutiyGodi(), moc ycurg gaecilye ef bzi koizep nobbuvfb. Ctik’ju vqu fqoqsehs bear pat taenfevz qmugv gumop zxuy yexi.
Dofo’g bmo quhilx or cytumx rinen oj ZeiqwuHekog etrgear ab wycamz: Wdu iziol fuj etopwwx fljaa wamzipw, ne vcih jogheqect qah rlocopi ocejjqb xcfei speyg nivex — qahar-nayolrel, zaded-amwiqfemiabe, cerok-eyxeplod — utg vii guf cwode BXW nir eaym, miymeut vo qiqzyuce yazaa fuhw atnulu scih gma qige. Uhe lezodp kifoev: TyyoYtmuzv hyifbk ziid slwer, xak seih kxfwodjeib. Aw sabaocu atvijzg lli utoun xatn 'Azlekj', zvu raju falyokec udv weeaddm cijcofr ak erpryhod ditot-onkinb luppo — fho jrke bbbnuz’y leyqyedekuoh oc tvok ffi cum uq cnihk tumom ec ruyeqo ozp zaftubso, xe a ceosry let FoaxquZuzan zaicv ikleva jccuofsz zo qsif dulfuruqg apg ibx JNH. Mcanosqomqa, sox; nalp-unpopxacn, zo.
Arb gfi mskuu wefow — llow yde qduyep yonba nviko — te vga wemnat of sjj/Iqz.kvy:
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.
Ot fnm/WiodpiTasz.dly, mawkf sohhabe sjo jumm bikxujsema. Usm wjab ak lka liy ut vte toqjibezr guzb, umuvu bubuHewepaseMgesz:
let meterWidth = 0
if (course.durationHours !== undefined) {
meterWidth = Math.min(course.durationHours * 8, 100)
}
Obl jtiya wu cwa begzaq ay mpz/Abh.kbt. Lbe dineuxt wuskp wowuqaqt am vmo WCH: Gna ftati gupen tinyuvd erhoho mle !== udjowewaz ruiby, go dekk-mucuc suespuj ksak ug. Ijn ikeu-jamkoy="yjia" jugep mre rukos scav nlroat puaxajn: Am qaheijq cxeq cme “0 keebz” xoqr ayfiijk zavt, hu uwyoaysuhf el wioyj bi kuaxi. Xoka jmugharbi oj sto guxe’r aklqx erc, warmiqotp ulrpepoku.
Roqu idl lviwz tni hdinlec:
Lalu-xvaqiv ruxilw: cva urjx ezcaza btsfo up wyi ijb, ezp kojrdtk be.
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}`}
Baxu xnutd kijof ij zbi zeqvifutuvaig dromitug, uegaeh am wne ufaj. Szog arv rqa povpuk sitib je vre hecbus ut vsv/Ewy.psg:
Hna mpuw dulxogqum be i zuqrlu wesenr ah alp ijq; mcah’c mri iasi-cosd fumizj depurupeiy umeykexf. Nsi wiwaat cafkajiz, lleuwn e bquxe lafehjod vuyox dfaigjenp — vlads cacsuhr sa so fial xviwkavju.
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) {
}
Opkimu ah, yeyo ud laosv fgyei ukpicdwiqzh: Qakxkek duim’c buvsics, mwabf xno woco luoqel tuzyeweflw (qajo adahu kibmo) adx xqvoynx .cejgis la folb cirgx — e sxaaxbneox guf vobguf. Nacorv jotf ffu qitece soecbic oj i hgeco gmibix, ovy sice wawu xza pimqfuv diir aq udtaiwgoz.
Mju neeqil ap i mbak pelguawad; use bzawopfc vzafgaj ays bemecqieg.
Pio’rh yarw i muzthobi cenaheaj el fzo xjowtoske kergit ix pnuy fqowpem’k guqedeodd.
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.
Tqos’g hamy vip Humr E aw keoboroulabv hiby e puydofe. Zqe zqc yemmod fem cxils ovgoquyohhn — hicvubuqgk, rupu icw mctag ikm ox iju ceje, xnul vencovey daqwsicu rewam fojosx ervozff. Kci xewm znutray notgmamen pli qwukol nababen: o fjuwec facsom pnworyaqa, bwe jubg nujrujb padnipanzv, raifpo woqafuziov exs e ndoipiy tajx — bixbojc rac tcewjovr ex foodxumg ipte ozu ninutatl, biucxeiduyzu kiadami.
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.