Design System for Yara International
Design System for Yara International
| Company | Yara International |
| Industry | Industrial agriculture, AgriTech |
| Year | 2023 – 2024 |
| Role | Design System Architect, Lead Design System Engineer, Engineering Manager |
| Team | Bridge the Gap — Varya Stepanova, Irina Samoylova, Andriy Kotko, and Alexandra Urberg — partnering with Yara's design-system lead, in-house designers, an in-house UX designer focused on engineer experience, in-house and consultant React Native engineers, and product engineers across multiple Yara product teams |
| Stack | React, React Native, TypeScript, Stitches, NX monorepo, Storybook (6 → 7), Jest, React Testing Library, axe-core, Chromatic, Detox |
My role
I joined as design-system architect, lead engineer, and engineering manager on the Bridge the Gap side. The work was hands-on and managerial at once. Yara's design-system lead managed me directly. I set the architecture — the token system, the compound-component pattern, the Storybook direction — and I wrote a large part of it. I also ran the team: roles and ownership, 1:1s, professional development, sprint planning and retrospectives, and hiring and onboarding. The four of us were distributed across six countries.
Adoption was voluntary. A standing part of the role was design-system coaching and governance, so product teams would choose Ahua rather than be told to. That meant agreeing the token vocabulary with the design leads, cross-company demos, the Slack changelog after each sprint, and the day-to-day conversations with product engineers. I was the person aligning those engineering decisions with product and with leadership.
Executive summary
Yara International — the multinational crop-nutrient and digital-agriculture company — runs a multi-product UI surface on top of an internal design system called Ahua, shipped as two production libraries (React for web, React Native for mobile). The product views are data-heavy: long forms, tables, and operational screens. When we joined, the system was visible to product engineers, but the developer experience was uneven: Storybook 6 with hand-authored MDX1 stories, no shared composition pattern across compound components, parallel token systems for web and React Native that drifted from each other, and a documentation surface that didn't quite reflect how product engineers actually used it.
Over six months, Bridge the Gap (my consultancy team) partnered with Yara's design-system lead, the in-house designers, the in-house UX designer, and engineers across the wider product organisation. As the design-system architect on our side, I led the technical and documentation direction across the engagement. Together with my team, we shipped 10 new components on a shared compound-component pattern, brought 20+ legacy components onto a single dotted-display-name convention, migrated both libraries from Storybook 6 to 7, and rebuilt the Storybook surface — driven by user research with Yara product engineers — into a branded, scannable documentation experience. We also designed and delivered the new tier-based token system: vocabulary, naming convention, Figma re-organisation, and the cross-library codegen plan.
The end result is a design system that product teams reach for first, that has consistent component APIs across the catalogue, and whose Storybook pages designers and engineers navigate together.
Context — what we walked into
Ahua lives in an NX monorepo. At engagement start it housed:
libs/ahua-design-system-react— the React web library. Stitches-based theming, ~50 components, Storybook 6 with hand-written.stories.mdxper component.libs/ahua-design-system-react-native— the React Native library. A parallelGlobalTokensset and a Storybook 6 setup hybridised with@storybook/addon-react-native-webso RN components could render in a web Storybook.libs/ahua-design-system-common— a tiny shared utilities package.apps/react-vite-app/apps/react-native-app— playground apps for trying components in a real React/RN context (rather than just inside Storybook).
The libraries worked, but a closer audit surfaced four recurring patterns:
- Compound components without dotted display names. Components like
Card.Headerrendered as plainHeaderin Storybook code snippets — Storybook reads the runtime function name, not the dotted path. Across the catalogue this was inconsistent; every doc page that showed a code snippet had to be read defensively. - Two parallel token sources of truth. A change to font sizes meant editing the web's Stitches config and the RN's
GlobalTokens.tsindependently, with no automation to keep them aligned. Drift was already visible. - Storybook 6 with MDX1 stories. Story authoring was inconsistent (
.stories.mdxwith embedded JSX), and Storybook 7 — which the wider ecosystem was moving onto — used a different story format. Without migration, the gap would only widen. - Documentation that didn't help product engineers. The default Storybook chrome wasn't the surface engineers wanted to scan when they came to pick a component. Tokens and component APIs lived in formats that designers and engineers consulted in different mental models.
These weren't crises — they were the friction patterns that slow down a healthy product team. Yara's design-system lead asked us to take ownership of the foundation while their team kept shipping product.
How we worked
Spec-driven definition before code
I work with a method I call ultra-defined components — closely related to spec-driven development, but with a stronger emphasis on the human side of the conversation.
Before a single line of component code is written, I produce a written spec that lists everything the component does: the interface (props, sub-components), accessibility behaviour, edge cases, and a natural-language test plan ("the dialog closes on overlay click unless it's modal"). The spec is then walked through together — designer, lead, and developers — until the disagreements surface and resolve upfront.
Two things make this method work:
- The spec is written for humans first. A designer who never reads TypeScript can argue with a sentence like "the dialog closes on overlay click unless it's modal." They cannot argue with a type signature. So the discussion stays where the disagreements actually live: behaviour, not syntax.
- The test plan precedes implementation. Once the spec is signed off, the test list often becomes the test file almost verbatim. The component itself is then frequently a no-op for the developer — sometimes Copilot finishes it on the first try.
I wrote up the method publicly on the Bridge the Gap blog: Define your rock-solid design system components.
Embedded across two libraries
We worked as one team across the engagement. Bridge the Gap brought four engineers (including me) for six months. On the Yara side we partnered closely with the design-system lead — who managed me directly across the engagement — the in-house designers, the in-house UX designer focused on engineer experience, and the in-house and consultant engineers shipping on the React Native side.
The libraries are independent codebases but are scoped together: every architectural decision (composition pattern, story format, custom Storybook UI, documentation pages) had to land symmetrically in both. A weekly sync held the cadence; quarterly reviews kept the bigger arc in focus.
Sprint-end demos and a Slack changelog
We ran sprint-end demos throughout the engagement: the team would walk through what shipped, what was in review, and what the next sprint was tackling, with product engineers in the room. After each demo, I wrote a short Slack changelog — what's new, what's renamed, what's deprecated — into the design-system channel. That became the regular product-team-facing surface. Daily support stayed in Slack and on calls: a product engineer hitting friction with a component could ask, get a same-day answer, and that question often became a docs improvement or a small fix the next sprint.
Design-system evangelism
The system is Yara's, owned in-house. My role on the BTG side was as much about making the design system visible across the wider Yara engineering organisation as it was about building it. I was in continuous contact with product engineers — in-house and consultant — who were adopting components, asking about coverage, or hitting the gaps that hadn't been built yet. Every Slack thread or call was a feedback channel, and that feedback fed directly into what we prioritised.
Signature work
A new tier-based token system
Tokens are the cheapest thing to get wrong in a design system. They look like a plumbing concern, but they decide whether recolouring the brand is one PR or twenty, whether designers and developers describe the same colour with the same words, and whether dark mode is a mode of the same system or a fork of half the catalogue.
Ahua had two parallel token sources of truth — Stitches theme.* for web, GlobalTokens.ts constants for React Native — with no automation between them. And inside the web config, all three abstraction levels (global, semantic, component) lived flat in the same theme.colors lookup, distinguished only by their kebab-cased prefixes:
// theme.colors — three tiers in one namespace, two naming grammars
blue50: 'hsla(208, 92%, 45%, 1)', // global
'brand-base': '$blue50', // semantic
'fill-action-primary-brand-enabled': '$brand-dark', // component
'fill-action-primary-brand-hover': '$brand-dark',
'fill-action-primary-brand-pressed': '$brand-darker',Three things wrong here, all visible from the snippet:
- One namespace, two grammars. Globals are
blue50(no separator). Semantics and components are kebab-case (brand-base,fill-action-primary-brand-enabled). Autocomplete, grep, and IDE refactors all suffer. - No tier boundary. A product engineer can write
color: '$blue50'directly, bypassing the brand layer that is meant to mediate every brand-coloured surface. The system's promise of "edit one token, change everywhere" only holds if the tiers are enforced. - No cross-platform translation. A token added to Stitches is invisible to React Native; a token added to
GlobalTokens.tsdoesn't propagate to the web library or to Figma. Designers have a separate vocabulary again. Translation is manual, slow, and silently divergent.
The new Ahua token system fixes all three by making the tiers explicit and the source single.
Three tiers, three namespaces
Every token lives in exactly one tier, and the tier is visible in its name. The runtime prefix is ahua. (the system's identity), then a tier marker, then the rest:
| Tier | What it holds | Example |
|---|---|---|
| Global (primitives) | The palette, the type scale, the spacing ramp — values with no semantic meaning | ahua.color.global.blue.50 |
| Semantic (concept) | What the value means in the system: brand, destructive, success, neutral | ahua.color.concept.brand.base |
| Component / Group | What the value does inside a component or family: input border, action fill, form-controls hover | ahua.input.border.focus / ahua.formControls.border.focus |
Each tier references the tier above it. Component tokens never reach globals directly — they go through semantic. So a brand-colour change ripples one way: edit ahua.color.global.blue.50, every semantic token that references it updates, every component token that references that updates. One edit, full reach.
Naming convention
Every token name is built from the same six positional parts. Earlier slots are the broadest (system, category); later slots get more specific (entity, state, value).
Each slot fills a role: DS is the system identity prefix (ahua); Category is the type (color, space, fontSize, radius, or a platform override like android / ios); Group is the context (global, concept, input, action, formControls, …); Entity is the named property inside the group (brand, border, borderWidth); Modifier is the state or sub-quality (base, hover, focus, light, …); ID is the leaf identifier on globals (e.g. a ramp step like 50).
Not every token uses every slot — globals skip Modifier; component-tier tokens skip Category and ID. The grammar is the same at every tier; only the slots that are filled change:
| DS | Category | Group | Entity | Modifier | ID | Result |
|---|---|---|---|---|---|---|
ahua | color | global | blue | — | 50 | ahua.color.global.blue.50 |
ahua | color | concept | brand | base | — | ahua.color.concept.brand.base |
ahua | — | input | border | focus | — | ahua.input.border.focus |
ahua | — | formControls | border | focus | — | ahua.formControls.border.focus |
ahua | — | formControls | borderWidth |