A portfolio that argues for systems should be built like one.

I'd spent years telling people I work in systems and build what I design. The site making that case was five years of accumulated markup with the values typed in by hand. So I rebuilt it from the token layer up, in public, and left the commit history where anyone can read it.

Focus
Design tokens, component library
Built with
HTML, CSS, Style Dictionary
Source
Public on GitHub (opens in a new tab)
Wireframe of a case study page, annotated with the atomic layer and class name of each region: site-nav, case-hero, the twelve-column grid, project-row, and site-footer.
Where the rebuild ended up. Every region is a named component, and every label on this drawing is a real class in src/styles, filed at the layer it lives in.

The old site worked. It just didn't back up anything I was saying.

Every page rendered. Nothing was broken in a way a visitor would notice. Underneath, the presentation layer was five years of decisions nobody had written down: a red in a Sass variable here, the same red typed as a hex there, a style attribute on a heading that needed to be slightly bigger one afternoon in 2021.

None of that is visible from the front. It is extremely visible to someone who opens DevTools, which is exactly the person this site exists for. I was applying for roles where the work is the system underneath the interface, and handing over an interface with no system underneath it.

The work I wanted to be hired for was the work the site had never had done to it.

A portfolio is usually treated as a container for the work. For design systems and design engineering roles it's also a sample of it, because the thing being hired for is the layer the visitor never sees.

That reframed the whole project. The rebuild wasn't a visual refresh with a new grid, it was a working proof that I structure decisions the way I say I do.

Diagram comparing one colour copied into three files, each spelled differently, against the same colour defined once in a token file and referenced by name from three stylesheets.
The actual problem, drawn honestly. Three spellings of one red, and nothing in the code saying they were ever meant to match.

Step one was deciding where a value is allowed to live.

Before a single page was touched, every value moved into JSON in tokens/, split across three layers. Base holds raw hex and pixel values and is the only layer permitted to have them. Semantic gives those values roles, named element-property-role, so --text-color-link reads as what it styles, which property, in what job.

Component tokens come last, and only once a component has earned one. Style Dictionary compiles all three into dist/tokens.css, which every page links before the stylesheet. The compiled file is generated, so it is never edited by hand.

The three token layers, base to semantic to component, compiling into dist/tokens.css, with one red traced across all four and a crossed-out return path from component back to base.
Dependency runs one direction only. The crossed path is the move the architecture forbids.

A design system isn't a component library, it's an agreement about where decisions live.

The layering does one useful thing above all else: it turns a matter of taste into a rule you can check. A raw hex outside tokens/base/ is a bug, and anyone can grep for it. Nobody has to remember the convention or be senior enough to enforce it.

The alias chain also survives compilation. Open DevTools on any page here and --text-color-link resolves through --color-brand-primary-600 back to a hex, so the reasoning is readable from the browser without reading the source.

The layer is the constraint

If a rule needs a value the semantic layer doesn't have, that means a role is missing, not that a raw value is allowed just this once.

Dark mode stopped being a redesign

Because every colour is a role, a second set of base values generates the whole dark theme. No stylesheet was edited to add it.

Step two gave the stylesheet the same shape as the design.

The CSS follows Atomic Design, and main.css is nothing but imports in dependency order. Base, then atoms, then molecules, then organisms, then layout, then utilities. Every rule below the base layer consumes semantic tokens and nothing else.

That order is doing real work. The sheet is imported before the nav because the navigation panel is a sheet variant, and an override has to come after the thing it modifies. Getting the sequence right means the cascade never needs to be fought with a higher-specificity selector.

The layers of main.css listed in import order with their files: tokens, base, atoms, molecules, organisms, layout, utilities, with an arrow showing dependency running one way.
The whole stylesheet, in the order it loads. Nothing in a lower layer knows anything about a higher one.

Import order is dependency order, so the cascade never has to be argued with.

The practical payoff is that everything has one obvious address. A button problem is in atoms/button.css, and there is nowhere else it could be hiding. Deleting a component means deleting a file and an import line.

Specificity stays flat as a result. Almost every selector in this site is a single class, which is what makes the whole thing safe to change quickly.

Step three rebuilt the pages out of the parts, instead of the other way around.

With tokens and components in place, the pages were rebuilt as assemblies. A case study is a case hero, a series of sections on the twelve-column grid, and a project row at the end. The markup describes structure, and the stylesheet owns every measurement.

Spacing is a strict 8px scale, no 4s and no 12s. If a gap isn't divisible by eight it isn't in the system, which sounds arbitrary until you stop having the conversation about whether something should be 12 or 14.

A case study section drawn on twelve columns: a heading spanning the first eight, supporting paragraphs on a subgrid, a bold statement hung across columns five to twelve, and dashed empty spacer spans.
One section of this page, drawn on its own grid. The dashed blocks are empty spans that push a block into a later column.

A page should be assembly. When it isn't, every page becomes a fresh set of decisions.

The asymmetric measure was carried over from the old site deliberately, because it was the one thing about it worth keeping. Headings hang left, the argument hangs right, and CSS subgrid makes a column inside a block line up with the real grid instead of an even split of whatever width that block happens to have.

This page is the strictest test of that, and the reason a new stylesheet layer exists. Every case study before it carried its vertical rhythm as a style attribute on each block. Here the rhythm lives in organisms/case-study.css, so the page contains no inline styles at all.

The header is on every page, so it was the obvious component to lock first. My first pass made the markup identical everywhere but left the links alone, on the reasoning that links are content and content varies by page.

That reasoning was wrong, and it took someone saying so to see it. A global navigation that changes from page to page isn't global. The one variation I'd have defended was aria-current, which says which page you're on — and that was the thread worth pulling.

Consistency is a test, not a finish. What refuses to be identical is usually wrong.

Forcing the links to match immediately turned up two real defects. The homepage used bare fragment links that only worked when you were already on the homepage. And it pointed Resume straight at the PDF while eleven other pages pointed at resume.html — one question with two answers, and no single page could tell me which one was right.

Neither was findable by looking at any single page, because each page was internally consistent. They only surfaced when twelve pages had to agree on one answer. The PDF won, signposted so the new tab is announced rather than sprung, and the web resume kept its link from Contact.

Step five found two accessibility failures I'd been shipping for years.

Auditing the palette against WCAG 2.2 turned up two colours that had been on the live site the whole time. The brand red measures 4.36:1 on the page background, just under the 4.5:1 needed for body text. Muted text sat at 40% black, which measures 2.6:1 and isn't close.

Both were fixed at the value rather than the usage. Body links now take the 600 step of the ramp at 5.9:1, and muted text sits at 60% black. Neither original value was deleted, because both are still correct in the places WCAG allows them.

A contrast spec sheet: the 500 red at 4.36 to 1 failing at body size, the 600 red at 5.9 to 1 passing, 40 percent black at 2.6 to 1 failing, and 60 percent black at 5 to 1 passing.
The four measurements that changed the palette. The failing values survive in the roles where they still pass.

Fix it in the value and it can't quietly come back.

This is the argument for tokens that I find most convincing. A contrast fix applied page by page is a fix with a shelf life, because the next person to need a red will reach for the one that looks right. Encoded as --text-color-link, the accessible answer is also the convenient one.

Focus is a token pair for the same reason, a 2px near-black ring on :focus-visible defined once and applied globally. Every page opens with a skip link, and form errors are wired through aria-describedby and spell out the word Error, because colour on its own isn't an affordance.

Step six made the site show its own work.

There's a Design/Code toggle in the header of every page. Switching it to Code outlines each element that carries a data-xray attribute and labels it with its layer and component name, which is how the annotations on the drawings above were derived.

The labels aren't a diagram of the architecture, they're attached to the components themselves. If the structure ever drifts from what the labels claim, the labels start lying in public, which is a good reason to keep them true.

Claiming a system and showing one are different jobs.

Every portfolio in this field says the same words about systems thinking. Almost none of them let you look. The toggle costs one stylesheet and a few lines of JavaScript, and it replaces a claim with something checkable in about two seconds.

It's also genuinely useful to me. Turning it on is the fastest way to spot a region that has quietly stopped being a component.

Step seven was the least interesting and the most overdue.

The rebuild lived in a v5/ folder while the old pages stayed at the domain root. For a while that was sensible, and then it stopped being sensible, because an outside audit pointed out that none of the new work had ever been deployed. Real visitors were still getting the old build.

So v5/ was promoted to the root, the legacy pages were archived out of the working tree, and a deploy config now copies only the paths that should be served. The icon sprite is generated from the official library by a script that fails loudly on a typo rather than shipping a blank square.

Work that hasn't shipped is work nobody has seen, however good the repo looks.

I'm including this because it's the most common way a rebuild like this fails, and because leaving it out would make the story tidier than it was. The system was sound for a week before anyone outside the repo could see any of it.

The fix was boring and it was the whole difference between a nice local project and a live site.

What the rebuild actually bought.

Sixty-five commits over nine days, and the site is now the same kind of artifact as the work it describes. The visible design changed less than the amount of effort suggests, which is the correct outcome for a structural rebuild.

What changed is the cost of the next change.

One place to change a decision

A colour, a spacing step, or a type size is edited in one JSON file and rebuilt. Nothing has to be found first.

Pages got cheap

This page is new markup and one small stylesheet. Every other part of it already existed and was reused as-is.

Accessibility is structural

Contrast, focus, and skip links are defaults in the token and base layers, so passing is what happens when nobody does anything special.

The repo is part of the portfolio

The source is public, the commit messages explain the reasoning, and the migration from the old build is readable in the history.

What this sets up, and what it doesn't.

The point of building it this way is that the next few things become small. Some of them are already obvious, and a couple of them are still open problems I'd rather name than hide.

These are in the order I expect to get to them, not in order of how impressive they sound.

React without a rewrite

Every page already carries the same DOM shape for a given component, with content as the only variable. Converting the locked components into real React components is a translation job, not a redesign.

Deployment is still the weak link

Publishing is a manual step, which is precisely how the built work went unseen for a week. Until it runs on merge, the same failure can happen again.

The rules should be tested, not remembered

Contrast ratios and the no-raw-values rule are both checkable by a script. Right now they're checked by me noticing, which is the least reliable option available.

Dark mode is generated, not designed

A full set of dark tokens compiles today, but nobody has sat with the result and made judgements about it. Generated is not the same as considered.

The older pages still carry inline styles

The five case studies written before this one hold their spacing in style attributes. The stylesheet that replaces them now exists, so that cleanup is a delete.

The real test is month six

Any system is easy to keep clean in the week you build it. Whether this one still has one source of truth after a year of small edits is the only result that counts.

The site isn't really the argument. The commit history is.

A finished page only shows that something was made. The sequence of decisions behind it, why a value moved layers, why a component was locked, why an accessible colour replaced a prettier one, is the part of this work that transfers to a team. That's why the source is public and why this page describes the reasoning instead of the result.

Next project

Next · Terafina

The product worked. People just didn't trust it enough to finish.

View project