Netcall Spark design system 05 / 05 Why is every team rebuilding the same button?
Netcall's platform teams each built their own inputs, their own focus rings, their own error text. Spark decided those once — as Figma variables, Tailwind tokens and a Vue component library in Storybook — so a new screen inherits contrast, focus and keyboard behaviour instead of re-arguing them.
Layers, Not
a Library
Foundations at the bottom, components built on those, patterns assembled from those. The order is the point: a decision taken on the bottom sheet cannot be un-taken on the top one.
A design system earns its keep the day nobody has to argue about a focus ring again.
Netcall’s platform is several products under one roof, built by several teams. Left alone, each team answers the same small questions separately — and answers them differently.
Rebuilt, Not Reused
Every team needed a button, an input, a card. Each built its own, so the platform gained variants nobody had decided on.
Hand-off By Interpretation
Design and engineering had no shared names. A spec was read, interpreted, then rebuilt a second time in a second tool.
Accessibility With No Owner
Contrast, focus and keyboard behaviour were settled per screen. Settled per screen means missed on some screens.
“Decide it once in the component, and every screen inherits it.”
— view full size Buttons and form controls — the primitives every other pattern is assembled from.
Tabs, breadcrumbs and pagination, then the task rows and approval actions built on top of them.
Status chips, alerts, validation and progress — how the platform says what just happened.
Generated summaries, confidence and citations, and a human-in-the-loop review step.
The middle sheet of that stack, in full — every family the platform draws on, each with its states already decided. Hover the markers.
Five States,
Decided Once
A text input and a primary button, stepped through the states every interactive component has to answer for. This is not a screenshot — it is the control, drawn from the same tokens the system hands a product team.
What matters is the work the reader is no longer doing. Nobody picks the hover colour, nobody decides whether the focus ring clears the border, nobody judges how red the error text should be. Each of those was measured once and put inside the component.
- surface/default/base
- border/default/strong
- content/default/primary
- surface/brand/strong
- surface/brand/base not for a filled label
Field text on surface/default/base
Button label on surface/brand/strong
Ruled out — button label on surface/brand/base
A filled button takes the strong step of the brand ramp, not the base step, because the base step carries white at 3.43:1. A product team never has to know that. They ask for a primary button.
- surface/default/hover
- border/default/strong
- surface/brand/strong-hover
Button label on surface/brand/strong-hover
Hover only ever moves down the same ramp, so a hovered control can never end up lighter than the one beside it. It also never carries meaning on its own: there is no hover on a touch device and none for a keyboard, so anything only hover says is unsaid for them.
- focus/ring/brand
- focus/ring/offset
- border/default/strong
Focus ring on surface/default/base
Two pixels of ring, two of offset, drawn outside the border so it survives a dense form. One token, applied by the component, is the whole reason a keyboard user gets the same ring on a chip, a card and a select.
Use letters, numbers and hyphens only.
- surface/danger/subtle
- border/danger/strong
- content/danger/default
Error message on surface/danger/subtle
Red is never the only carrier. The state ships with an icon and a sentence naming what to change, and that pairing lives in the component, so a screen in a hurry cannot drop half of it.
Locked while this workflow is running. Stop it to edit.
- surface/default/disabled
- border/default/subtle
- content/default/disabled
Disabled label on surface/default/disabled
The one pairing here that misses 4.5:1, and the only one WCAG exempts. The system fills the gap with its own rule instead: a disabled control always renders the reason it is disabled as ordinary text beside it.
A static recreation of the states, so nothing here takes focus away from the page. Every ratio is the computed WCAG value for the two colours the panel beside it actually paints.
Semantic colour tokens
Surfaces and interaction states
Colour primitives are named by purpose, so the system can express context consistently across product screens and component states.
Surfaces
Interaction States (Brand Primary)
That specimen is the ramp the states draw from. Names carry purpose rather than value — surface/brand/strong, not teal-700 — so the component can change colour without every screen that uses it having to change with it.
A System That Constrains Without Freezing
A system tight enough to guarantee consistency is a system teams route around. The first time someone’s pattern does not fit, they fork a component — and the fork is invisible until it is everywhere.
Too loose and it guarantees nothing. A palette and a page of guidance is a suggestion, and suggestions lose to deadlines.
So the line I drew was by kind of decision, not by amount. Fixed where a decision is an accessibility or interaction contract — focus, keyboard order, contrast, the names of the states. Open where it is composition. Teams arrange the system however their product needs. They cannot quietly redefine what focus looks like.
- The focus ring — its colour, its width and the offset that clears the border
- Keyboard order, roles, and what each state announces
- The contrast of every pairing a state can paint
- The names of the states themselves
Change it here and it changes everywhere. Nowhere else.
- Layout, grouping, and what sits beside what
- Density — which step of the spacing scale a screen takes
- Which components a screen reaches for at all
- Every word of the copy inside them
The system has no opinion, on purpose. This is what stops a fork.
A component per team
The fastest thing to ship, and the thing already happening. It costs a shared vocabulary, so every hand-off restarts the same conversation. It also costs a single place to fix an accessibility defect.
Tokens without components
Publishing the foundations alone would have been a fraction of the work. But a palette nobody is obliged to use decides nothing — it documents the inconsistency rather than ending it. What that loses is the only enforcement a design system actually has: the component.
A single big-bang migration
Replacing every control at once would have made the platform consistent on a known date. It also asks every team to stop shipping product on the same date, which nobody agrees to twice. Demoted to progressive adoption, a component at a time, at the cost of a long stretch where old and new controls share a screen.
One Definition,
Three Places
Figma variables and component properties were built on the same foundations as the Tailwind token structure, and the Vue library in Storybook is the coded counterpart. Same component name, same states, same usage rules — seen in Figma, tested in Storybook, used in the platform.
That removes two specific things. Interpretation during hand-off: no engineer is reading a spec and rebuilding it from the picture. And the ambiguity about when a pattern has earned its way into the library — if a team is drawing something the system already names, it surfaces under that name in both tools.
Interactive specimen
A working recreation of the Form Controls pattern from the SPARK component library. The controls use native behaviours, focus states and labels.
The coded controls the system supplies — native inputs, real labels, real focus behaviour. A team building a form starts here rather than at a rectangle.
— view full size An assistant that brings its own UI is a second design system.
The AI work lands inside the same platform as everything else, so the chat header, the input area, the launcher and the response card are library components with states — not a bolt-on. Which is why an AI surface inherits the same focus ring and the same error treatment as a settings form.
Adoption Is
the Result
A design system has no user-facing number standing behind it. Its result is adoption and consistency — whether teams reach for it, and whether the platform stops diverging — and both take quarters to read rather than sprints.
Where I’d Start
A design system's results are adoption and consistency, not a user-facing number — and none of it is measured yet. A starter set, to sharpen once the first teams are on it.
Screens built from the library
Share of new screens assembled from library components rather than one-offs. Adoption, stated plainly.
Duplicate components retired
And, more tellingly, whether new ones stop appearing. A system is being routed around long before adoption dips.
Designer start to running code
The hand-off is what the Figma-to-Storybook pairing exists to shorten, so it is the honest thing to time.
Where accessibility defects surface
In the component, or in a downstream audit. This is the whole argument for deciding it once, so it is the measure that settles it.

