Capstone — Building a Real UI Component With Three Approaches
CSS Frameworks
Chapter 10 · Capstone: Building a Real UI Component With Three Approaches
Nine chapters covered four paradigms in depth, closing with a real decision framework. This capstone makes that framework tangible — building the exact same component three genuinely different ways.
The Scenario
One real component: an accessible dropdown/select menu ("Choose a category"). Simple enough to build three ways concisely, but complex enough to actually exercise real accessibility concerns — keyboard navigation, ARIA state, focus management — exactly the kind of component cssfw1-1 named as the real test case for comparing paradigms.
Approach 1 — Vanilla CSS + BEM
Per css_intermediate_11's own BEM precedent — real HTML, real hand-written CSS, and real hand-written JavaScript for every piece of interactive behavior:
<div class="dropdown"> <button class="dropdown__trigger" aria-haspopup="listbox" aria-expanded="false" id="dropdown-trigger"> Choose a category </button> <ul class="dropdown__menu" role="listbox" aria-labelledby="dropdown-trigger" hidden> <li class="dropdown__item" role="option" tabindex="-1">Electronics</li> <li class="dropdown__item" role="option" tabindex="-1">Home Goods</li> <li class="dropdown__item" role="option" tabindex="-1">Books</li> </ul> </div> .dropdown { position: relative; display: inline-block; } .dropdown__trigger { padding: .5rem 1rem; border: 1px solid #ccc; border-radius: .375rem; background: #fff; } .dropdown__menu { position: absolute; top: 100%; left: 0; margin-top: .25rem; background: #fff; border: 1px solid #ccc; border-radius: .375rem; box-shadow: 0 2px 8px rgba(0,0,0,.1); list-style: none; padding: .25rem 0; min-width: 100%; } .dropdown__item { padding: .5rem 1rem; cursor: pointer; } .dropdown__item:hover, .dropdown__item:focus { background: #f3f4f6; } // Real, hand-written interactive behavior — keyboard nav, focus, ARIA state const trigger = document.getElementById('dropdown-trigger'); const menu = trigger.nextElementSibling; const items = menu.querySelectorAll('.dropdown__item'); let activeIndex = -1; trigger.addEventListener('click', toggleMenu); trigger.addEventListener('keydown', e => { if (e.key === 'ArrowDown' || e.key === 'Enter') { e.preventDefault(); openMenu(); items[0].focus(); activeIndex = 0; } }); document.addEventListener('click', e => { if (!trigger.contains(e.target) && !menu.contains(e.target)) closeMenu(); }); items.forEach((item, i) => { item.addEventListener('keydown', e => { if (e.key === 'ArrowDown') { e.preventDefault(); activeIndex = (i + 1) % items.length; items[activeIndex].focus(); } if (e.key === 'ArrowUp') { e.preventDefault(); activeIndex = (i - 1 + items.length) % items.length; items[activeIndex].focus(); } if (e.key === 'Escape') { closeMenu(); trigger.focus(); } if (e.key === 'Enter' || e.key === ' ') selectItem(item); }); item.addEventListener('click', () => selectItem(item)); }); function toggleMenu() { menu.hidden ? openMenu() : closeMenu(); } function openMenu() { menu.hidden = false; trigger.setAttribute('aria-expanded', 'true'); } function closeMenu() { menu.hidden = true; trigger.setAttribute('aria-expanded', 'false'); } function selectItem(item) { trigger.textContent = item.textContent; closeMenu(); trigger.focus(); }
This is cssfw1-6's own "you own 100% of the accessibility risk yourself" case, made concrete — a genuinely real amount of code, not a toy example.
Approach 2 — Pure Tailwind Utilities
The same structure, styled entirely with Tailwind utility classes (cssfw1-2's own material) instead of BEM and custom CSS:
<div class="relative inline-block"> <button class="px-4 py-2 border border-gray-300 rounded-md bg-white" aria-haspopup="listbox" aria-expanded="false" id="dropdown-trigger-tw"> Choose a category </button> <ul class="absolute top-full left-0 mt-1 bg-white border border-gray-300 rounded-md shadow-lg list-none py-1 min-w-full" role="listbox" aria-labelledby="dropdown-trigger-tw" hidden> <li class="px-4 py-2 cursor-pointer hover:bg-gray-100" role="option" tabindex="-1">Electronics</li> <li class="px-4 py-2 cursor-pointer hover:bg-gray-100" role="option" tabindex="-1">Home Goods</li> <li class="px-4 py-2 cursor-pointer hover:bg-gray-100" role="option" tabindex="-1">Books</li> </ul> </div> // The exact same hand-written JavaScript from Approach 1, unchanged
Critically, the interactive behavior JavaScript is identical to Approach 1 — pure Tailwind (cssfw1-2 through cssfw1-4) is only a styling solution. Switching from BEM to Tailwind changes how the component looks; it removes none of the accessibility-implementation burden at all.
Approach 3 — Headless Primitive + Tailwind
cssfw1-6's own headless primitive, styled with the exact same Tailwind utilities as Approach 2:
<DropdownMenu.Root> <DropdownMenu.Trigger className="px-4 py-2 border border-gray-300 rounded-md bg-white"> Choose a category </DropdownMenu.Trigger> <DropdownMenu.Content className="bg-white border border-gray-300 rounded-md shadow-lg py-1"> <DropdownMenu.Item className="px-4 py-2 cursor-pointer hover:bg-gray-100 outline-none">Electronics</DropdownMenu.Item> <DropdownMenu.Item className="px-4 py-2 cursor-pointer hover:bg-gray-100 outline-none">Home Goods</DropdownMenu.Item> <DropdownMenu.Item className="px-4 py-2 cursor-pointer hover:bg-gray-100 outline-none">Books</DropdownMenu.Item> </DropdownMenu.Content> </DropdownMenu.Root> // No hand-written interactive-behavior JavaScript at all
Every line of hand-written keyboard/focus/ARIA logic from Approaches 1 and 2 is gone entirely — replaced by a professionally-built, tested implementation. This is cssfw1-6's and cssfw1-7's own real payoff, demonstrated concretely rather than described abstractly.
Side-by-Side Comparison
| Approach | Styling mechanism | Behavior/accessibility mechanism | Accessibility risk owned by the developer |
|---|---|---|---|
| 1. Vanilla CSS + BEM | Hand-written CSS | Hand-written JS | 100% |
| 2. Pure Tailwind | Utility classes | Same hand-written JS | 100% |
| 3. Headless + Tailwind | Utility classes | Professionally-built primitive | Near-zero (usage/config only) |
Why This Comparison Matters
Approach 1 vs. Approach 2 is entirely about the styling mechanism — identical behavior code, different CSS approach. Approach 3 changes a genuinely different axis altogether — who owns the behavior and accessibility implementation, not just how something looks. This is exactly why cssfw1-1's own paradigm framing treated headless libraries as a structurally different kind of choice, not simply "styling approach #3."
Chapter Attribution
| Capstone element | Chapter |
|---|---|
| BEM structure and vanilla CSS | css_intermediate_11 / cssfw1-1 |
| Tailwind utility classes | cssfw1-2, cssfw1-3 |
| Hand-written keyboard/focus/ARIA logic, and why it's identical across Approaches 1-2 | cssfw1-6 |
| Headless primitive replacing all hand-written behavior code | cssfw1-6, cssfw1-7 |
| The four-factor decision framework this comparison makes tangible | cssfw1-9 |
web-accessibility1's own material on real device/AT testing, remains a real, separate verification step beyond this capstone's own scope. It also doesn't cover mobile-specific touch interaction patterns, and deliberately doesn't build a fourth Bootstrap-based comparison — cssfw1-5's own material already covered Bootstrap's trade-offs in depth, and adding a fourth full implementation here would dilute this capstone's own focused three-way comparison rather than strengthen it.
cssfw1-1 opened this course by naming three (then four) genuinely different paradigms for solving the same underlying styling and consistency problems. This capstone is the proof: one real component, three genuinely different, directly comparable implementations, closing the loop this course opened.
Hands-On Exercises
Explain why Approach 1 and Approach 2 use identical JavaScript, and explain specifically what that identical code demonstrates about what Tailwind actually does and doesn't provide.
📄 View solutionUsing this chapter's own side-by-side comparison table, explain why Approach 3 represents a genuinely different KIND of change from the Approach 1-to-2 transition, not simply "a third styling option."
📄 View solutionUsing this chapter's own scope note, explain why this capstone deliberately doesn't build a fourth Bootstrap-based comparison, and explain why this is presented as a deliberate scoping decision rather than an oversight.
📄 View solutionChapter 10 Quick Reference — Course Complete
- One real component (an accessible dropdown), built three genuinely different ways
- Approach 1 (BEM) vs. Approach 2 (Tailwind) — same behavior code, different styling mechanism only
- Approach 3 (headless + Tailwind) — a genuinely different axis: who owns the accessibility implementation, not just the styling
- Headless + Tailwind eliminates 100% of the hand-written interactive-behavior code in Approaches 1-2
- Honest scope note: no full AT testing, no mobile touch patterns, no fourth Bootstrap comparison — deliberate, not missing
- This closes the full 10-chapter CSS Frameworks course