Roster
The component library this site is built out of, with exactly one real customer: me. Four apps, one substrate, published to npm and versioned like the dependency it is, which turns out to be a different engineering problem than shipping one to strangers.
- 40
- Components live · package exports
- 310
- Commits since Feb 2026
- 778
- Tests roster @ 4.2.0
- 5
- Apps consuming it including this one
Four apps, four sets of the same button
Game Verdict, Retrospect, MegaSquad, and BB’s Grove are unrelated products with unrelated palettes. What they share is the substrate: a button that handles its own loading state, an input that knows how to be wrong, a dialog that traps focus properly. Rebuilding that four times is how you end up with four subtly different definitions of “disabled.”
I had just finished building a shared component library at work when it became obvious I could do the same for myself in a fraction of the time. Roster was never a bid for stars. It was a refusal to rebuild the same button on a Saturday.
It ships pre-compiled CSS, so a consuming app does not need Tailwind installed to use it, and it is versioned like the dependency it is.
Theme-aware without being theme-locked
A component library that hardcodes its palette is a library you can use exactly once. Roster’s classes compile down to var(--roster-*), so a consuming app repaints every component at once by overriding custom properties. No forks, no wrapper components, no !important.
That claim is easy to make and annoying to prove, so here it is running. Both columns below render the same component from the same installed package. The left one is pinned to the tokens npm actually ships, read out of dist/tokens.css at build time. The right one inherits this page’s. Flip the production state at the top and watch which one moves.
The shipped palette is applied as inline styles on a wrapper. Importing tokens.css a second time would have put a competing :root block in the cascade and repainted the whole site.
Dark mode is class-based rather than tied to the OS preference, which is what makes a manual toggle possible at all, and is why this page can have two production states instead of one.
Two majors in one day, one of them broken
Roster is on its fourth major. Three of those went out roughly as planned. One did not: 3.0.0 shipped with DataTable exported from the main entry while statically importing @tanstack/react-table, an optional peer. Every consumer that had not installed TanStack, which was most of them, would fail to build on import.
4.0.0 went out the same day with DataTable moved to its own entry point, and 3.0.0 was deprecated with a note explaining exactly what was wrong with it. That note is still on npm. It seemed more useful there than quietly unpublished.
- 1.0.02026-03-11
- 2.0.02026-04-11
- 3.0.02026-08-13deprecated
Broken: DataTable was exported from the main entry with a static @tanstack/react-table import, so any import fails without that optional peer installed. Fixed in 4.0.0, which moves DataTable to @blakesteve/roster/data-table.
- 4.0.02026-08-13
An optional peer dependency that is statically imported is not optional. It is a required dependency with a misleading label.
The library changes when an app hits a wall
Roster does not get planned so much as discovered. Something gets built twice in a consuming app, that is the signal, and it moves into the library. This site alone produced most of a release:
A CSS class used 26 times became a component. The small tracked-out uppercase label carrying the folio, the prop rows, and the token strip was a hand-rolled .u class before anyone asked what it was. A utility class used that often is a component nobody has named yet. It is Eyebrow now.
A bordered call to action written twice became RCta. Once on the contact block, once on a case study sidebar, identical markup both times.
And one gap is still open. Roster’s Button is typed to HTMLButtonElement with no as and no href, so the most common control on a portfolio, a button-shaped thing that navigates, cannot be built from it. Every workaround is bad: a button with an onClick router push loses middle-click and the right role, and hand-rolling it is how the two above got written twice. It is logged, not fixed.
One customer, no cover
A library with one customer is not a smaller version of a library with many. It is a different job. There is no roadmap, because there is no one to ask for anything. Nothing gets built speculatively, because speculation has no payer. Every feature in Roster exists because something I was building stopped and waited for it.
The flip side is that mistakes arrive immediately and personally. A bad major does not generate issues, it breaks four apps I was going to work on that weekend. That is a tighter feedback loop than most libraries get, and it is the only reason a packaging bug that had been quietly wrong for months finally got found.
Nobody would have noticed if I had quietly unpublished the broken major. That is exactly why the tombstone is still there.
Two things this page deliberately does not do: it does not list the components, because /system reads them live from the installed package along with every token ramp and a set of working specimens. And it does not walk through the CSS packaging failure in detail, because that one earned its own post.
What it does instead: press ⌥X and every Roster component on this page outlines itself and says its own name, including the buttons two sections up and the breadcrumb you arrived through. The claim and the evidence are the same object.