Skip to main content
All writing

Staff-level UI engineering

A guide to shared UI platform work: component APIs, accessibility, package releases, governance, and the teams using the system.

COMPONENT LIBRARY → UI PLATFORM Components Public API Releases GOVERNANCE AND ADOPTION A shared UI platform

What this covers.

  • How technical scope grows from a component to a shared UI platform.
  • The decisions behind APIs, accessibility, testing, and releases.
  • How governance and adoption connect the system to its users.

A shared component carries decisions into every product that uses it. Staff and Principal UI engineering expands that responsibility to the platform: its APIs, accessibility, releases, and the teams depending on them.

This guide examines Staff-level UI engineering for design systems and shared UI platforms. It connects browser technology and component architecture to package distribution, developer experience, and adoption. Use it to understand the scope of the work, review a role, or identify where your own practice needs to grow.

Anything here that’s a matter of record is footnoted to a primary source. Anything that’s judgment is mine, stated plainly. One caveat: Senior, Staff, and Principal aren’t standardized titles. Monzo says so directly, and GitLab’s frameworks differ from everyone else’s.123 Read every level description below as one company’s answer.

What the job is

Published career frameworks show what actually changes at each level. GitLab separates Senior, Staff, and Principal individual-contributor levels, with broader technical and organizational scope at each step.423

Its Staff framework describes Staff engineers as technical leaders for a domain: owning tradeoffs and architecture, removing blockers, decomposing complex requirements, and blending technical, product, and design concerns for longer-term impact.2 The Staff Frontend framework adds delivering despite unclear requirements, leading large architectural changes, improving standards and tooling, mentoring, and influencing frontend quality beyond your own tasks.5 Principal scope is broader still: organizational technical leadership, cross-team architecture, tradeoffs that affect the business, unblocking multiple teams at once.36

Monzo’s public write-up is a useful counterweight: one company being specific rather than a framework being general. It describes Staff engineers as individual contributors with no direct reports, working on highly ambiguous problems across squads, mentoring, and creating multiplicative impact. It also admits what most ladders leave out: Staff engineers often write less code, because the scope widened underneath them.1

Across these frameworks, each level changes who your work is for.

LevelTrusted toWhere the leverage sits
SeniorSolve hard problemsThe problem in front of you
StaffImprove how a group of engineers solves hard problemsA domain, and the teams inside it
PrincipalImprove how an organization solves classes of hard problemsArchitecture, standards, and strategy across teams

It’s a simplification, but it holds up well in interviews and career planning.

How scope changes at Staff level

Take a concrete case. A button component has an accessibility defect.

The senior response

Diagnose the defect. Fix the component. Add a test. Document the behavior. Ship the release.

This is correct, necessary work, and none of what follows replaces it. Someone still has to fix the button.

The staff response

All of that, plus a wider set of questions:

  • Why did our process let this ship?
  • Do other components share the same defect?
  • Are the automated tests checking the right thing?
  • Does the component API make accessible behavior easy or hard?
  • Can the default be made safe, so product teams need no accessibility expertise to use it correctly?
  • How do existing consumers receive the fix, and is it a patch or a break?
  • How will we know which applications are still on the bad version?
  • Does the design library teach the same behavior as the code does?

The Staff engineer usually still writes the fix. The difference is that the engineer also repairs whatever produced the defect, so the next one has a harder time getting out.

Make correct UI behavior easier for every product team, not only for the application you happen to be working on today.

Staff and Principal without direct reports

Staff and Principal can be individual-contributor levels. GitLab publishes both on its IC career matrix, and Monzo’s example explicitly describes the role as having no direct reports.41 Leadership travels through artifacts instead: architecture proposals, technical specifications, code reviews, standards, prototypes, migration plans, and the unglamorous work of helping other engineers reach good decisions without you in the room.

If you like building things and do not want to manage people, Staff or Principal design-system work is one of the few paths that keeps getting more interesting rather than more administrative.

From component library to UI platform

These three terms get used interchangeably, and the difference between them is most of what this guide is about.

Component library

How do developers reuse this UI code?

Reusable implementation code. A button, an input, a dialog, a date picker. The output is code other people import.

Design system

How should this organization design and build interfaces?

The library plus the shared decisions around it: patterns, tokens, accessibility guidance, documentation. The output is agreement, and code that encodes it. This is a working definition for this guide, not a universal one.

UI platform

Can many teams depend on this safely, for years?

What a design system becomes once enough teams depend on it that it needs a deliberate public API, a release process, a compatibility strategy, a support model, and a migration path. The output is infrastructure, and infrastructure has consumers.

The third row is where Staff-level UI work becomes most valuable, because it is the layer where many teams share the consequences of one architectural decision.

A platform is a pipeline with a fan-out in the middle, and every stage of it is somebody’s job:

Design decisions

What the organization has agreed a button, a spacing step, or an error state is. Usually held in design tooling, occasionally only in someone’s head.

Design tokens

Those decisions given names and made portable, so they can travel between tools and platforms without being retyped by hand.

Shared components

The tokens rendered as behavior. Web Components put this layer on the browser rather than on a framework, which is the whole reason to choose them.

Consumers

A React adapter, plain HTML, whatever else is in the estate. Each one is a translation layer with its own ergonomics and its own maintenance cost.

Products

The applications people actually use. Everything upstream is only worth what reaches this row.

Around that pipeline sits everything that keeps it alive: accessibility, documentation, testing, versioning, distribution, governance, adoption, support. The component is one piece, and rarely the piece that fails.

The public API

A product team using a shared component does not care whether its internals are Lit, Stencil, or hand-written Custom Elements. It cares about the contract. For a Web Component, that contract is bigger than most people first assume:

  • Tag name
  • Attributes
  • Properties
  • Methods
  • Events
  • Event payloads
  • Slots
  • CSS custom properties
  • CSS parts
  • DOM semantics
  • Keyboard behavior
  • Focus behavior
  • Accessible name and role

Treat that whole surface the way you would treat an API you sell to customers. Changing an internal implementation is cheap. Changing a contract that hundreds of applications depend on is not, and the cost lands on people who did not choose it. This is the single principle that governs everything in the migration section later.

The browser platform beneath the frameworks

The HTML Standard defines Custom Elements as a way for authors to build their own DOM elements with custom behavior. The DOM Standard defines the event dispatch, composition, retargeting, and Shadow DOM concepts those elements rely on.78 Stencil and Lit are higher-level APIs over exactly those capabilities.910

That’s the reason to learn the layer underneath: a Staff-level Web Component engineer should be able to reason about a component after every framework name is deleted from the conversation. Frameworks are the top row, and the top row is the one that gets replaced.

Lit / Stencil / FAST

Reactive updates, templating, and less boilerplate. This is the layer most likely to be replaced.

Custom Elements

The registry binding a tag name to a class, plus the lifecycle callbacks around connection and attribute change.

Shadow DOM

A private DOM tree with its own styling boundary, which gives a component encapsulation.

DOM events

How a component speaks outward, and whether that speech crosses the shadow boundary.

HTML semantics

Role, name, and state, supplied free by the browser if you picked the right element.

CSS

Custom properties and parts are the only styling hooks a consumer gets. You choose them on purpose or they get chosen for you.

Accessibility APIs

What assistive technology reads. It sits at the bottom of every stack, and no framework can work around it.

Custom Elements

A native button is created with <button>Save</button>. A Custom Element lets you define a new element name and associate it with a JavaScript class through the browser’s registry.7

js snippet js
class UiStatusMessage extends HTMLElement {
  connectedCallback() {
    if (!this.hasAttribute('role')) {
      this.setAttribute('role', 'status')
    }

    this.textContent ||= 'Ready'
  }
}

customElements.define('ui-status-message', UiStatusMessage)

That element now works anywhere HTML works, including inside templating languages that have never heard of it.

The lifecycle detail that catches people out is that an element can be attached, removed, and attached again. So this leaks:

js snippet js
connectedCallback() {
  window.addEventListener("resize", this.onResize);
}

Every teardown needs to pair with its setup, the same pattern Lit recommends whenever a component listens to something it doesn’t own, like window or document.11

js snippet js
connectedCallback() {
  window.addEventListener("resize", this.onResize);
}

disconnectedCallback() {
  window.removeEventListener("resize", this.onResize);
}

Attributes and properties are different things

They look interchangeable in a framework and they are not.

Attributes

Serialized text sitting in markup. They survive in static HTML, show up in the inspector, and can be targeted by CSS selectors.

Because the value is a string, everything else has to be parsed out of one.

Good for: simple scalar configuration. A variant, a size, a boolean state.

Properties

Live JavaScript values on the element object. They hold anything the language holds: objects, arrays, functions, class instances.

They leave no trace in markup, which means no static HTML and no CSS selector can reach them.

Good for: structured data. A list of menu items, a formatter, a fetched record.

In practice that means the same component is configured two ways, and the split is not arbitrary:

html snippet html
<ui-button variant="primary"></ui-button>
js snippet js
userMenu.items = [
  { id: 1, label: 'Profile' },
  { id: 2, label: 'Sign out' },
]

Lit provides reactive properties that trigger rendering and can optionally reflect back to attributes.12 Stencil exposes public data through @Prop(), documented as attributes and properties on the element.13 Both make reflection a one-word change, which is why it gets overused. Reflect when it has a job: declarative HTML configuration, a CSS selector that needs the state, debugging visibility, or semantics that belong in markup. Reflecting a large object because the decorator allowed it just puts noise in the DOM.

Shadow DOM and styling hooks

Shadow DOM gives a host element its own encapsulated tree. The DOM Standard distinguishes the document tree from shadow trees and defines how both participate in event dispatch.8 Lit uses it by default.14

The component gets a small private room, and <slot> marks the doors where consumer content is allowed in:

html snippet html
<!-- what the component holds internally -->
#shadow-root
<article>
  <slot name="name"></slot>
</article>

<!-- what the consumer writes -->
<ui-user-card>
  <strong slot="name">Joshua</strong>
</ui-user-card>

Encapsulation solves one problem and creates another: consumers still need to restyle things, and if you give them no legitimate way to do it, they’ll find an illegitimate one. Two platform mechanisms exist. CSS custom properties pass values in. CSS Shadow Parts, via the part attribute and ::part() selector, expose selected internal elements for outside styling.15

css snippet css
/* Component: declare the hook */
:host {
  --ui-button-background: ButtonFace;
}
button {
  background: var(--ui-button-background);
}

/* Consumer: use it */
ui-button {
  --ui-button-background: rebeccapurple;
}
html snippet html
<!-- Component: expose one internal element by name -->
<button part="control">
  <slot></slot>
</button>
css snippet css
/* Consumer: reach it */
ui-button::part(control) {
  border-radius: 999px;
}

Both of these are API design decisions. Expose too little and consumers hack around the system, usually with !important and a selector that breaks on your next release. Expose every internal element and you have made your private implementation public, permanently. The useful question is whether you are willing to keep this element, with this name, in this position, for the next three years.

Server rendering touches this same boundary. The HTML Standard supports declarative Shadow DOM through shadowrootmode, so a shadow root can be expressed in markup instead of only constructed by client-side JavaScript.16

html snippet html
<ui-card>
  <template shadowrootmode="open">
    <article>
      <slot></slot>
    </article>
  </template>
  Hello
</ui-card>

Not every component needs to be server-rendered. But rendering strategy, hydration cost, search requirements, and first-paint behavior stop being one product’s concern the moment a design system is shared, because the platform’s answer becomes every product’s answer.

Events

Events are how a component reports that something happened. The DOM Standard defines bubbles, cancelable, and composed, and defaults all three to false.8 The third one is the one that matters here, because composed controls whether an event can cross a shadow boundary at all.

js snippet js
this.dispatchEvent(
  new CustomEvent('value-change', {
    detail: { value: 'hello' },
    bubbles: true,
    composed: true,
  }),
)

Lit treats events as the normal way elements communicate upward, in a “data down, events up” model.1117 Stencil is explicit that there is no such thing as a Stencil event: its decorators define and emit ordinary DOM events.18

The consequence is easy to state and easy to forget. Event names and event payloads are public API. Renaming value-change to valueChange is a breaking change for every listener in the estate, and unlike a renamed TypeScript export, nothing fails to compile. The listeners just stop firing.

Retargeting is the other Shadow DOM behavior worth knowing by name. When an event crosses a shadow boundary, the DOM’s retargeting rules change what an outside listener sees as event.target: it points at the host rather than the internal node actually clicked.8 This protects encapsulation and surprises everybody once. You don’t need to memorize the algorithm, just know it exists and reach for composedPath() when an event arrives with a target you didn’t expect.

Form controls

The HTML Standard defines form-associated custom elements and the ElementInternals capabilities that let a Custom Element participate in native form behavior properly, rather than shadowing it with a hidden input.7 Stencil documents support for form-associated components and their lifecycle callbacks.19

js snippet js
class UiRating extends HTMLElement {
  static formAssociated = true

  #internals = this.attachInternals()
  #value = ''

  set value(nextValue) {
    this.#value = String(nextValue)
    this.#internals.setFormValue(this.#value)
  }

  get value() {
    return this.#value
  }
}

customElements.define('ui-rating', UiRating)

Before building any of it, ask whether a native control already solves the problem. Native controls arrive with years of browser, form, keyboard, mobile, and assistive-technology behavior you’d otherwise reimplement at your own expense, on your own timeline, with your own bugs. A custom rating widget is reasonable to build. A custom checkbox usually isn’t.

Lit

Lit describes itself as a library for building lightweight Web Components. Lit components are standard Web Components; what Lit adds is a reactive base class, declarative templates, scoped styles, and a set of conveniences on top.10 The browser supplies Custom Elements, the DOM, Shadow DOM, and events. Lit supplies less boilerplate around them.

ts snippet ts
import { LitElement, css, html } from 'lit'
import { customElement, property } from 'lit/decorators.js'

@customElement('ui-button')
export class UiButton extends LitElement {
  @property({ reflect: true })
  variant: 'primary' | 'secondary' | 'danger' = 'primary'

  static styles = css`
    button {
      font: inherit;
    }
  `

  render() {
    return html`
      <button type="button" data-variant=${this.variant}>
        <slot></slot>
      </button>
    `
  }
}

The Lit lifecycle

Lit batches reactive updates asynchronously rather than rendering once per assignment, so “when does this run” has a real answer worth knowing.1220

constructor

The element exists but is not in the document. Set defaults. Do not touch the DOM or read layout.

connectedCallback

Now it is in the document. Subscribe to things you do not own, and remember that this can run more than once per element.

willUpdate

Before rendering. Compute derived values here so render stays a pure description of output.

render

Return the template. No side effects, no fetching, no measuring.

firstUpdated

First render only. The shadow tree now exists, so this is where one-time DOM setup goes.

updated

Every render after work is committed. Read layout here if you must, and expect to pay for it.

Lit also exposes updateComplete, a promise resolving around the end of the update cycle. Its documentation recommends awaiting a completed render before emitting events whose listeners need to observe the new rendered state.20 Learn the lifecycle well enough to answer “when should this work happen,” not well enough to recite the names.

Public property or internal state

Lit separates public reactive properties from internal reactive state.12 The decorator you pick declares whether a value is part of the public API.

ts snippet ts
@property()
value = ""; // public API. You now own this for years.

@state()
private validationMessage = ""; // implementation detail. Yours to change.

If consumers do not need to set it or read it, do not make it public. Every public field is another behavior somebody will depend on, discover by accident, and file a bug about when it changes.

Reactive Controllers

Reactive Controllers package reusable state, behavior, and lifecycle participation so it can be shared across components.21 They earn their place for cross-cutting concerns: localization, resize observation, shared keyboard behavior, state synchronization. They stop earning it the moment they become the place unrelated component logic goes to hide. A controller that four components use is infrastructure. A controller that one component uses is a file you moved code into.

For an engineer who already knows Stencil, Lit is high-leverage as a second implementation model precisely because it forces the separation: after building the same component twice you can tell which ideas belonged to Stencil and which belonged to Web Components. That knowledge lasts longer than fluency in any single framework.

Using Web Components in React

The old line about React being unable to consume Web Components is out of date. React 19’s release notes state that it added full support for Custom Elements and passes the Custom Elements Everywhere suite, and the current React DOM documentation covers custom-element event handling including CustomEvent through JSX props.2223

What is still true is that interoperability and ergonomics are different questions. A component can work correctly in React and still feel foreign to a React team, which is what a thin adapter is for. @company/components publishes <ui-button> and owns the behavior; @company/react publishes Button and adapts that public API to React conventions of naming, typing, refs, and event props. Lit publishes @lit/react for exactly this.24

ts snippet ts
import * as React from 'react'
import { createComponent } from '@lit/react'
import { UiButton } from '@company/components/button.js'

export const Button = createComponent({
  tagName: 'ui-button',
  elementClass: UiButton,
  react: React,
})

Keep adapters thin. Once a wrapper contains its own interaction model, its own state, or its own opinions about behavior, you no longer have a design system with a React adapter. You have two design systems, and only one of them has tests.

Migrating Stencil to Lit

Stencil’s compiler reads metadata from decorators like @Prop() and its event and lifecycle declarations, then compiles component output.91318 Lit uses standard Web Components with its own reactive-property and rendering layer.1012 The migration involves more than swapping one decorator vocabulary for another, and teams that treat it as a find-and-replace tend to ship a rewrite that breaks every consumer.

The goal is to replace the implementation and preserve the consumer contract, unless there is a deliberate, argued reason to change it. So the first deliverable is an inventory, per component, of everything a consumer can currently observe:

  • Tag name
  • Attributes
  • Properties
  • Property types
  • Reflection behavior
  • Methods
  • Events
  • Event payloads
  • Bubbling and composition
  • Slots
  • CSS custom properties
  • CSS parts
  • Default content
  • Keyboard behavior
  • Focus behavior
  • ARIA and semantics
  • Form behavior

One component in Stencil and Lit

Here is a toggle in Stencil:

tsx snippet tsx
import { Component, Event, EventEmitter, h, Prop } from '@stencil/core'

@Component({
  tag: 'ui-toggle',
  shadow: true,
})
export class UiToggle {
  @Prop({ reflect: true }) checked = false

  @Event({ bubbles: true, composed: true })
  checkedChange!: EventEmitter<boolean>

  private toggle = () => {
    this.checkedChange.emit(!this.checked)
  }

  render() {
    return (
      <button
        type="button"
        aria-pressed={String(this.checked)}
        onClick={this.toggle}
      >
        <slot />
      </button>
    )
  }
}

And in Lit:

ts snippet ts
import { LitElement, html } from 'lit'
import { customElement, property } from 'lit/decorators.js'

@customElement('ui-toggle')
export class UiToggle extends LitElement {
  @property({ type: Boolean, reflect: true })
  checked = false

  private toggle() {
    this.dispatchEvent(
      new CustomEvent<boolean>('checkedChange', {
        detail: !this.checked,
        bubbles: true,
        composed: true,
      }),
    )
  }

  render() {
    return html`
      <button
        type="button"
        aria-pressed=${String(this.checked)}
        @click=${this.toggle}
      >
        <slot></slot>
      </button>
    `
  }
}

The code is completely different, but the consumer sees the same tag, reflected attribute, event name and payload, slot, and keyboard behavior. That only works because someone wrote the contract down first.

This example is also deliberately small. A real migration has to account for event casing conventions, generated type definitions, framework bindings, style output, hydration strategy, form behavior, test utilities, build targets, and package structure. None of that shows up in a toggle.

Migrating in steps

Unless there’s a business reason for a rewrite, migrate a large shared library in slices, keeping consumers whole while the implementation changes underneath them.

Inventory the current public contracts

Everything in the list above, per component, written down where other people can read it.

Add contract tests to the existing components

Written against consumer-visible behavior, not internals. These are what let you swap implementations at all.

Pick one low-risk component

Something with few consumers and no form or focus behavior, so the first migration tests the process itself.

Reimplement it, and run both through the same tests

One suite, two implementations. Any divergence is a contract change you did not intend.

Publish a prerelease and validate real consumers

Use a real product with its real build and framework, instead of a demo application.

Migrate in slices, then remove the old implementation

Removal comes after adoption, which you can only confirm if you are measuring it.

The key point in that sequence is that a migration has consumers, and every decision in it affects another team’s sprint.

Contracts: TypeScript, packages, versions

Types as API documentation

The TypeScript Handbook documents unions, narrowing, generics, and utility types as mechanisms for describing and constraining JavaScript APIs.25262728 These matter more in a design system than in application code, because a public component API gets reused by people who will never read its source.

A union closes a set:

ts snippet ts
type ButtonVariant = 'primary' | 'secondary' | 'danger'

const variant: ButtonVariant = 'banana'
// Type error

A discriminated union goes further and makes invalid combinations unrepresentable, so the type describes which data is valid in which state:

ts snippet ts
type LoadState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error }

function renderState<T>(state: LoadState<T>) {
  switch (state.status) {
    case 'success':
      return state.data

    case 'error':
      return state.error.message
  }
}

There is no state in which data exists and status is 'error', because the type will not let you construct one. TypeScript’s narrowing model refines the union from the discriminant at each branch.26

Generics and open or closed types

Generics describe a relationship between types while preserving information about the values passing through.27 Same shape, different data:

ts snippet ts
interface SelectOption<TValue> {
  label: string
  value: TValue
}

const numberOption: SelectOption<number> = { label: 'Five', value: 5 }
const stringOption: SelectOption<string> = { label: 'Connecticut', value: 'CT' }

The harder decision is how closed to make a type at all:

ts snippet ts
type Size = 'small' | 'medium' | 'large'
// versus
type Size = string

The first gives autocomplete, catches typos, and forbids anything you didn’t anticipate. The second is extensible and lets 'smal' ship to production. There’s no universal winner: the real question is whether consumers are expected to extend this value space. Close it for strict visual consistency. Leave it open if teams legitimately need sizes you haven’t thought of, since otherwise they stop using the prop and reach for a class instead, and you lose both consistency and the signal that they needed something.

Staff-level TypeScript is less about knowing every utility type and more about designing public APIs that communicate intent, reject invalid states where practical, and stay legible to somebody who only reads the autocomplete.

Package entry points

Node and npm use package.json to describe entry points, dependencies, and module behavior, and Node supports ECMAScript modules as an official format.293031 The exports field defines which entry points consumers are allowed to import, and Node’s own publishing guidance warns that adding exports to an existing package is a breaking change if consumers were previously importing undeclared deep paths.32

That warning sums up this section: a four-line change to a config file, and every application that reached into your dist/ folder stops building.

json snippet json
{
  "name": "@company/components",
  "version": "5.2.0",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./button": {
      "types": "./dist/button.d.ts",
      "import": "./dist/button.js"
    }
  },
  "types": "./dist/index.d.ts",
  "sideEffects": false
}

The exact shape follows your build output. The architectural question does not: which imports have we promised to keep working? The moment your documentation contains import '@company/components/button', that path is public developer experience, whether or not anyone wrote it down as API.

Dependency types

npm documents the dependency fields, and the mental model is short.3330 dependencies are needed at runtime. devDependencies are needed to build, test, or develop the package. peerDependencies declare a compatibility relationship with something the consuming environment supplies.

A React adapter declares React as a peer rather than shipping its own copy, because two React runtimes in one application is a class of bug nobody enjoys diagnosing:

json snippet json
{
  "peerDependencies": {
    "react": ">=19"
  }
}

These choices decide duplicate runtimes, bundle size, install behavior, upgrade constraints, and how much support you end up giving. They are made in a config file and felt in every consuming application.

Semantic versioning for design systems

Semantic Versioning defines MAJOR.MINOR.PATCH once a public API is declared: incompatible changes bump major, backward-compatible features bump minor, backward-compatible fixes bump patch.34

What SemVer won’t do is tell you what your public API is, and for a design system that reaches well past TypeScript signatures: the same surface listed earlier (attributes, events and their payloads, CSS hooks, slots, keyboard behavior, semantics, import paths, framework adapters, or visual output products built layouts around) can all be breaking. Tightening a button’s default padding by 4px ships as a patch in most teams and breaks three product layouts by Tuesday.

One package, or many

There is no standard answer, and the tradeoffs are real in both directions.

ShapeWhat you getWhat it costs
One package (@company/design-system)Simple discovery. Versions always in step. One dependency for consumers to reason about.Broad releases. Every change feels coupled to every consumer, whether or not it is.
Layered (tokens, icons, components, react)Clear architectural boundaries. Adapters evolve independently. Tokens can serve non-web consumers.Compatibility between layers becomes something a human has to manage.
One per component (button, dialog, select)Genuinely independent install and release.Version sprawl, expensive dependency coordination, and real discoverability cost.

For most enterprise design systems, a small number of deliberately layered packages is the right starting point. Split when a boundary actually exists, not because the monorepo tool made splitting cheap.

Managing breaking changes

SemVer announces that a break happened.34 It does not perform the migration, and the version number is the least useful part of the process to a team with forty applications and a quarter of committed roadmap.

Proposal, and consumer impact analysis

Who breaks, how badly, and how many of them are mid-release when it lands.

Deprecation period with a replacement already shipped

Deprecating an API before its replacement exists just tells teams to feel bad about code they can’t change yet.

Migration guide, and a codemod where it is practical

A guide moves the migration work onto each consuming team, and a codemod automates most of it.

Prerelease, validated against real consumers

The point is to find what your inventory missed while it’s still cheap to find.

Major release, then adoption tracking

You won’t know the change succeeded until you can see which teams have upgraded.

Removal of the old API

Last, and only once tracking says it’s safe. A deprecated API you never remove is a permanent API with a warning on it.

A system used by a hundred applications needs a different policy from one used by three. The failure mode is running the hundred-application process at three applications, which reads as bureaucracy, or the three-application process at a hundred, which reads as chaos.

Accessibility, testing, and browser support

WCAG and the APG

WCAG 2.2 is a W3C Recommendation containing testable success criteria for web content.3536 The WAI-ARIA Authoring Practices Guide provides patterns and examples for common widgets and interaction models.3738 WCAG asks what outcomes accessible content must satisfy. The APG asks how a given interactive pattern might be built. Neither is a certification stamp: an APG example is a reference implementation of one pattern, not a guarantee about your product, your semantics, or your users.

Accessible behavior is part of the contract

A component isn’t accessible just because it renders <div role="dialog">. Accessibility depends on the interaction model. For a dialog that means the trigger activates it, focus moves somewhere sensible, keyboard interaction works, background interaction is handled, and on close focus returns to where the user left it.38 The best accessibility work in a design system makes the safe behavior the default, so using the component correctly is easier than using it incorrectly.

Prefer native semantics wherever they fit. When the interaction really is a button:

html snippet html
<!-- not this -->
<div role="button" tabindex="0">Save</div>

<!-- this -->
<button type="button">Save</button>

ARIA is valuable and sometimes unavoidable. Using it to reconstruct semantics HTML already provides is how a design-system team acquires behavior it now has to maintain forever, for nothing in return.

Limits of automated accessibility testing

Playwright’s accessibility documentation is plain about this: automated tests catch common problems, many issues need manual testing, and the recommendation is to combine automated checks with manual assessment and inclusive user testing.39 Storybook can run the same checks in component development and CI.40

ts snippet ts
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

test('dialog has no automatically detectable violations', async ({ page }) => {
  await page.goto('/components/dialog')

  const results = await new AxeBuilder({ page }).analyze()

  expect(results.violations).toEqual([])
})

A green result tells you a specific set of rules found nothing. It doesn’t tell you whether the keyboard flow makes sense, whether the label is comprehensible, or whether a screen-reader user’s path through the task is efficient, and all of those problems can ship with a passing test run.

Test layers

Storybook is built for developing and documenting components in isolation; Playwright runs end-to-end across Chromium, Firefox, and WebKit through one API.4142 A shared UI system usually wants several layers, because each answers something the others can’t.

LayerThe question it answersIn practice
UnitDoes this small function behave correctly?expect(normalizeSize('SMALL')).toBe('small')
Component renderDoes it render the right state for the right input?Mount with properties, assert the output tree
InteractionDoes clicking, typing, opening, dismissing work?Driven interaction against a rendered component
AccessibilityDo automated rules find common violations?axe against each rendered state
BrowserDoes it work in the engines we committed to?The same suite, three engines
ContractDoes the public API still behave as promised?Set the property, assert the attribute and the event
Visual regressionDid the appearance change without anyone deciding to?Screenshot diff per component state

Contract tests are the highest-leverage row for a platform team: they’re what lets the implementation move while consumer behavior stays still, and the reason the Stencil-to-Lit migration described earlier is possible at all.

Say the documented contract is: changing the open property on <ui-dialog> makes the dialog visible, and closing it emits ui-close. The test is written against that sentence, not against Lit:

ts snippet ts
import { test, expect } from '@playwright/test'

test('dialog public contract', async ({ page }) => {
  await page.setContent(`
    <ui-dialog id="dialog">Hello</ui-dialog>
    <script type="module" src="/dist/ui-dialog.js"></script>
  `)

  await page.locator('#dialog').evaluate((element) => {
    element.open = true
  })

  await expect(page.locator('#dialog')).toHaveAttribute('open')

  // Continue with the documented close interaction
  // and verify the documented public event.
})

The test doesn’t depend on which library rendered the dialog, which is what lets the implementation change safely.

Browser support policy

Playwright supports testing Chromium, Firefox, and WebKit, covering the major engine families in automated runs.4243

Don’t start compatibility planning by asking what polyfills you need. Start by asking which browsers and product environments the organization has actually promised to support: that answer determines the polyfills, feature detection, fallbacks, and which high-risk interactions get checked by hand.

Then write the policy down and publish it. Every product consuming the system inherits those decisions whether or not they were told, and an unstated policy gets discovered during an incident.

Tokens, governance, and adoption

The stable design tokens format

The Design Tokens Community Group, a W3C Community Group, published the first stable Design Tokens Format Module in October 2025, defining a file format for exchanging tokens between tools.4445 Its technical material describes tokens as individual design-system values: color, spacing, typography.46

json snippet json
{
  "color": {
    "action": {
      "primary": {
        "$type": "color",
        "$value": {
          "colorSpace": "srgb",
          "components": [0.1, 0.3, 0.8],
          "alpha": 1
        }
      }
    }
  }
}

Follow the applicable DTCG version rather than that simplification. What matters more than the syntax: a token is a named design decision that can travel between tools and platforms without a human retyping a hex value into a second system and introducing a third one by accident.

Figma Code Connect

Figma’s Code Connect documentation describes it as a way to connect code components in a repository to their Figma counterparts in Dev Mode, with documented support for HTML and Web Components.4748

It links a Figma component, the design intent behind it, a component API, and an implementation, but neither side is automatically correct: sometimes the design model is stale, sometimes the code API is wrong, and sometimes the two model genuinely different concepts and need an explicit translation between them.

That’s what makes design-code mismatch a Staff-level problem rather than a ticket. Resolving it usually requires design and engineering to agree on what the thing is, and no amount of renaming a prop substitutes for that conversation.

Governance

The word sounds bureaucratic. In practice it answers a short list of questions that would otherwise get re-litigated every quarter: who can add a component, who owns the API, what counts as breaking, how proposals get reviewed, who signs off on accessibility, how deprecated APIs get removed, and how design and code stay in step.

Good governance reduces repeated arguments. Bad governance either blocks everything or lets the system drift into inconsistency, and both failure modes look like “we have a process.”

A lightweight proposal flow is usually enough: problem, evidence of consumer need, proposed API, alternatives, accessibility and design review, engineering review, prototype, decision. Not every button change needs an RFC. A change to how every button in the company reports its state probably does.

Decisions worth remembering get an Architecture Decision Record, which can be very small:

markdown snippet markdown
# ADR: Dialog uses native <dialog> internally

## Status

Accepted

## Problem

We need a consistent modal-dialog primitive.

## Constraints

- Must support keyboard users.
- Must work in supported browsers.
- Must expose a stable Web Component API.
- Must allow future internal replacement.

## Decision

Use the native <dialog> element inside the component.

## Alternatives considered

- Custom fixed-position div
- Third-party dialog primitive

## Consequences

Positive:

- More platform behavior is delegated to the browser.

Negative:

- We must validate browser behavior against our support policy.

The record matters more than the template. Eighteen months later, when someone proposes replacing the internals, they can find out why the decision was made instead of re-deriving it or assuming it was arbitrary. Leave enough context that the next engineer can disagree with you knowledgeably.

Diagnosing low adoption

A design system can be technically excellent and still fail. When adoption is weak, diagnose before prescribing: “we need better documentation” is the answer teams reach for regardless of the actual problem. Nine questions, and what a “no” to each one is really telling you:

AskWhat a “no” actually means
Do teams know the system exists?A communications problem, and the cheapest one on this list to fix.
Does it contain what they need?A roadmap problem. They are not refusing the system, they are routing around a gap.
Can they understand the API?A design problem in the API itself. Documentation is the symptom, not the cause.
Can they install it without help?A packaging problem. Entry points, peer dependencies, or build assumptions.
Does it work with their framework?An architecture problem, and the most expensive one to discover late.
Can they customize it enough?A governance problem. Your constraints and their product needs have diverged.
Do releases break them?A versioning problem. Trust, once spent here, takes several clean releases to earn back.
Can they get help when stuck?A staffing problem that looks like a documentation problem.
Is migrating cheaper than staying?An economics problem. Until this one flips, nothing else on the list matters.

Most of these problems can’t be fixed with better documentation.

Developer experience

Storybook’s model centres on building and documenting components in isolation, which is why it’s the common tool here.41 But the documentation site is one part of a longer surface: package installation, import paths, TypeScript autocomplete, naming, examples, error messages, release notes, migration docs, framework adapters, local development speed, debugging, the contribution workflow, and where someone goes when they’re stuck at 4pm.

The bar for a component API is that an ordinary product engineer can use it correctly without knowing how the design-system team built it. If using it correctly requires understanding the implementation, the API is the bug.

Measuring success

Useful signals are mostly unglamorous: applications on the system, package versions deployed, the lag between a release and consumer upgrade, deprecated API usage, component usage, support request volume, migration completion, accessibility defects originating in shared components, and build or bundle regressions.

Every one needs context. Low adoption can mean the system is failing, or that it’s deliberately scoped to one product family. Read in groups, they answer three different questions:

LensThe question it answersSignals
Consumer successCan product engineers build correct interfaces faster, with less repeated decision-making?Adoption, time to implementation, support burden, successful upgrades, developer feedback
End-user successAre the experiences accessible, consistent, and reliable for the people actually using them?Accessibility defect rates, usability findings, consistency audits, interaction failures
System healthCan the platform keep changing safely?Upgrade latency, release reliability, test stability, deprecation backlog, unresolved architectural debt

Use all three, because a team measuring only the first optimizes for shipping components, and a team measuring only the third optimizes for never shipping anything.

“We have 87 components” says nothing about whether any of them are useful, accessible, maintainable, or used.

Making and sharing decisions

Start architecture questions with constraints

Someone asks how you’d build a design system for fifty applications. The weak answer is “I would use Lit.” Lit may well be right, but nothing yet established makes it right.

Start by discovering constraints. What frameworks consume the system? Is server rendering required? Which browsers are supported? What already exists, and who depends on it? How many teams, and how independent are they? How quickly can consumers upgrade, given their release process? What accessibility standard does the organization target, and is it a legal obligation? Are there native or mobile consumers of the same tokens? How are packages published? Is the organization optimizing for strict consistency or product flexibility? Who owns governance?

Let the architecture follow from the answers.

A repeatable decision model

Work through the consumer, problem, constraints, options, tradeoffs, decision, rollout, and measurement, in that order, every time.

Take a real question: should the shared system be React components or Web Components?

Consumer and constraints

Thirty applications. Twenty React, six Angular, four framework-light. One visual language, accessibility consistency required, long product lifetimes, some server-rendering needs.

None of this is a technology preference yet, just facts about the estate.

Options on the table

  • React-only core
  • Web Component core with adapters
  • A separate implementation per framework
  • Shared headless logic with framework-native view layers

Each moves interoperability, ergonomics, SSR strategy, team expertise, package complexity, and maintenance cost in different directions.

The decision comes after that, and it’s defensible because everyone can see what it was weighed against. Staff-level interviewing is mostly about demonstrating which facts determine the answer, not naming the interviewer’s preferred technology.

Leading through written artifacts

GitLab’s Staff and Principal frameworks emphasize architectural leadership, standards, mentorship, cross-team work, and enabling others.23 Without direct reports, all of that has to travel through things you make: an RFC, an ADR, a prototype, a migration guide, an API specification, an accessibility contract, a reference implementation, a code review, a test harness, a release plan, documentation.

The best Staff engineers don’t make themselves indispensable by holding knowledge. They make their judgment reproducible, in tools and standards and examples, so the same decision gets made correctly in rooms they’re not in. That’s also the difference between mentoring and being an answer machine. Compare:

Answering

”Change this to composed: true.”

Correct and fast, but it only fixes one bug, and the engineer mostly learns that you know the answer.

Mentoring

”Does this event need to be observed outside the shadow tree? If so, let’s look at how composed affects the boundary.”

It takes longer the first time, but the engineer can solve the next four event problems without you.

Mistakes to avoid

Three habits are worth resisting, because each one looks like diligence.

You do not need to master every frontend framework. A UI-platform engineer gets far more leverage from deep platform knowledge plus strong adaptation skills. Go deep on HTML, CSS, JavaScript and TypeScript, accessibility, component and API design, and browser behavior. Stay strong on Lit or Stencil, React interoperability, testing, package tooling, and design tooling. Working knowledge is enough for other consumer frameworks, build systems, and SSR environments. Frameworks change, but platform concepts carry over.

Complexity is not seniority. Nobody demonstrates Staff-level ability by selecting the most sophisticated available architecture. Sometimes the correct decision is to use <button>, or to not build the component, or to keep the old API for another year because migration costs more than the benefit is worth. Judgment includes knowing when not to introduce an abstraction.

Not every problem is a design-system problem. Some UI is legitimately product-specific. A design system should absorb repeated organizational patterns, not every unique feature that happens to look tidy in a shared repo. Ask whether multiple consumers will genuinely benefit, or whether product logic is being moved into shared infrastructure because it feels neater there. The wrong abstraction creates more coupling than the duplication it replaced.

A practice project

The most useful practice project for this specialization is a very small component platform, designed so you have to solve platform problems instead of feature problems.

text snippet text
ui-system/
├── packages/
│   ├── tokens/
│   ├── components/
│   └── react/
├── apps/
│   ├── docs/
│   └── react-demo/
└── tests/

Four components is enough: ui-button, ui-text-field, ui-alert, and ui-dialog, because they cover progressively harder concerns in order. The button covers semantics, the text field covers form participation, the alert covers announcements and status, and the dialog covers focus management and modality, which is where many component libraries give up.

Build one component with no framework at all

A native Custom Element. Lifecycle, attributes and properties, event dispatch, Shadow DOM, slots, styling boundaries. Then rebuild the same component in Lit. The comparison is the actual lesson, and you only get it by doing both.

Build the other three in Lit

With typed properties, documented events, slots, CSS custom properties, deliberately chosen parts, real accessibility behavior, stories, and tests. Lit’s documentation is the reference here, not a tutorial from 2021.

Package it properly

Three packages: tokens, components, react. Practise exports, declaration files, ESM, versioning, peer dependencies, and installing it somewhere else. Use the Node, npm and SemVer documentation as your source of truth rather than copying an old package template off a blog.

Consume it from React, twice

Use the Custom Elements directly first. Then add an adapter. Then answer honestly whether the adapter improved anything, because that is the question you will be asked in an interview.

Break it on purpose

Rename variant=“danger” to intent=“critical”, then do the work a platform team would owe its consumers: explain why, deprecate the old API, warn at the right moment, write the migration guide, consider a codemod, ship a prerelease, update the adapter, the tests, and the examples.

That last stage teaches more Staff-level thinking than building ten additional visual components, because it is the only one where the hard part is other people.

A ten-week study plan

If you want the same thing on a schedule, this is the sequence I would run. The deliverable column is the part that matters; the study column is just what you need in order to produce it.

WeeksFocusDeliverable
1-2The browser platform. Custom Elements and their lifecycle, Shadow DOM, slots, DOM events, bubbles and composed, retargeting, attributes versus properties, CSS custom properties, ::part, ElementInternals, declarative Shadow DOM.7815One useful native Custom Element, built with no component framework.
3-4Lit. LitElement, templates, reactive properties, internal state, lifecycle, events, styles, controllers.10122021The four-component mini design system.
5TypeScript API design. Unions, narrowing, discriminated unions, generics, utility types, public versus internal types.252627Every public property and event payload typed, with a written reason each type is open or closed.
6Accessibility architecture. WCAG 2.2, the APG patterns relevant to your four components, keyboard behavior, focus management, names and descriptions, the limits of automation.3537An accessibility contract per component, verified by hand and by machine.
7Packaging. npm package anatomy, ESM, exports, dependencies, peer dependencies, SemVer.29323034The components packaged and consumed from a genuinely separate project.
8React interoperability. Current Custom Element behavior, event integration, refs, typing, wrapper tradeoffs.222324The system supported from React both directly and through an adapter, with the adapter’s existence justified in writing.
9Governance and migration.One ADR, one RFC, one breaking-API proposal, one migration guide, a Stencil-to-Lit plan, and a versioning policy.
10Interview simulation. System design, component API critique, accessibility architecture, packaging, migration, behavioral stories, technical disagreement, ambiguous requirements.Practice explaining your reasoning out loud, without rehearsed speeches.

Self-assessment checklist

The checklist has three groups. It isn’t a certification, and nobody will ask you to produce it. It’s useful the same way a preflight checklist is: gaps are hard to see until you look for them.

Technical depth

Can you explain, without reaching for a framework’s vocabulary:

  • Custom Elements, without using Stencil or Lit terminology
  • The tradeoffs of Shadow DOM, in both directions
  • Attributes versus properties, and when to reflect
  • Event bubbling, composition, and retargeting
  • Component lifecycle, and what work belongs where
  • Form-associated custom elements and ElementInternals
  • Accessibility semantics and keyboard behavior
  • TypeScript API design, and when to close a type
  • Package entry points and dependency kinds
  • SemVer, and what counts as breaking in a design system
  • A browser testing strategy and why it has those layers

Platform thinking

Can you:

  • Define a component’s public contract, completely
  • Design a migration that does not require rewriting every consumer
  • Decide what belongs in the core versus an adapter
  • Explain how the library is packaged and released
  • Diagnose low adoption without defaulting to “better docs”
  • Design a support model that does not depend on you
  • Write contribution and governance rules people will follow
  • Measure system health across more than one lens
  • Explain a tradeoff instead of naming a preference

Influence

Can you:

  • Lead a technical decision without managing anyone
  • Write a proposal other teams can act on
  • Mentor through reasoning rather than answers
  • Hold design and engineering concerns at the same time
  • Challenge a requirement constructively
  • Operate when the requirements are incomplete
  • Identify which problem actually matters most right now
  • Leave the organization better able to solve the next one

Finding the right role

The title matters far less than the work, but you have to type something into the search box:

  • Staff Design Systems Engineer
  • Principal Design Systems Engineer
  • Staff UI Engineer
  • Principal UI Engineer
  • Staff Frontend Engineer, Design Systems
  • UI Platform Engineer
  • Staff UI Platform Engineer
  • Principal UI Platform Engineer
  • UX Engineer
  • Staff UX Engineer
  • Principal Product Design Engineer
  • Design Systems Architect

Reading the job description

A real platform role gives one person responsibility across several of these: shared component architecture, Web Components or platform standards, accessibility, design tokens, component APIs, package publishing, semantic versioning, documentation, Storybook, cross-framework consumers, React adapters, Figma integration, browser support, testing infrastructure, governance, adoption, migration strategy, developer experience, technical specifications, cross-team architecture, and mentoring.

The faster tell is the language. Job descriptions leak scope:

Platform scope

  • ”used across multiple products"
  • "shared UI platform"
  • "framework-agnostic architecture"
  • "component distribution and versioning"
  • "design-system governance"
  • "drive adoption across engineering teams”

Every one of these implies consumers who are not you, which is the whole distinction.

Feature scope, relabelled

  • ”implement Figma designs in our React app"
  • "own our component library” with no consumers named
  • ”pixel-perfect implementation"
  • "maintain our CSS framework"
  • "partner with design to deliver features”

Also: application state management, feature delivery with occasional component cleanup, or people management wearing a technical title.

There is nothing wrong with the right-hand column. Those are good jobs, and simply a different career. Taking one while expecting the other is how people end up frustrated eighteen months later. The same goes for a short contract when what you want is stability: no amount of technical fit fixes that mismatch.

Industries that hire for this work

An organization with many products, many product teams, long-lived applications, accessibility obligations, several frontend frameworks, and one shared brand has structural reasons to fund a UI platform. An organization with two products and one team does not, and should not.

That points toward large technology firms, financial services, insurance, healthcare, enterprise SaaS, retail platforms, and government-facing systems. It says where the problems live, not that every company there has a mature design-system team. Plenty have the problem and nobody assigned to it, which is either the opportunity or the warning depending on how the rest of the interview goes.

How to describe yourself

“Frontend engineer” is accurate and tells a hiring manager almost nothing. If your differentiators are component systems, accessibility, cross-framework UI, and design-engineering infrastructure, then something like Design Systems & UI Platform Engineer carries far more signal. The specialization is an intersection: close enough to design to argue about what a component should do, close enough to engineering to ship the thing that does it.

An introduction should explain that specialty rather than recite your resume. Career foundation, specialization, technical depth, the organizational problems you solve, why this role fits:

“My background is in frontend engineering, but over time my work has concentrated on design systems and shared UI infrastructure. I focus on accessible component APIs, Web Components, developer experience, and the engineering systems needed to distribute components across products. What interests me about Staff-level UI-platform work is solving those problems once at the system level, instead of repeatedly inside individual applications.”

Treat it as an example to adapt; the structure is the reusable part.

Preparing for the interview

Prepare stories in advance so that interview time goes on reasoning rather than on remembering your own career. One story each:

StoryWhat it demonstrates
A difficult component APITechnical judgment
An accessibility problemAccessibility depth
A cross-framework problemPlatform thinking
A package or release problemDistribution knowledge
A design and code mismatchDesign-engineering collaboration
A developer-experience improvementConsumer empathy
A decision that turned out wrongLearning and judgment
An ambiguous initiativeStaff-level autonomy
A cross-team disagreementInfluence
A migrationRisk management

Structure each one as situation, constraint, options considered, decision, result, and what you learned. “Options considered” is what separates a Staff answer from a Senior one, because it is where architectural judgment becomes visible.

Expect a system-design prompt along the lines of “design a framework-agnostic component platform used primarily by React applications.” A good answer covers requirements, browser support, token architecture, Web Component contracts, the React adapter, accessibility, testing, packaging, versioning, documentation, governance, and adoption. The interviewer will learn more from which constraint you ask about first than from your final architecture.

Expect migration questions, and notice what they are really asking. How would you migrate Stencil to Lit? Move consumers from version 4 to 5? What if 20% of products cannot upgrade this year? What if accessible behavior requires a breaking API change? Each of those questions tests consumer empathy and risk management.

And prepare a real failure. “I care too much about quality” is not a failure and every interviewer has heard it. A usable one contains a genuinely mistaken assumption. Mine: we designed an API for maximum flexibility, consumers used it inconsistently, accessibility and documentation both got harder, and we learned the system needed stronger constraints rather than more options. Staff credibility goes up when you can show your judgment changed because reality corrected it.

Questions to ask them

Ask questions that reveal the system behind the job description.

The problem

  • What problem caused you to open this role?
  • What happens if you do not fill it?

System maturity

  • How mature is the existing design system?
  • How many applications and teams consume it?
  • Which frameworks are those applications built in?

Architecture

  • Who owns component API decisions today?
  • How are packages distributed and versioned?
  • How are breaking changes handled?
  • Is a migration already underway?

Design partnership

  • How closely do product design and design-system engineering work?
  • How are design tokens managed?
  • How is Figma and code alignment handled?

Scope and success

  • How do you distinguish Senior, Staff, and Principal here?
  • What would this person decide independently?
  • A year from now, what would make you call this hire a success?

Employment structure

  • Is the role permanent or contract?
  • If contract, what duration and what conversion expectation?
  • Who is the actual employer?
  • What benefits and protections differ from direct employment?

Ask that last group early rather than late. A technically perfect role can still be the wrong opportunity if its risk profile conflicts with what you need, and finding that out at offer stage wastes everyone’s time.

The long game is not learning every part of software engineering equally. It is becoming unusually capable in one vertical slice: accessible UI, component architecture, web standards, design systems, release infrastructure, cross-framework consumption, governance, adoption, platform architecture. That is depth without a detour into people management or backend distributed systems. Not every company has every rung, so chase the responsibility rather than the label.

Summary

The guide comes down to four layers.

The component

Can I build it correctly?

The API

Can other engineers use it correctly, without reading how it works?

The platform

Can many teams depend on it safely, over years, while it keeps changing?

The organization

Can I improve the decisions, standards, tools, and people that keep the platform healthy?

Senior UI engineering tends to be strongest in the first two. Staff UI-platform engineering increasingly operates in the second and third. Principal scope reaches the fourth. Real companies draw those boundaries differently, so treat it as a thinking tool rather than a ladder to climb.

A Staff-level UI engineer does more than build the hardest dropdown, valuable as that is. They ask how the component should behave, how its API should behave, how product teams will consume it, how it stays accessible, how it gets tested, how it is packaged, how changes ship safely, how teams migrate, how designers and engineers stay aligned, how anyone will know whether the system is working, and how the next engineer ends up more effective than the last one.

The transition is from building UI to engineering the platform through which an organization builds UI.

Sources

Everything above that is a matter of record is footnoted, and the full reference list sits at the foot of this page. The sources are standards bodies, official framework and tool documentation, and public company engineering frameworks, in that order of preference.

If you read only eight of them, read these:

One caution about secondary material. If you find older Microsoft FAST or Fluent Web Components articles, check them against the current FAST repository before designing anything around them. Framework-specific guidance in that ecosystem has aged considerably faster than the Web Component standards underneath it, which is a decent argument for learning the standards first.

Footnotes

  1. Monzo, “My path from intern to Staff Engineer at Monzo,” Aug. 10, 2023, https://monzo.com/blog/2023/08/10/my-path-from-intern-to-staff-engineer-at-monzo ↩ ↩2 ↩3

  2. GitLab, “Staff Engineer framework,” https://handbook.gitlab.com/handbook/engineering/careers/matrix/staff/ ↩ ↩2 ↩3 ↩4

  3. GitLab, “Principal Engineer framework,” https://handbook.gitlab.com/handbook/engineering/careers/matrix/principal/ ↩ ↩2 ↩3 ↩4

  4. GitLab, “Engineering Career Matrix,” https://handbook.gitlab.com/handbook/engineering/careers/matrix/ ↩ ↩2

  5. GitLab, “Staff Frontend Engineer,” https://handbook.gitlab.com/handbook/engineering/careers/matrix/development/dev/frontend/staff/ ↩

  6. GitLab, “Principal Engineer” public role expectations, within GitLab’s Engineering Career framework, https://handbook.gitlab.com/handbook/engineering/careers/matrix/principal/ ↩

  7. WHATWG, HTML Standard, “Custom elements,” https://html.spec.whatwg.org/multipage/custom-elements.html ↩ ↩2 ↩3 ↩4

  8. WHATWG, DOM Standard, https://dom.spec.whatwg.org/ ↩ ↩2 ↩3 ↩4 ↩5

  9. Stencil, “Component API,” https://stenciljs.com/docs/api ↩ ↩2

  10. Lit, “What is Lit?”, https://lit.dev/docs/ ↩ ↩2 ↩3 ↩4

  11. Lit, “Events,” https://lit.dev/docs/components/events/ ↩ ↩2

  12. Lit, “Reactive properties,” https://lit.dev/docs/components/properties/ ↩ ↩2 ↩3 ↩4 ↩5

  13. Stencil, “Properties,” https://stenciljs.com/docs/properties ↩ ↩2

  14. Lit, “Shadow DOM,” https://lit.dev/docs/components/shadow-dom/ ↩

  15. W3C, CSS Shadow Parts, https://www.w3.org/TR/css-shadow-parts-1/ ↩ ↩2

  16. WHATWG, HTML Standard, scripting and declarative shadow roots, https://html.spec.whatwg.org/multipage/scripting.html ↩

  17. Lit, “Component composition,” https://lit.dev/docs/composition/component-composition/ ↩

  18. Stencil, “Events,” https://stenciljs.com/docs/events ↩ ↩2

  19. Stencil, “Form-Associated Components,” https://stenciljs.com/docs/form-associated ↩

  20. Lit, “Lifecycle,” https://lit.dev/docs/components/lifecycle/ ↩ ↩2 ↩3

  21. Lit, “Reactive Controllers,” https://lit.dev/docs/api/controllers/ ↩ ↩2

  22. React, “React v19,” Dec. 5, 2024, https://react.dev/blog/2024/12/05/react-19 ↩ ↩2

  23. React, “React DOM Components,” https://react.dev/reference/react-dom/components ↩ ↩2

  24. Lit, “React,” https://lit.dev/docs/frameworks/react/ ↩ ↩2

  25. TypeScript, “The TypeScript Handbook,” https://www.typescriptlang.org/docs/handbook/intro.html ↩ ↩2

  26. TypeScript, “Narrowing,” https://www.typescriptlang.org/docs/handbook/2/narrowing.html ↩ ↩2 ↩3

  27. TypeScript, “More on Functions,” generics material, https://www.typescriptlang.org/docs/handbook/2/functions.html ↩ ↩2 ↩3

  28. TypeScript, “Utility Types,” https://www.typescriptlang.org/docs/handbook/utility-types.html ↩

  29. Node.js, “Modules: Packages,” https://nodejs.org/api/packages.html ↩ ↩2

  30. npm, “package.json,” https://docs.npmjs.com/cli/v12/configuring-npm/package-json/ ↩ ↩2 ↩3

  31. Node.js, “ECMAScript modules,” https://nodejs.org/api/esm.html ↩

  32. Node.js, “Publishing a package,” https://nodejs.org/en/learn/modules/publishing-a-package ↩ ↩2

  33. npm, “Specifying dependencies and devDependencies in a package.json file,” https://docs.npmjs.com/specifying-dependencies-and-devdependencies-in-a-package-json-file/ ↩

  34. Semantic Versioning, https://semver.org/ ↩ ↩2 ↩3

  35. W3C, “Web Content Accessibility Guidelines (WCAG) 2.2,” https://www.w3.org/TR/WCAG22/ ↩ ↩2

  36. W3C WAI, “WCAG,” https://www.w3.org/WAI/standards-guidelines/wcag/ ↩

  37. W3C WAI, “ARIA Authoring Practices Guide,” https://www.w3.org/WAI/ARIA/apg/ ↩ ↩2

  38. W3C WAI, “ARIA APG Patterns,” https://www.w3.org/WAI/ARIA/apg/patterns/ ↩ ↩2

  39. Playwright, “Accessibility testing,” https://playwright.dev/docs/accessibility-testing ↩

  40. Storybook, “Accessibility testing,” https://storybook.js.org/docs/writing-tests/accessibility-testing ↩

  41. Storybook, https://storybook.js.org/ ↩ ↩2

  42. Playwright, https://playwright.dev/ ↩ ↩2

  43. Playwright, “Best Practices,” https://playwright.dev/docs/best-practices ↩

  44. W3C Design Tokens Community Group, “Design Tokens specification reaches first stable version,” Oct. 28, 2025, https://www.w3.org/community/design-tokens/2025/10/28/design-tokens-specification-reaches-first-stable-version/ ↩

  45. Design Tokens Community Group, “Design Tokens Format Module 2025.10,” https://www.designtokens.org/TR/2025.10/format/ ↩

  46. Design Tokens Community Group, “Technical Reports 2025.10,” https://www.designtokens.org/TR/2025.10/ ↩

  47. Figma Developers, “Code Connect,” https://developers.figma.com/docs/code-connect/ ↩

  48. Figma Developers, “HTML / Web Components Code Connect,” https://developers.figma.com/docs/code-connect/html/ ↩

Useful to your team?

Share on LinkedIn