Fieldwire by Hilti

A design system for the field

iOS, Android, Design System • 2024
The list component in a real Fieldwire screen, Android and iOS, in light and dark mode

Summary

Fieldwire is construction-management software for the people who run job sites, and part of Hilti since its acquisition. Its iOS and Android apps needed a mobile design system, and I built one backwards from production. I started from what the apps actually shipped, found the inconsistencies, and resolved each one into a single token, style or component in Figma. Color and type are variables with modes: light and dark for color, and for type the apps' native fonts, SF Pro and Roboto, plus Lato, the brand font the apps were meant to move to. Once the system was complete, every designer used it as their default. The engineers began fixing inconsistencies across the apps as soon as they found them, without being asked.

Team

Principal Product Designer (me), Head of Design (review and sign-off), design team (critique)

Role

Design System, Product Design

Scope

Figma design system library
Fieldwire's iOS and Android apps
23 components, 386 variants
12 renderings each: 2 platforms × 2 modes × 3 fonts
27 primitives, 62 color tokens
74 production screenshots audited
April to July 2024

The Problem

The apps in people's hands were the truth, and nothing in the design files held all of it. A new screen started with a designer looking for a kit. The kit rarely matched what production used, so neither did the screen.

Insights
  1. Production carried inconsistencies and one-off styles and components.
  2. Designers had to find a kit for native iOS and Android controls each time, so their screens were inconsistent with the app.
  3. Two platforms, each with its own native font and type scale, plus a brand font and two color modes, make too many combinations to keep in sync by hand.

So the system had to start from what the apps already were, not from a blank file. The file sets out three goals.

Goals

  • Efficiency. Speed up designers, developers and QA.
  • Accuracy. Improve the end product with more pixel-perfect designs.
  • Focus. By making the process more efficient and accurate, spend less time on trivial UI issues and more on designing and validating the right thing.

Working Backwards From Production

I started from the shipped apps rather than a blank file. For each component, I screenshotted every place it appeared in the Android and iOS apps and laid them side by side: 74 screens across five components: lists, inputs, the top and bottom app bars, and search. Then I marked each one: keep it, add what's missing (a count, a chevron), or question it when I couldn't tell where it was used. What survived became a single component. I built the system on my own and brought the work in progress to the design team's critique sessions as it took shape.

The option I didn't take was the more common one: design an ideal system and then ask engineering to migrate to it. Starting from production meant the system was true on the first day, and every fix after that brought the apps closer to it.

Production screenshots of every list in the Fieldwire apps, with each row type marked keep, add or question
Every list in the shipped apps, marked keep, add or question

Color: From Primitives to Tokens

Color got the most structure. It is built in two layers of variables, primitives and tokens, and the names do the enforcing.

Primitives hold the palette. Every official color is a primitive, named for what it is rather than where it goes: blue-01, red-01, gray-01 to gray-10, and a set of accent colors. The palette frame says it plainly: official colors, use as-is.

Tokens say where a color is used. 62 tokens sit on 27 primitives. A token points at a primitive and names its job. General tokens cover the whole interface: text, fill, border and icon, such as color/general/text/primary or color/general/border/default. Component tokens follow one structure, color › component › property › variant › state, so color/button/fill/primary/default reads the same way to a designer in Figma as it does to an engineer in code.

One rule decides which to use. General tokens come first. A component-specific token is created only when the general ones are not enough, and a custom color only where an exception is unavoidable, like the iOS-only segmented button. That keeps the token list short and every new token deliberate.

Light and dark are two modes of the same tokens. A designer or engineer who uses color/general/text/primary gets the right value in either mode without knowing what it is.

The alternative was one flat list of named colors used directly in components. That works until a color changes, and then every screen has to be found and fixed by hand.

The Colors foundations frame: the official palette of blues, reds, grays and accent colors
The primitives: the official palette, used as-is
The color-token rule and the naming structure: color, component, property, variant, state
How a token is named, from color down to state
The Figma variables panel: the Tokens collection, each token aliased to a primitive, with light and dark values
Tokens aliased to primitives, with a light and a dark value each

Type and Font Modes

All 47 text styles are built from variables, 13 for Android and 34 for iOS. Size, line height, letter spacing and weight are each a variable, 81 in all, and each platform follows its own type scale: Android from H1 down to the overline, iOS from Large Title down to Caption 2. A single font-family variable feeds every style, and its modes switch the whole type system between Roboto on Android, SF Pro on iOS and Lato.

Lato was the reason for the font modes. The apps used the system fonts, but the plan was to move them to Lato, Fieldwire's brand font. I set up the font mode so that the move would be easy when it came. Every component can be rendered on either platform, in light or dark, in any font, by switching modes rather than redrawing a screen. The system was ready for a decision the product hadn't made yet.

The alternative was a copy of the type styles for each font, and a Lato version to be built when the migration was approved. Copies have to be kept in sync by hand, and that is how a system drifts.

The same Android list in full-width and grouped styles, in light and dark, with native fonts and with Lato
One list, eight renderings, no redrawing
The Typography foundations frame: Roboto and SF Pro as native fonts, and Lato as the brand font
Two native fonts and a brand font, driven by one font variable

Native Where Native Ships

Where the apps used the operating system's own controls, the system says so. The system page holds the iOS and Android defaults production used: status and navigation bars, keyboards, and date and time pickers, in light and dark. A designer places exactly what the app shows, instead of finding a platform kit each time and producing a screen that doesn't match. When a platform updates, those components can be swapped out in one place.

Elevation follows the same rule. The app is flat unless a component says otherwise, and where there is elevation, it comes from the platforms' own libraries: Material 3 for Android and Apple's iOS 17 kit.

The alternative was to redraw native controls as custom components. That looks tidier in the file, but it hands designers a version of iOS that doesn't exist.

Native date and time pickers for Android and iOS, in light and dark
Native pickers, Android and iOS, light and dark

Elements to Components

Small elements, such as the icon slot, the status chip and the logo, build into 23 components, and those carry 386 variants across Android and iOS. Every component uses auto-layout, so it resizes with its content, and many keep their building blocks alongside them: the rows of an action sheet, menu items, tabs and page-control dots. The largest are the list, with 116 variants, and the input, with 104.

One component per platform. My first attempt kept iOS and Android in a single component with a platform switch, so the two could share properties and styles. It didn't hold up: the platforms don't have the same properties (the Android list row has an optional icon, the iOS one doesn't). So I split them by platform from the start. It is easier to pick the right one from the assets panel, and it takes fewer clicks.

The list is where the unifying work shows most. Production had list rows that differed in color, spacing, type and layout from screen to screen. I brought them onto the same tokens, type styles and spacing, which gave 56 variants per platform: every style, type and position. The newer list sets reduce that to two, assembled from building blocks. Fewer, composable components are easier to keep consistent than one variant for every combination.

The icon library sits alongside, with Fieldwire's own construction icons, such as plans, RFIs, submittals and task status, next to the general set.

The top app bar component: its Android and iOS variants
The top app bar component, with its Android and iOS variants
The Buttons component: Android and iOS button sets and the floating action button
Buttons, per platform
The icon library, including task-status and document-link icons
The icon library, with construction-specific icons

The Stickersheet: Components in Real Screens

A component page shows every variant a component has. It doesn't show which one a screen needs. So the stickersheet rebuilds actual Fieldwire screens from the system's components: the task and plan actions, the app bars over Tasks, Plans and Settings, the lists of forms, files and assignees. Each one is shown on Android and iOS, in light and dark.

It makes the system immediately clear in use. A designer sees how the variants come together in the product they already know, and anyone reviewing the system can check it against the app on their phone.

The alternative was a grid of every variant side by side. That proves coverage, but it leaves the designer to work out how the parts fit together.

The action sheet stickersheet: task and plan actions on Android and iOS, in light and dark
Task and plan actions, built from the action sheet component

Documentation Inside the File

At Hilti, the design system's documentation lived in Confluence, one step away from the work. For Fieldwire I kept it inside Figma. Every frame uses the same documentation components: a header with the title, a status chip and the platforms it covers, plus dividers and annotations. New components are marked as new and old variants as deprecating, so a designer can see where each part of the system stands.

The tooltip guideline shows the form a guideline takes. It covers when to use a tooltip: truncated text, a decision that needs more information, a term that needs defining, an icon-only button. It shows the single-line and multi-line types and the anatomy. It then sets the behavior: the tooltip appears 4px above its element on tap and hold, disappears after 1.5 seconds, and closes any other open tooltip.

The tooltip guideline: when to use, types, anatomy and behavior
The tooltip guideline, documented in the file

Where It Ended

The system became every designer's default. The engineers took it on as their own. When one of them found an inconsistency, or a one-off style or component, they fixed it across the apps without waiting for it to be prioritized. I handed the system to the design manager when I left for Circle Medical in July 2024.

What I'd Do Differently Now

I built this system before AI agents designed and wrote interfaces. It was made for people, and people kept it true. Today the same system would also have to guide an agent, and an agent without guidance drifts to whatever looks plausible.

Write every decision down where an agent reads it. The token rule and the tooltip guideline were written for people, on frames in Figma. I would also record each decision in a spec file when it is made, and give every variable and component a description and every variable its SwiftUI and Compose code names. Then an agent chooses from a closed set of named values instead of inventing plausible ones.

Connect the components to the code. I would use Code Connect to map each component, the native ones included, to its SwiftUI and Jetpack Compose code. Then an agent uses the real component instead of redrawing it.

Check for drift rather than instruct against it. A rules file tells an agent what to do, and a check finds out whether it did. The engineers' habit of fixing inconsistencies as they found them was drift control done by people, and today I would make it a routine. I would scan the files on a schedule for hard-coded values, text set outside the type styles, and detached components. And whenever a variable or component changed, I would flag every piece of documentation that mentions it, so that no one, person or agent, works from a rule written for an older version.

Credits

Nick Kinling: Head of Design, Fieldwire (review and sign-off)