An accessibility-first design system
Thirty-plus components published as an npm library and adopted as mandatory across an engineering organization of several teams.
A component library is not hard because components are hard. It is hard because every team wants a slightly different one, and every exception you grant is a bug you will ship in someone else's product.
- Vue 2.7
- TypeScript
- SCSS
- Storybook
- design tokens
- WAI-ARIA
- axe
Thirty-plus components, every engineering team, and one rule: if the library has a component for it, you use the library’s. I was a principal contributor to that system — an accessibility-first component library published as an npm package and adopted as mandatory across the engineering organization at Traackr.
I built a large share of the components and their Storybook coverage, and contributed to the design-token library underneath them, the theming and the release pipeline.
What follows is about technique, not about a former employer’s product. The components are the interesting part, and they are portable.
The polymorphic button
The most-used component in any system is also the one people get wrong most often, because it has to be three different elements.
A control that performs an action is a button. A control that navigates is an
a with an href. A control that navigates within a single-page application
is a router link, which is still an anchor underneath. All three look identical.
None of them should behave identically.
<Button> → <button type="button">
<Button href="…"> → <a href="…">
<Button to="…"> → <router-link to="…"> (renders an anchor)
The trap is the fix people reach for first: styling an anchor to look like a
button and adding role="button" to make the semantics “match”. That is
backwards. It tells a screen reader the control performs an action when it
navigates, and it breaks the behaviors a link is supposed to have. Ctrl-click
stops opening a new tab. The link no longer appears in the browser’s list of
links. Enter and Space swap meanings, because a button activates on both
and a link activates only on Enter.
Render the element that is already correct and let the platform supply the
semantics. The component’s job is to pick the element from its props, not to
paper over the wrong one. So the API accepts href or to or neither, and the
decision is one branch at the top.
That single rule removed an entire class of defect from the codebase, and it is why I think a design system’s real product is constraint rather than components. The button in this site’s own navigation works the same way.
The checkbox that has three states
A select-all checkbox has three states, not two: none, some, all. HTML has a
representation for the third — the indeterminate property — and it is not an
attribute. It cannot be written in markup, only set on the element. So every
implementation that tries to express it declaratively ends up simulating it,
usually with aria-checked="mixed" applied by hand to a native checkbox, which
then contradicts the property the browser is already mapping.
The correct implementation writes the property after every render that could
have changed it, and adds no ARIA at all. The browser exposes mixed on its
own.
This component is one of the bulk-selection pieces I built, with the floating action toolbar, the GraphQL batch mutations underneath, and bidirectional cursor pagination written by hand. Together they let a customer select and act on thousands of records that the browser had never loaded.
The breadcrumb that measures itself
A breadcrumb has to fit a space it does not know the width of, containing labels it does not control the length of. The usual solutions are to truncate every label to the same width, which makes them all equally unreadable, or to collapse to a fixed number of items, which is wrong at both ends of the range.
I built one that measures. It renders, asks the DOM how much room it actually has, and drops items from the middle into an overflow control until the trail fits — keeping the first and the last, which are the two a person needs. It re-measures on resize, and it does its measuring after the browser has laid out, not after the framework has updated its own state. Those are not the same moment, and assuming they are is a bug that only appears on first paint.
What the token library actually governs
Color and typography, expressed as named decisions rather than values. The useful property is not that the values live in one file — it is that a component cannot express a color the system has not sanctioned, because there is no literal to reach for.
This site is built the same way. Its palette is checked against WCAG minimums by a script that reads the token file rather than restating it, so the check cannot drift from what ships. It found five failures in the light theme the first time it ran. The write-up is on the accessibility page.
The part that was not code
For a stretch I was the UX team’s engineering ambassador: helping engineers on other teams turn designs into components, filling gaps in the library when they found them, and resolving usage the documentation had not anticipated. I hosted internal talks on building accessible components, on CSS layout, and on how DOM events actually work.
That work taught me the thing I would tell anyone starting a design system: the library is adopted at the rate people can get their questions answered, not at the rate you can publish components.
