Case study · Design systems · Platform

From a developer-run product to a system the whole team builds on.

PriceLabs grew on vanilla JS and CSS with a single designer, so most UI decisions were made in code. After repeated inconsistency issues, we moved the product to React and built a design system to go with it: foundations, components, patterns and a voice for the product.

Role
Lead designer for the system
Team
Shail, Kaustubh, Faraz (front end)
Timeline
2024 – 2025, ongoing
Status
In use · extended to mobile
PriceLabs DSL documentation in Storybook: the Popover component introduction with its anatomy labelled: header, dismiss, body text, slot, tertiary, secondary and primary actions.
A page from PriceLabs DSL in Storybook: every component starts with a labelled anatomy, and those names become props in code.
Outcome
38documented components and patternsone template, Figma + Storybook
100%of product areas adopted itthrough the React migration
↓ 40%UI inconsistency bugssix months after migration
22mobile componentsthe app built on the same system
Part 1

Why PriceLabs needed a system

A fast-growing product built screen by screen in code had quietly become five products that looked alike. Before designing anything, we had to prove it.

01 · Context

A product built screen by screen in code, where the same button looked and behaved differently on every page.

For years PriceLabs had one designer and a product written in vanilla JavaScript and CSS. Engineers often built UI directly from requirements, so the product was largely developer-run. That got features out fast, but every page solved the same problems in its own way.

As the product and team grew, the cost showed up as repeated inconsistency issues: different buttons, inputs and spacing on every screen, and behaviour that users had to relearn page by page. One example: the calendar, customizations and settings pages each had their own “Save” button, with three heights, two reds and different disabled states.

The engineering team decided to move the front end to React. That was the moment to build the system, not after.

02 · Starting with an audit

We inventoried every button, input and colour before designing anything new.

Before designing anything new, we inventoried what already existed: screenshotting every instance of each UI element across the product and grouping them.

17button styles found
24text colours / greys
11input & dropdown variants
14font sizes in use
The buttons audit board in Figma: screenshots of every button across PriceLabs grouped by page, with blue notes on each inconsistency.
The buttons audit, from the Figma file. Every button in the product, screenshotted in context and grouped by page, with a note on each inconsistency. Click any image to zoom in.
Audit cluster: settings and listing tables with mixed button styles and notes.
Settings and tables: four styles for the same “save” action.
Audit cluster: Market Dashboard, export and comp-set screens with notes on hover, active and hyperlink button states.
Market Dashboard: hover, active and link-style buttons that behaved differently.
Audit cluster: report and banner screens with inconsistent call-to-action buttons.
Reports and banners: ad hoc calls to action.

The audit did two jobs: it showed engineering and leadership the scale of the problem, and it told us which components to build first, the ones used most and broken most.

03 · Principles

We aligned on a few principles to settle debates quickly.

Clarity over density
  • Revenue managers live in data, so the UI should make numbers easy to scan, not decorate them
Consistent by default
  • The easiest thing for an engineer to build is the right thing
Accessible from day one
  • Contrast, focus and keyboard behaviour are built into components, not bolted on
Part 2

Building the foundations

With the problem on the table, we built from the smallest decisions up: tokens first, then components, then the documentation that made them usable without asking a designer.

04 · The system at a glance

Four layers, each built only from the one below it.

Before drawing a single component we agreed on the shape of the system. Foundations hold every raw decision; components are built only from tokens; patterns are built only from components; product pages are built from patterns. If a screen needs a hex value or a one-off margin, something is missing from a lower layer, and that gap becomes a request instead of a workaround.

System map of the PriceLabs DSL: four stacked layers (foundations, components, patterns, product) with the Figma library and Storybook kept in sync on the left, and web, mobile and theme layers on the right.
The system map. Figma and Storybook mirror each other; the web, mobile and theme layers reuse the same stack instead of forking it.

04 · Foundations & tokens

Seven type tiers, including a high-density tier for data, and five colour groups.

Tokens are the named decisions (colour, type, spacing, radius, elevation) that designers and engineers share. Defining them first meant every component inherited the same choices.

xlHeading48Revenue
heading32Portfolio analytics
heading124Pricing calendar
heading218Applied customizations
bodyText14 · Regular / SemiboldBase price is the foundation of your pricing.
bodyText / Small12Last synced 7 hours ago
bodyText / HighDensity11$524 · 5 nights · Booked — for dense tables and calendar cells

A dedicated high-density 11px tier exists because revenue managers read calendars and tables with hundreds of cells; icon sizes (Font Awesome) are matched to each type tier: 18 / 14 / 12 / 11.

brandred · black · white · marketing
greysecondary · disabled · border · background
feedbacksurface · accent for success, warning, error
calendardemand levels for the pricing calendar
badgestatus and count badges
effectsCard · Top shadow · Bottom shadow
Colour

Five style groups: brand, grey, feedback, calendar and badge

Typography

Seven tiers from 48 to 11, including a high-density tier for data

Spacing

A 4px base scale: 4, 8, 12, 16, 24, 32, 48 (--pl-space-100 to --pl-space-1200), documented on the Padding & Margin page.

Radius & elevation

Three effect styles (card, top and bottom shadow) and four radii: 4 (inputs, chips), 8 (cards, panels), 12 (modals) and full (pills, toggles).

Naming

pl-[category]-[property]-[variant]-[state], for example pl-color-action-primary-hover or pl-type-body-highdensity. The same names appear in Figma variables and in code.

Accessibility

Every component page has an accessibility section: screen-reader-readable text, a minimum 3:1 contrast checked with the Contrast plugin, and labels that match visible text

Three tiers of tokens

Early tokens were just named colours, so changing “the red” meant hunting through every component that used it. We split tokens into three tiers. Primitives hold the palette and scales. Semantic tokens say what a value is for (color-action-primary, color-border-default). Component tokens are the only thing a component reads.

Token tiers: primitive tokens such as red-600 #F37579 and grey-900 #333333 map to semantic tokens such as color-action-primary, each with a light and dark value, which map to component tokens such as button-primary-bg and input-border.
Primitive → semantic → component. Rebranding edits a primitive; dark mode remaps semantic tokens; a one-off fix overrides a component token, never a hex value.

That middle tier is what makes theming possible: the dark-mode work on buttons and split buttons only remaps semantic tokens, and no component code changes.

05 · Components & variants

38 documented components and patterns, each with anatomy and usage rules.

We started with the basics that appear on almost every screen, and designed each with a clear anatomy, variants, sizes and states. The library grew to 38 documented components and patterns, each signed off in Figma before it was built:

ButtonsIcon ButtonsToggle ButtonsText FieldsSearch FieldDropdownMultiselectRadio / CheckboxSwitchSliderChips / TagsSelection CardsModalModal IconRight PanelPop OverMenuTooltipToastersInfo BoxTabsVertical TabsExpansion PanelPaginationTableStatsWidgetSlotMapsSkeleton LoaderSpinnersEmpty State404ShadowsPadding & MarginStylesGlobal LayoutsMobile LayoutsDate PickerFormsPop UpBanner

documented and shipped   in progress

Button variant matrix: primary, secondary, tertiary, ghost and link styles in three sizes and states from enabled to disabled, plus a blue onboarding variant.
The button family in PriceLabs DSL: emphasis levels × three sizes × states.
The library, page by pageOpening frame of each page in the Figma file · 38 documented ✅ · 4 in progress · plus foundations
404 documentation page
404
Buttons documentation page
Buttons
Dropdown documentation page
Dropdown
Expansion Panel documentation page
Expansion Panel
Global Layouts documentation page
Global Layouts
Maps documentation page
Maps
Menu documentation page
Menu
Mobile Layouts documentation page
Mobile Layouts
Multiselect documentation page
Multiselect
Padding & Margin documentation page
Padding & Margin
Pop Over documentation page
Pop Over
Radio / Checkbox documentation page
Radio / Checkbox
Skeleton Loader documentation page
Skeleton Loader
Slot documentation page
Slot
Spinners documentation page
Spinners
Styles documentation page
Styles
Toggle Buttons documentation page
Toggle Buttons
Widget documentation page
Widget
Dashboards documentation page
Dashboards
Icon Styles documentation page
Icon Styles
Typography documentation page
Typography
Banner documentation page
BannerIn progress
Date Picker documentation page
Date PickerIn progress
First wave
  • Buttons
  • Inputs & form fields
  • Dropdowns / selects
  • Checkboxes, radios, toggles
  • Text fields, search, tooltips and modals
Second wave
  • Right panel
  • Links & icon buttons
  • Checkbox & radio groups
  • Tabs, chips, toasters, pagination and tables
Specialised
  • Pricing-calendar cells, maps, stats and widgets, slots, selection cards

Every component got an Introduction page (what it is, its anatomy) and a Usage guidelines page (when and how to use it) in Storybook.

Button anatomy: leading icon, label, trailing icon and height.
Button: leading icon, label, trailing icon and a fixed height per size. Links and icon buttons live in the same family.
Checkbox anatomy: group label, checkbox base, checkbox label, help text for a checkbox and for the group.
Checkbox & group: named parts (base, label, help text) map one-to-one to props in code.

06 · One documentation template

Every component page answers the same five questions, so engineers know where to look.

Each of the 38 component pages in Figma follows one template, headed PriceLabs DSL / Documentation and linked to the matching Storybook page. Here is the Expansion Panel, exactly as documented:

  1. IntroductionWhat it is and its anatomy, with every part named
  2. VariationsEvery property combination, labelled with its prop values
  3. Usage guidelinesWhen to use it, limits, and content rules
  4. PlatformsWidths and rules for desktop and mobile
  5. AccessibilityKeyboard, screen-reader and contrast requirements
Expansion Panel introduction page: anatomy with title, panel overview as chips or text, leading visual, trailing icon, content slot and accordion item.
1 · Introduction. The anatomy names every part (leading visual, title, overview as chips or text, trailing icon, content slot), and those names become props in code.
Expansion Panel variations: leading none, leading icon or number, description as text, description as chips, expanded.
2 · Variations. Each variant is labelled with the props that produce it, for example leading = True, description = Chips, expanded = True.
Expansion Panel usage guidelines: when to use, how many items can be expanded, the 12-section limit, title length limits and title wrapping.
3 · Usage guidelines. Concrete limits: fewer than 12 sections, titles no longer than 2 lines on desktop and 3 on mobile, and no manual line breaks.
Expansion Panel platform page: desktop use with minimum 400px and maximum 800px width, and mobile use up to 320px or the viewport width.
4 · Platforms. Desktop: 400 to 800px wide. Mobile: up to the viewport width, with the same anatomy.
Banner variant of the expansion panel used for mapping rules: collapsed, expanded, with a menu, dismiss or action button.
Built for real screens. The same component as an information banner for mapping rules: collapsed, expanded, with a menu, a dismiss icon or an action.

The fifth part, Accessibility, was the thinnest in Figma (a heading and a one-line brief). That gap is what the spec below fills.

06 · From documentation to spec

A designer reads anatomy; an engineer needs a contract. For each component we paired the Figma page with a spec that maps one-to-one to the React component. The Expansion Panel spec:

ExpansionPanel@pricelabs/dsl · React
PropTypeDefaultNotes
titlestringrequired2 lines max on desktop, 3 on mobile; wraps automatically
overviewstring | Chip[]noneThe “panel overview”: one line of text or a row of chips
leading'none' | 'icon' | 'number''none'Leading visual from the anatomy
trailing'chevron' | 'menu' | 'dismiss' | 'action''chevron'Banner variants use menu, dismiss or action
variant'default' | 'banner''default'Banner adds the info icon and accent border
expanded / defaultExpandedbooleanfalseControlled or uncontrolled; any number can be open
onToggle(expanded) => voidnoneFires on click, Enter or Space
childrenReactNodenoneThe content slot
Statescollapsed · expanded · hover · focus-visible · disabled
Tokenspl-color-border-default, pl-color-surface-info (banner), pl-space-400 padding, pl-radius-200
Keyboard & ARIAHeader is a button with aria-expanded and aria-controls; Enter/Space toggle; focus ring 2px, 3:1 contrast
LayoutDesktop 400–800px; mobile up to viewport width; 12 panels max per group

Specs were written for this case study from the Figma documentation; prop names illustrate the contract rather than the exact production API.

06 · Tokens, from Figma to code

Tokens were the bridge. A decision made once in Figma variables flowed into code under the same name, so “change the brand red” was a one-line change.

Figma variablecolor / action / primaryspace / 400radius / 200
→
Token (JSON)"pl-color-action-primary": "#F37579""pl-space-400": "16px""pl-radius-200": "8px"
→
CSS variablevar(--pl-color-action-primary)var(--pl-space-400)var(--pl-radius-200)
→
Component<Button variant="primary"><ExpansionPanel><Card>

The mobile app added a pl-m-* layer on top (48px controls, 32px chips), so the same names served both platforms.

Part 3

Making it behave the same everywhere

Components alone don’t make a product consistent. How they combine, and how the product speaks, had to be designed too.

06 · Patterns & tone of voice

Components alone don’t make a product consistent; how they combine does. We documented interaction patterns and a product voice alongside them.

Action rules
  • One primary button per page
  • Primary sits on the right, outermost; secondary actions to its left
  • Groups: one primary plus same-emphasis secondaries
  • Buttons perform actions; links navigate
  • Blue is reserved for onboarding
Tone of voice
  • Clear, not clever
  • Confident, not pushy
  • Expert, without jargon: say “minimum stay”, not “LOS restriction”
Writing rules
  • No title case on buttons
  • Icons only where text alone isn’t enough, and never on both sides of a label
  • “Save & Refresh Prices Now!” → “Save and refresh”
  • “Invalid input” → “Minimum price must be lower than base price”

07 · Example: the right panel

Guidelines covered behaviour, not just parts.

Product patterns were documented the same way as components. The right panel shows how far the guidelines went: not just the parts, but the behaviour.

Right panel anatomy: back button, title, description, close, content slot and footer with secondary and primary actions.
Anatomy: back, title, description, close, a flexible content slot, and a footer with secondary and primary actions.
Right panel usage guidelines: contextual actions, compact and focused, open and close behaviour, responsiveness.
Usage guidelines for context, width, behaviour and responsiveness.
  • Contextual, secondary work only: details or settings for a selected item, with the main page still visible under a 50% overlay
  • Deliberate closing: Esc and outside clicks don’t close it, so a half-edited form can’t be lost by accident; a discard action makes the exit explicit
  • Responsive by rule: on small screens it becomes a bottom sheet, the same decision that later shaped the mobile app

08 · Accessibility, checked at the token level

Test the token pair once, and the result holds everywhere it’s used.

Screen-by-screen accessibility reviews never finish in a product this size. Instead we checked contrast on token pairs against WCAG 2.2 AA, so a pass or a fail applies to every place that pair appears. Ratios below are calculated from the production hex values.

Accessibility board: eight token pairs with contrast ratios, for example 12.6:1 for #333 on white (AA), 2.75:1 for white on brand coral #F37579 (fails), 4.6:1 for dark text on coral (AA) and 5.4:1 for red-900 as brand text; plus rules for focus rings, target sizes, never relying on colour alone, and keyboard and ARIA.
The palette check. Body text passes comfortably; the brand coral and the status green don’t carry white or small text.
  • The brand colour was the biggest catch. White text on coral #F37579 is 2.75:1, below even the 3:1 minimum for large text. Coral stays a fill and an accent; text and links that need to read as “brand” use red-900 (5.4:1).
  • Status colours never work alone. Green #31C48D is 2.2:1 on white, so success, warning and error always pair colour with an icon and a label, including the demand colours in the pricing calendar.
  • Secondary grey is for supporting text only. #7A7A7A is 4.3:1: fine for timestamps and helper text at 14px+, not for anything a user must read to act.
  • Every component page carries the same checklist: visible 2px focus ring, 32px minimum targets on web and 48px on mobile, keyboard map, and ARIA roles that match the Figma property names.
Part 4

Shipping it, and keeping it alive

A system only counts once it’s in the code. We migrated page by page, then audited real usage to stop it drifting.

08 · Migrating to React

We got a dedicated engineer for the migration. Design and engineering worked side by side: each component was designed, built in React, reviewed for parity and then rolled into pages.

  1. Page by page, not big-bang. The highest-traffic pages (pricing calendar, dashboard, customizations) went first.
  2. Component before page. Each component was built in React and checked in Storybook before any page used it.
  3. Parity review. Old and new screens were compared side by side with a checklist (spacing, states, keyboard, copy) before release.
  4. Swap and delete. Once a page shipped, its old CSS was removed so the two systems never lived side by side for long.
The same settings form before the design system, with mixed buttons, greys and spacing, and after, built from DSL components and tokens.
Same form, before and after. One button hierarchy, one grey scale, one spacing scale. (Illustrative recreation.)

09 · Scaling the system

An audit of real code usage kept the system honest after launch.

After the migration we kept growing the library, and the same foundations later carried the mobile app: mobile variants on existing components instead of a separate library.

The system lives in two places that mirror each other: the Figma library for design, and Storybook (PriceLabs DSL) as the documented source of truth for engineering, with introduction and usage pages for each component.

Full button usage guidelines page with do and don't examples for width, placement, icons, variants, groups, buttons versus links and colours.
A full usage-guidelines page: dos and don’ts for width, placement, icons, variants, groups, buttons vs links, and colour.

A status page before anyone builds a duplicate

The most common way a system fails is quietly: a squad can’t tell whether a component exists or is ready, so it builds its own. We added a status page that tracks every component across Figma, React, Storybook, documentation, accessibility review and mobile, updated with each release.

Component status page: a table of components such as Button (96 variants), Icon button, Dropdown, Checkbox, Expansion panel, Right panel, Date picker, Table, Toaster, Skeleton loader, Bulk actions bar and Split button, each marked Stable, Beta or Planned with readiness in Figma, React, Storybook, docs, accessibility review and mobile.
Stable, beta or planned, per component and per surface. Gaps found in the code audit (like the bulk actions bar) show up here as beta work, not as hidden debt.

Driving adoption, and measuring it

Make it easy to find
  • Status page and Storybook as the single place to check
  • Release notes to every squad with each version
  • A short demo when a new component lands
Make it easy to ask
  • Weekly design-system sync for requests and reviews
  • A shared Slack channel for “does this exist?” questions
  • Design reviews check for detached or one-off components
Measure what’s used
  • Code audits of real usage, starting with buttons
  • Figma library analytics: inserts and detaches per component
  • UI inconsistency bugs tagged in QA, tracked over time

Auditing the system in code

A system drifts once it’s in engineers’ hands. In August 2025 we audited how buttons were actually used across the React codebase, screenshotting each case and tracing it to its file, then resolving every one against the guidelines.

FindingWhereResolution
Bulk actions built as buttons with variant='none'TableBulkOperationsOverlay, MinStaySettingsModalA dedicated bulk-actions component instead of an escape hatch
Buttons too large (48px) in dense tabsPmsOtaTabsSize down to the 32px / 40px variants
“Go” button breaking on a global min-widthSearch inputsSize rule fixed for inline actions
Unclear variant for “Create dashboard”, “Change connect”, Vrbo integrationGetDashboardModal, IntegrationVrboMapped to primary / secondary by emphasis rules
Marketing CTAs (e.g. Subscribe now) styled ad hocTrailBannerAligned to the marketing button styles

Reviews happened in Figma comments with our design manager, and the same process now extends to dark-mode variants for buttons and split buttons. Design owns the Figma library and the documentation; engineering owns Storybook and the React package. Anyone can propose a component, and every change goes through the same path:

  1. RequestA template: the problem, where it appears, and why existing components don’t fit
  2. ReviewWeekly design-system sync: reuse, extend or create
  3. DesignAnatomy, variants, states, platforms and accessibility in Figma
  4. BuildReact component and Storybook story, with props matching Figma
  5. ReleaseVersioned, with a changelog note to every squad

10 · Impact

38documented components & patterns
100%of product areas adopted itthrough the React migration
22mobile componentsthe app’s layer on the same system
↓ 40%UI inconsistency bugsreported in QA, six months after migration
  • Faster buildsNew pages are assembled from components, so design QA focuses on flows instead of pixels.
  • A shared languagePMs, designers and engineers name the same things the same way, from Figma to Storybook to the codebase.
  • Ready for the next platformThe mobile app reused the same tokens and component logic instead of starting over.

Evolution

A design system is never done. How it grew after v1.

  1. v12024Foundations

    Tokens + core components for the React migration

    Why
    Repeated inconsistency in a developer-run product
    What shipped
    Tokens, basic components and variants, patterns, tone of voice
    Result
    Core pages migrated to React on shared components
  2. v22025Expansion

    Second-wave and product-specific components

    Why
    Squads building analytics and settings needed panels, groups and denser tables
    What shipped
    Right panel, links & icon buttons, checkbox and radio groups, documented in Storybook, plus tables, pagination, toasters and skeleton loaders
    Result
    38 documented components and patterns
  3. v32026Mobile

    Mobile variants for the app

    Why
    The mobile app needed touch-friendly components without a second library
    What shipped
    22 mobile components: 48px controls, 32px chips, AAA/AA contrast, mobile tokens
    Result
    App launched Jul 2026 on the same system