Linktree · 2025–26

Arbor

Company: Linktree Industry: Creator platform Role: Senior Software Engineer, Design Systems Duration: June 2025 – September 2026 Tools Used: React, TypeScript, Tailwind CSS, Figma, design tokens, codemods, MCP

One system across product teams

Arbor is Linktree’s design system. I joined as a senior software engineer to lead its build, and spent the next year consolidating Linktree’s micro-frontends onto one version and one token set. At first, the work was less about creating components and more about defining the naming conventions for the tokens they’d be made from.

The work crossed design and engineering. I researched component behavior, worked with designers on the conventions, and built the delivery and migration tools that carried those decisions into production. A Figma plugin, a CI check, and a pull-request reviewer answered different needs. Choosing where the help belonged was part of the design.

One name, three answers

Primary button meant three different things, and each one was defensible.

Arbor had shipped alongside the legacy component library rather than replacing it. Both rendered on the same surfaces, at the same time, and each carried its own tokens. The work was to give them a shared foundation while those surfaces kept shipping.

So the same name resolved differently depending on where you were standing. A primary button was a black field with white text in one place, a purple field with white text in another, and in at least one feature a secondary-level variant promoted by hand because that was the one that read correctly against everything around it.

Each of those made sense at the moment someone chose it. Nothing broke, so nothing flagged it. A button could render correctly in its own context while leaving a different answer for the next team to interpret.

One name, three answers

A single component name on the left fans out to three different renderings on the right: a black field with white text from the legacy library, a purple field with white text from the new system, and a secondary-level variant used as the primary. Nothing arbitrates between them.

Black field, white textthe legacy libraryPurple field, white textthe new systemA secondary-level variantused as the primaryPrimary buttonresolves three ways

Two libraries rendered on the same pages, each with its own tokens, so “primary” had three answers and no rule for picking one.

Fixing that wasn’t a matter of picking one of the three. The design-systems team needed the standing to change the shape of the front-end codebase, which meant untangling module federation and making sure CSS loaded only once and in the right order. The naming decisions needed a dependable way to reach the page.

Separate the value from its use

It wasn’t a color problem.

Underneath the divergence sat a token hierarchy with no levels in it. No scoping. No naming conventions describing where in a component’s structure a token applied. Everything flat, on one level, sharing relationships.

You can’t have a primitive token that also acts as a component token.

A value can’t be both the raw material and the application of the raw material. The moment it’s both, every consumer of the primitive inherits a decision that belonged to one component. Changing it for that component could affect other consumers that had no reason to inherit the same decision.

One value on two levels

Two levels of a token hierarchy. On the left, a raw value sits on the primitive level and a separate token applies it on the component level, each doing one job. On the right, a single value spans both levels at once, acting as the raw material and as its own application.

Primitive levelComponent levelA raw valueone jobApplied to a partone jobOne value, two jobsraw material and its application

Left: each token level does one job. Right: one value does both, so changing it for one component changes it everywhere the primitive is used.

The team had run into this friction before I arrived and had reached a decision about it: never add another layer of component tokens. As I understand it, the thinking was that to the designers who owned token creation in Figma, a second layer seemed more complicated, and keeping everything on one level was simpler.

That was a real maintenance concern. Adding a layer means more relationships to name and understand. But keeping one level had pushed those relationships into the places using the tokens, where they were harder to see and change together.

The question was where to carry the complexity. I wanted the structure to name a component’s decision explicitly, so another team could use the primitive without unknowingly adopting that decision too.

The work between teams

The design-systems function didn’t exist when I arrived. For the first few months it was me.

A staff product designer joined afterward and we paired on building and delivering the system. He spearheaded the Figma component library and owned that half of the equation: he sat on the design team and worked daily with the designers shipping features who needed opinions about which components to use where. I owned the code.

Two of us, and more than ten micro-frontends.

We kept it working with a few standing meetings: weekly office hours with the design team, a standing 1:1 with the staff designer, and a weekly meeting with the two product design teams driving the visual language. We brought design opinions into the engineering work and engineering constraints into the design work. Those conversations helped us see connections between teams and surface problems while they were still cheap.

A lot of what I shipped was guided by people describing how they wanted to work or something that felt broken to them. Their feedback shaped the details of the tools as well as the direction. My product-design experience helped me find the issue underneath the request; my engineering work let me carry the response into the workflow.

Make the system readable

One Tailwind configuration. One component library. One token set.

With two token sets overlapping, a rendering problem could begin in either set or in the way they were loaded together. Before improving individual components, I needed to make those relationships traceable. Consolidating the Tailwind configuration, library, and tokens gave us a dependable place to investigate and correct a shared decision.

Unifying how surfaces mounted and loaded wasn’t normalization for its own sake. It was making one library’s CSS arrive once, in a known order, so that a token could have a single answer.

Two naming schemes, then one

Before: two token sets, one from the legacy library and one from the new system, both define the same token name, and nothing arbitrates between them — so four of the more than ten micro-frontends receive two answers and no rule for choosing. After: a single token set defines the name once, and the same four surfaces receive one answer.

BeforeAfterLegacy tokensArbor tokensOne token setOne token nameone answerfour of 10+ micro-frontendsthe same four surfacesOne token nametwo answers, and no rule to pick

Before: two token sets define the same name and nothing decides which wins. After: one set, one answer.

Make migration part of the change

Audit what’s actually consumed. Remove what isn’t. Write the codemod. Then change.

@linktr.ee/arbor went from version 9 to version 27, eighteen major releases, while every micro-frontend stayed on one version and one token set throughout. Automated migration helped us carry the library’s changes into the product instead of leaving each team to work out its own upgrade.

We shipped the codemods as their own package, for upgrading Arbor across major versions and migrating off the legacy component library. Most of those majors removed features that were unused or used sparingly. I audited the consumers and prepared the changes before removing the syntax. Even when a removal affected no known consumer, I labeled it major so the release would signal that the contract had changed.

The codebase kept moving. Product teams could add uses of an old convention after a codemod had already run, so the migration tools needed to be useful again. We could rerun them against the new work. Migration was part of maintaining the system while people used it.

Give reviewers the reasons

Intention encoded in the commit. CI enforcing the format on push. A release script deriving each package’s version from those commits. The version flagging the pull request for human review. And the written record letting the reviewer approve confidently.

The version number flagged a change that deserved attention. The written context explained what was removed, where it was consumed, and how the upgrade had been prepared. That helped reviewers assess the actual risk. A major version couldn’t tell that story on its own.

This added a review cost. I thought that was worth paying while we changed shared foundations: the context made approval easier, and the review gave teams a place to question the change. A codebase that was updated too quickly needs slow thinking.

The change I pulled back

I shipped the rename before I’d shipped a way to migrate to it, and had to pull it back.

The clearest instance of the flat hierarchy was in type. The brand typeface ships as two cuts in one family — Display, which is more extended, and Product, whose width reads as body copy — and which cut a token resolved to was a value someone had to know rather than a name they could select. That’s a primitive carrying a component’s decision, in the one corner of the system where I could fix it by renaming.

I renamed the family to Product, promoted Display to a peer primitive, and exposed both as named utilities from a single variable font file, so choosing correctly cost nothing. The semantics were right. What I hadn’t shipped was a migration path. Consumers had to carry the change themselves.

So I reverted it at the next major boundary, un-shipping my own work to protect the surfaces the project existed to fix. Then I built the codemod, hardened it against the cases that actually occur (!important prefixes, class-merging groups, headings that had lost their weight), and taught it to flag adjacent font-family utilities, so a hand-written declaration sitting beside a migrated one couldn’t pass silently. The rename re-landed inside the typography core three weeks later and held.

You can’t rename a token. You can only migrate one.

Put the fix beside the work

Documentation didn’t always change the patterns developers were used to writing. I worked on three ways to bring the guidance closer to their work:

Documentation asks them to remember it exists, go and find it, and apply it. A lint rule in CI and locally flags the problem at the moment of writing, and asks them to work out the fix. A reviewer that annotates pull requests can suggest the code to write and explain the correction where the engineer is already reviewing it.

That last one came out of auditing Arbor against the best systems in the field and noticing the gap in our own workflow. The research didn’t only answer component questions: it produced the roadmap for the system’s own tooling.

The goal is to shrink the distance between knowing and doing until the right thing costs less than the wrong thing.

The escalation ladder

A plot, not a list. Two axes: work asked of the developer, rising toward the top, and closeness to the moment of decision, rising toward the right. Documentation sits furthest from the decision and asks the most — remember it exists, find it, apply it. A lint rule sits nearer and asks less. A reviewer that writes the change sits nearest and asks only that it be accepted. The three fall on a line: the closer the intervention, the less it asks.

Documentationremember · find · applyLint, in CI and terminalnotice · work out the fixmore work asked of the developer, at the topcloser to the decision, to the rightA reviewer writes itaccept it

The closer a tool sits to the moment someone’s deciding, the less it asks of them. Docs still matter; they’re just the furthest away.

Check the guidance too

Making guidance available to agents gave us two things to examine. Did the guidance change what a model produced? And were the claims in that guidance true?

We built evals for the first question and a CI fixture for the second. A model following an instruction doesn’t establish that the instruction is correct. We needed to check the guidance against the system it described as well as measure its effect on the output.

The checks caught a few claims that needed correcting, and we fixed them. We added provenance to the guidance artifacts so a consumer could see where a claim came from, and status flags to every component so a consumer could ask may I recommend this? rather than guess.

That extended the same responsibility from the code into its guidance. A shared convention needed a way to check its implementation; a recommendation needed evidence behind its claims. The last part of Building Arbor develops that distinction.

The accessibility work followed the same shape. An audit found gaps in several hundred places. We counted them, fixed them, and added a gate so the number couldn’t grow, plus warnings that catch the most common cause while someone’s writing the code.

What the team could carry forward

At handoff, token changes could travel from code to Figma and from Figma into a code pull request. Shared branch naming let the pipeline map a code branch to its Figma counterpart. The sync ran in a GitHub Action with linting and a log of what changed and why. It removed the manual copying between those two representations.

The documentation and agent tools drew on the same underlying information. The site, MCP server, and CLI gave people and models different ways to reach it, reducing the separate copies the team had to maintain.

I left Linktree with a Design Systems team that hadn’t existed when I arrived, and a system for carrying shared decisions into the tools where people worked. The handoff included the means to keep changing it.

Write something once and then point at it.

Five deciders, one body of knowledge

One source line, written once, branching to five points where a decision gets made: a designer on the Figma canvas, an engineer in a pull request, anyone reading the docs, an agent writing UI, and a reviewer approving a release. The five are equal consumers of the same truth, and one of them isn’t a person.

DesignerFigma canvasEngineerpull requestAnyonethe docsAn agentwriting UIA reviewera releaseThe system’s knowledge — written once

Shared guidance reaches five places decisions get made. People and agents need different interfaces to it, and both need the reasons behind a recommendation.

What I’d start with today

I’d begin earlier with the feature teams, finding out which parts of their surfaces were still in use, which were dead, and which nobody could confidently explain. The technical audit matters, but institutional knowledge is uneven. Someone often knows a reason the code alone can’t tell you.

That is the part of the work I keep returning to: researching the problem, designing a response with the people affected, and implementing it where it can help. A Figma plugin, a migration tool, or an MCP server can each be the right answer. I want to understand the need well enough to choose, then build the solution through to production.