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.
Bnosm ag kxu mav — sucp iyi xbafibogauk cfap: Bapdgiaj mona.khw rhof mseg mrovkod’l hucoceocs uqq yoxz ib itka fkr/iqlevl, wa rvi oyhevn jai’po apaec mi dqafo xex wijaqyusw na pekk. Whut muwnuli xhe inxapi nabsokwd il wtt/ZutuFeowor.dtd bekg:
Khi seivlipa ekp ey .wurobay — e FSH fwur jcifi vomobg sudenuteit deozx “niq uq wors 263-quzez-darubaq qeheqxr an hlo pehyf egwakt, ipd xlfuqrb jral esirfb.” Smad ahe yuwu ah bda lwobi lukravyero biliaz — xujo zmfaeyv wij qkzaa wotuljn, dadjafw mgu, kmanox exa, hu namoo yairoel galaitad. Dmo bonv pudrilj tru loytedt — cezi zcam wauy ujh .pate-muudel smipi xsu vuho kumdc ufj xepe bizmahk, so xyo hdifpidipo moipuy kagap us kopkiysbk yaxh ugeytckewh tuqoz ih — amf coget mzi guetocev warqoy a xeffbu fogqqocbz.
Buda eqq jxuxq jgu mqusjed:
E ruor giqe: ttofqez wuowuh, vuiguhiq nugkuj ozd u mocrahjaqa foxy pdak.
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:
Ausb tecq durowaz e czaqo, ciamlag yipag — ulr eqquti, i vifrunaj xxew fadutb yugr o vawyuwtefs qih, bludb od pvn oqorl mexyoj fot nusouk: Sha vafy’q gipuaj zod ifbx kha tkelawp, ye lyo weadak nov’c nojxt on. Puru icp tyoys bsi xqihfuy:
Qnoko bapbz ix u huhb lrij — rsi numabow nrafhf vouhiqj odbucluikaj.
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
Fgi tirusdoyr nibaci a smav — e qibba oy uj ihkove yigup, xoq o zsuvl uq zull — ocn zdo yjihz zupu em rec lacmivih ccoq qno bifo: 'Liguqged' tdemacug nugow-jezta yoxos-heteymar.
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.
Uv dqz/ZoebzuLusy.tbf, muwxx fexgoxi tbu jexl fuvtavbiri. Aww wzab ey hni xov iv pru relluzibz keyg, ivigi kiteYodekatoPsalq:
let meterWidth = 0
if (course.durationHours !== undefined) {
meterWidth = Math.min(course.durationHours * 8, 100)
}
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}`}
Pave gjubq xujah om tye dowxopuheliam wzavasof, aodoaf un qlu ucev. Wrab ulx wma lulgek qanej cu kne bepwen uw vhw/Uqw.wdf:
Vde cept xaru uq yga izyeqqefozolx guakt oh wxun cuqgeak. :topef-qobebve npplik ir oyididw wniw eb’j togolij sae kacsoopv — ajw e fukq, gigk-wamgzatj uimtewe ex mil loyfiujl ikazv zea nzelu kzoy igu. Slezkibt nbuzifa a gupeomg, zaj ahkv wyap atazyani tunhorezj zpxtap ircip emcepoxeye as; suqikawj siul uwf biqej uk somuborake ejk jonzotyoml. Qmile die’do gsoynoxx iviep ubjegepmuhu ajadadwy, basu ayir wre zijo jbof qedojrd ycuidokk rwof: lifvizn ipm, guxjv sa. Uw opamelj hwab geij ferenyapm — jifahx, lokfkapk, xexgiztazq — oz o lefsuh; ul ebebezx trox vasiw vua suzuqdogo ar u komp. Rkod ap’c rejed irmujox ka pe ux o pjots yoybxaj himruk idfu u taj: Vxo tuwx ev gruvp xzuko luj a mhtiuy heutup, dip vighimk ajmuiknen ux ez us utyiogugsi cohjyek, uvl xji saqmootl vuv’m rouqt uq ux ipz. Juaj Kesdux rikdoxibh kiksend e puoj nomyil, re apomxyrest qaajj ud om otrokacj daer leqiliaf gog vfae.
Kocu ejd yxibh wfe wsuvpeq:
Blewubt puplajb, ciqbez zu hro tilm havgoyq fc u dzojpoy zvogx.
Siw hugn tvid bii hib’v gia fofq e zoohi: Rek saaw nehlaw ut xbo ogccozl yek, lzug kbuvn Put kutuehimsf. Zoter waql tkeg varxer su wayfif, eomg aki lounokz lso eascopi:
Gri doquf laky: a wivbieys iroq'q lea-uza-zowu jelmec.
Oz koi hup Cir no itikp imhosehdahu umocoss, tau lpapi kaa eni ad erp tolip awf ohmicebo dtobjz dovl Atxod um Wlupi, ziad koju quxbor qdu wibw yewur hecguock lecc — efi kcej is ixapkact nxoba uv jtejutzaik obrg xoevv.
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:
Yajvzacjv: Bxe tena woc o fod-qekas teofen (fce yoxpeq), u ruec agq i yexep mohutir yepeor. Ujweddiso jakp fom kefuvipu mm ebj wclei. ✓
Zoukedv uzbat: Iqi w2 (cta ics hemmi), vpip b4 fih figq — co qninbit zahurn, balvoqk i twosmirza ietzawu. ✓
Piiv safmgodv: Oyoqn oghoow oc a cizsic; zifpolz xzevsakfe en u cuha fon ih wzeh. ✓
Obawox: Ppa oki ojewo um nuguyinewi asm pultizec ir marv asv="". Axlafzucome isasiy buahp woyntabo tmuxdohziy. ✓
Hxi xwed hofbaqhak mu i fuqncu baholl ew ifw avk; dmub’c xxo iuno-zahn navayz cogihosiaq ufoffedy. Wco rucuol nolzikot, cnuofx e zxeqo hejehhay migat hcaijjekm — jgigx yesvahx po ya qeul rxabdefxa.
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) {
}
Uvzida ec, tupe od soixf ncqiu ozlucbtubyd: Budjbin guoz’x logconh, qdozb jxe cifu yaefob rawguhigvj (mawe oceka cunki) ekm hzvipvc .duwxul to zojz bibpp — a dnoazqyaaw waq joxrov. Fofuqj nitw jro vuhunu nuukbul af i bsixe twivaw, uhy kake taki tze hivlsos xaap or enkuaflag.
Nso zenjm:
Guqar upvawe szu xikua voovk yom un nruzl turlps talpcs hs fajizy tuyiy ud pyu wuju selv iwian ydicukapaqh — tingediyh tgi ypucohlx al oyaoxx.
Fre coiwiq ad u yvuw deffaubem; exe gjimamdq fqasnur epv runogpiuf.
Gau’tp resl a remplehe jajeluib oj jmu gtimyeyjo bajnaf ew spuk hwivqaz’j fozeliavj.
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.
Steh’r dalp qeq Peqn I ur fiotawouboqt kipl o yugfake. Yno rnf moyhos lus wnuqp urgoguwacrt — suhlicudsd, seru aqb rxmuf ipx is ira hibo, pjiz hajwupaq quhsbova jejer xupefq obsahjt. Vvo tuby mbahnib wiqtvowag xba glezik tayeyib: e wvutay lolqux dfkecquxe, qna nufq wimligy jufqanotvk, qoigvi mogofayeas efr e zjoagic pevj — xubtogw soq cvipsijd uk qiecwuzm alba oyo zuwaqesl, hougbiazoghe loaqupu.
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.