All projects
2025 PERSONAL Architect and sole maintainer

Bock Design System

Founding design system behind Pixel Brew client apps — 87 Angular components, accessibility as a platform contract, shipped over a Cloudflare Worker and R2.

View live
  • Angular
  • TypeScript
  • SCSS
  • Cloudflare Workers
  • Playwright

TL;DR

  • 87 Angular components and seventeen style layers, distributed to client sites over a Cloudflare Worker and R2 so a static site can consume the system without adopting a build pipeline.
  • Deleted nine of ten layout components for zero adoption. An Angular component renders as an element, and you cannot put one between a <ul> and its <li>s without breaking list semantics.
  • Consolidated 114 hand-written copies of one grid into a single utility. Only nine carried the overflow guard; the other 105 scrolled sideways on a phone.
  • Accessibility is a platform contract rather than a utility collection, with one intentional opt-in class that wins on specificity alone.
Components
87
Style layers
17
Grid copies
114
Deleted
9

Overview

Bock is the design system behind Pixel Brew’s client applications. It is 87 Angular components and seventeen style layers, distributed to client sites over a Cloudflare Worker backed by R2 with an origin allowlist.

It is also the system where I get to make the calls I would argue for in a review and then live with the consequences directly. The two decisions I am most confident about in this project are both deletions.

The Bock design system showcase, dark theme, on the Buttons page: a left sidebar of components grouped into Foundation, Atoms, and Molecules, a button variant matrix, and an on-this-page rail.
The showcase application, which is also the system's documentation. Each entry in the sidebar carries a status, so the distance between what ships and what is documented stays visible instead of drifting.

Goals

  • One place where behavior, accessibility, and state logic live, instead of each client project re-solving them.
  • Accessibility as a default of the platform rather than a utility somebody remembers to apply.
  • Distribution that does not require a client site to adopt a build pipeline.
  • A codebase where every non-obvious decision carries its reasoning, so future-me does not re-litigate a solved problem.

Approach

Deleting nine of ten layout components

I added ten layout wrapper components in October 2025, built over the patterns in web.dev’s one-line layouts. Nine of them had zero adoption when I deleted them. Only the sidebar layout survived, because the showcase shell is built on it.

The reason they failed is the part worth keeping. An Angular component renders as an element. Most of these layouts wrap a <ul> whose children are <li>s, and you cannot put <bock-ram-layout> between them without breaking list semantics. The abstraction was fighting the platform, and the platform wins.

The replacement is a class, not an element: .u-card-grid applies to the <ul> itself and the <li>s stay its direct children. Semantics intact.

It is deliberately not a drop-in swap, and the codebase says so. The deleted component used auto-fit; the utility uses auto-fill. Anyone migrating needs to know that.

The card grid, and the one min() that mattered

The card grid shape had accreted into 114 hand-written copies across the monorepo. Nine of them carried the overflow guard. The other 105 had a bug nobody had named.

minmax(280px, 1fr) gives a container narrower than 280px a track that is still 280px wide. The grid overflows its own parent and the page scrolls sideways on a phone. min(100%, 280px) clamps the track floor to the container, so a narrow container gets one full-width column instead. Everything else in that file is convenience. That single min() is the fix.

Two further decisions are recorded there because both cost me time to learn:

auto-fill rather than auto-fit, because they differ only when items are fewer than columns, which is exactly the filtered-down-to-one-result case. A lone card stretched across 1400px reads as a broken page, not a full one.

The max has to stay the indefinite 1fr. A definite max like minmax(min(100%, 360px), 480px) makes auto-fill compute the track count from the max, which is what left a signups list rendering two cards beside 391px of dead space.

Accessibility as a platform contract

The accessibility layer is explicit that it is not a utility collection. The rules apply across the cascade and are part of what consuming an application from this system means.

The generic default is :focus-visible { outline: 2px solid currentColor } with a 2px offset, so the ring follows the text color and adapts to whatever surface it is drawn on rather than assuming a light background. Elements that should carry the brand ring instead opt in through a single utility class, and that class wins on specificity alone — 0,1,1 beats 0,1,0 — so nothing in the system needs a priority flag to hold its place in the cascade.

One intentional utility, one documented reason, and a grep-able list of consumers.

The same Colors and Tokens page in the light theme, showing the primary color ramp as swatches labelled with their CSS custom property names.
The tokens page with data-theme flipped to light. No component changed; the token values did. It is also the reason the default focus ring is currentColor — a ring hard-coded for either one of these two surfaces is wrong on the other.

Distribution without imposing a build

Client sites should not have to adopt a bundler to use the system. Bock ships over a Cloudflare Worker backed by an R2 bucket, served from a custom CDN domain with an explicit origin allowlist and a token-protected publish path.

A static client site references what it needs. A full application consumes the Angular library directly. Same system, two costs of entry.

Outcomes

  • 87 components across layout, forms, display, shell and navigation, interactive, and utility categories, with seventeen style layers underneath.
  • 114 duplicate grid implementations consolidated to one utility, taking the 105 that lacked the overflow guard with them.
  • Nine components deleted for zero adoption, with the semantic reasoning recorded so the mistake is not repeated.
  • Zero-JavaScript layouts. The responsive behavior is CSS. There is no resize observer keeping a grid honest.
  • Dark mode and WCAG 2.1 AA handled at the system level rather than per component.

Reflection

The thing I would tell a younger version of myself is that a design system’s most valuable artifact is not the component. It is the recorded reason.

Both of the decisions above look like small CSS choices from the outside. auto-fill versus auto-fit is one word. But each cost real debugging time, and neither is recoverable from reading the final code. Without the comment explaining that a definite max makes auto-fill compute track count from the max, the next person changes it back and rediscovers the dead space by shipping it.

Writing components is the part of this work that scales with effort. Writing down why is the part that compounds.

What’s next

  • Publish the migration note for consumers still on the deleted layout components, covering the auto-fit to auto-fill behavior change explicitly.
  • Visual regression coverage on the card grid at the narrow-container boundary, since that is where the original bug lived and where a regression would be least visible.