Ten years of app, two platforms and no shared language. The system started by counting what was there before deciding anything.
ClientTextPlus
RoleProduct Designer
Year2026
StatusDelivered
01 The problem
text+ is a messaging and calling app with more than ten years on iOS and Android. It never had a formal design system: styles lived in the code, each platform did its own thing, and the inconsistencies grew with every release. The Flutter rewrite of the app was already under way when I joined.
Before defining a single token I ran a full inventory of the interface: eight flows, 216 screens across both platforms, 571 elements classified by what they do rather than how they look. Grouping by function instead of colour was the decision that made everything else make sense: two “Continue” buttons, one yellow and one green, are the same button implemented badly twice, not two components.
Frequency by variance: the most used and most inconsistent pieces are the first ones to build.The list row is the skeleton of the app: chats, calls, contacts and settings are the same piece with different content.
My role
I was the only designer on the project, in a team of three: the CEO, who set priorities; a Flutter developer, my technical counterpart on every decision; and me. Two constraints: a team that size cannot maintain a complex system, and the developer was going to build screens from it without me reviewing each one, so nothing could stay implicit.
Audit of 216 screens and 571 elements
Token architecture in three modes
Kit of 15 components with auto layout
Usage rules written with their rationale
System documentation
Handoff to the Flutter theme
02 The decisions
A system for a team of three forces you to decide few things and write them down. These six are the system’s governance; each one is documented with its rationale, so the team can argue with it instead of reinventing it.
01
Tokens describe roles, never colours
brand/action, not brand/tangerine. If the name is a colour, the name is wrong. The brand group has exactly two tokens, action and emphasis, and does not accept a third.
A colour changes in one place and no name goes stale.
02
Two levels instead of three
Primitives and semantics. Component tokens add maintenance a small team cannot afford. Instead, every component carries a description with its intent and the tokens it consumes.
The system grows without its upkeep growing.
03
OKLCH so that modes are configuration
Eight palettes in a perceptually uniform colour space: the same lightness reads the same in any hue. Dark mode stops being a design project and becomes a mode change on a variable. AA contrast is checked on the token pair and fixed in the variable.
Three modes from the same 67 tokens, nothing repainted.
04
Yellow acts, green confirms
They never compete on the same screen. In dark modes yellow is replaced by lime automatically, because the action token resolves to a different value. Everything that is not the main action is secondary or a link.
One primary action per screen, in dark modes too.
05
Native Material 3, no custom widgets
The contract is simple: ThemeData and ThemeExtension, no custom widget classes. So the system leans on native Material 3 widgets, and hover and pressed states are not exported: Flutter handles them, not Figma.
The developer consumes tokens from the theme, with no translation.
06
A real device floor
Minimum width of 360 dp, from Android data; minimum height of 667 pt, from real traffic on iPhone 7 and 8. Below it a Flutter breakpoint hides or reflows, by design. Tablet and desktop wait for data.
Every component fits 360 × 667 and everything else stays out of scope.
The same 67 semantic tokens in Light, Carbon and Navy. The token does not change; the value does.
03 The system
Five layers, bottom up. Each one rests on the previous one and none skips ahead: a component never touches a primitive, and a rule never names a colour.
The layers of the system
01Primitives83 variablesEight OKLCH palettes with the same lightness at every step
03Scales13 spacing · 7 radii · 107 typeZilla Slab for the large headlines, Inter for everything else
04Components15 setsAuto layout, every fill bound to a variable, closed contracts
05Rules and themeUsage guidelines · ThemeDataWhen each thing is used, and the same token in Figma and in Flutter
Tokens. 277 variables in Figma, the single source of truth of the system. Spacing works in bands: 4 to 8 inside a component, 12 to 20 as padding, 24 to 40 between sections. Crossing a band breaks the rhythm, and that is written down.
The scale is named by value and used by band. You never jump from one to another.
Component kit. Fifteen sets built on the tokens: button, social button, list row, text field, switch, radio, checkbox, segmented control, action card, avatar, badge, plan selector, tabs, bottom bar and top app bar. Each set carries its description with the layer architecture, the tokens it consumes and the handoff notes for Flutter.
Eighteen variants. Hover and pressed are not among them: the Flutter theme resolves those.
The list row as a contract. It is the component that unlocks the most screens, so it has a closed contract that works as its API: the leading slot can be nothing, an icon or an avatar; the trailing slot is a fixed list of nine options, from chevron to switch, badge or value. No new type ships without updating the spec first. In Flutter that contract is an enum, not a free widget.
One row, three slots. All 47 versions in the app fit here.The app chrome: neutral top app bar, tabs and bottom bar bound to the tokens.
One-click theme switch. The reference screens are duplicated by changing only the variable mode on the section. Nothing repainted.
Light on top, Carbon below. The only difference is the mode selected on the section.
Iconography. The app mixed icons from several eras and weights. Instead of buying a new set, I consolidated the one the company already owned: a single library, 1.5 stroke, line style, 24 px base, and the rule that no icon is ever hand-drawn.
04 Results
BeforeAfter
Primary button44 versions scattered across the appOne set of 18 variants with usage rules: when primary, when secondary, what happens with two actions
List row47 different implementations, none as a componentOne component with a closed contract, an enum in Flutter
Colour and themeLoose styles in the code, one mode277 variables as the single source of truth and three modes from the same tokens, checked on real flows
HandoffConversational, screen by screenFifteen components with handoff notes, consumed from the Flutter theme with no translation layer
DocumentationDecisions in the head of whoever made themEvery decision published with its rationale on its own site, the next case in this portfolio
05 Learnings
01
Audit before you tokenise.
The inventory was not preliminary work: it was the most important design decision of the project. It surfaced the component that resolved the most screens and that nobody had asked for.
02
Rules are worth more than pieces.
A kit without rules produces eyeballed decisions on every screen. Writing down when each thing is used, and why, is what turns a library into a system, and what lets others decide without asking me.
03
Design for a machine to read it.
Names by role, documented intent, closed contracts. That is what lets the developer, or whoever joins next, build a screen and have it come out close to what the system intends.
A design system is not a deliverable. It is a decision about who can change what without asking permission.
216screens audited8 flows on iOS and Android, before defining a single token
3modes from one tokenLight, Carbon and Navy: switching theme is switching a mode
15component setsauto layout, every fill bound to a variable, handoff notes for Flutter
47 → 1versions of the list rowone closed slot contract replaces them all
Do you have a product that needs to scale with purpose?