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

ApproachStyling mechanismBehavior/accessibility mechanismAccessibility risk owned by the developer
1. Vanilla CSS + BEMHand-written CSSHand-written JS100%
2. Pure TailwindUtility classesSame hand-written JS100%
3. Headless + TailwindUtility classesProfessionally-built primitiveNear-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 elementChapter
BEM structure and vanilla CSScss_intermediate_11 / cssfw1-1
Tailwind utility classescssfw1-2, cssfw1-3
Hand-written keyboard/focus/ARIA logic, and why it's identical across Approaches 1-2cssfw1-6
Headless primitive replacing all hand-written behavior codecssfw1-6, cssfw1-7
The four-factor decision framework this comparison makes tangiblecssfw1-9
Honest scope note
This capstone doesn't cover every accessibility edge case a real production dropdown would need — full screen-reader testing across multiple real assistive technologies, per 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.
The throughline, closed
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

Exercise 1

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 solution
Exercise 2

Using 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 solution
Exercise 3

Using 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 solution

Chapter 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