hunter soares

Busca

Cases, contato e CV

Contact
PT

Design system · three products15 min read

Diletta design system

Three products with different brands, built from one language, and a procedure for changing it

3
products with different brands run on the same interface language.
263
published versions. The furthest behind product is 24 back today; in July, 176.
1,022
files of system updates landed in the app without changing a pixel.
3 days
to ship a new product under its own brand. It used to take six weeks.

System in production. Product Designer at Diletta Solutions, from November 2021 to today.

Three products, one languageOn the outside, the same product in both modes. In the middle, another company with its own brand. Under all three, the same components: what changes between them is the list of colors each one declares.

00 File

Product The design system behind the company's apps
Products on it Conta Bold, a digital bank, and CPF Seguro, which protects a person against fraud in their name · both live, and a third in progress
Role Product Designer · Diletta Solutions
Period November 2021 to today
Platform Flutter, iOS and Android · with the color layer already published for web
Tools Figma, Flutter, Git, Python
Scope the whole system: the rules, the components, the automated check, the versioned delivery, and the link to Figma
State on 09 Sep 2026 version 0.180.0, with 263 published versions

One line. A new product joins the system by declaring a list of 55 colors. No component is rewritten, no test is changed, and a report points out what that product forgot to declare.


01 Summary

Problem. Three apps, three brands, one small team. The normal path is to copy the component library into each product. Six months later there are three similar libraries, and a fix made in one never reaches the others.

What I did. I separated language from brand. The brand became a list of colors the product declares. Components stopped knowing specific colors and started knowing roles: background, text, error, success. On top of that, I built three things that did not exist: an automated check that answers whether a component is done, a versioned delivery where each product chooses when to upgrade, and a written procedure for when a product needs something the system does not have.

Result. 263 published versions. The furthest behind product is 24 versions back today, and the other one is 8 back. In the July audit one of them was 176 versions behind, with not a line written about what had changed. One update of 1,022 files landed in a consuming app without changing a single pixel.

Role. Product Designer at Diletta Solutions. End-to-end design, the architecture of the system, and the ruler that judges what gets into it.

The whole system, in blocks1200×1230The system on a single page: the language at the top, the two surfaces the team looks at, the three products that consume it, and the six sides where "done" gets measured: specification, code, catalog, Figma, painting and the way back. The numbers on the bottom row come out of the program that measures, not out of an estimate of mine.

02 Context

The company sells software to other businesses and also runs its own products. Three apps are live or in progress, each with its own name, brand and audience. The design and front-end team is the same for all three.

That creates a problem that is simple to state and expensive to solve: how do you make three products look like three different companies without maintaining three libraries.


03 Constraint

A small team for three brands. There were not enough people to maintain three systems in parallel, and no way to stop the products to rebuild the base.

The app decides when it updates. The system arrives in each product as a versioned dependency. If bumping the version breaks a screen, nobody bumps it, and the system stops circulating. The consumer holds veto power, and that is healthy.

Real brands, not a light and dark theme. Each product needs its own identity, not a hue shift on the same blue.

Figma and code cannot drift. With two sources of truth, within two weeks nobody knows which one is right.

I own the system and I design inside one of the products that consume it. That is a permanent conflict of interest: the same designer who asks is the one who judges the request. Half the decisions below exist to move that judgment out of my memory and into a file.


04 The problem in numbers

Before, the question "is this component done?" had no answer. Done in the code is not done in Figma, and neither is done in the documentation the team reads.

The audit I ran in July measured the cost of that in one of the products: 176 versions of the system published since the last one that app had adopted, and not one line written about what changed in each.

Nobody updates a dependency without knowing what's inside. The system had stopped circulating, and no meeting had decided that: it was the consequence of there being no record.

Today the same number, measured on 08 Sep 2026, is 2 versions behind in one product and 18 in the other, with a changelog per version. That number is what says whether a design system is alive, and it is the easiest one to hide.


05 What existed before, and why it didn't work

The same problem showed up four years earlier, in DPS, and it was solved by hand: twelve palettes in light and dark, twenty-four screens drawn one by one in Figma. This case is the second attempt, at the root.

The common way to handle brand in a design system is the theme: the component knows the colors and you swap the values from outside, by configuration.

That works while the brands are similar. When they are not, drift begins: one product needs a button the others don't have, someone copies the component to change it, and the copy never comes back. That is exactly the path DPS, the company's earlier product, had walked while solving twelve brands by hand in Figma.

So the question changed. Instead of "how do I configure this component's color", it became "how do I make the component never need to know the color".


06 Decisions

D1 · The brand is a list of colors, and nothing else

The product declares 55 required colors and 7 optional ones. From those, the system derives the 59 roles components use, in both modes: background color, text color, error color, success color, elevated surface color, and so on.

No component knows a color. All of them know roles.

Measured consequence: the difference between one product and another is those 55 colors. Zero changes in components, zero in tests.

Why: while the color lives inside the component, every new brand is a change to the component. Once it lives outside, a new brand is filling in a form.

Conta Bold, light mode
Conta Bold, dark mode
The same screen of the same product in both modes. No component knows dark mode exists: every color role has one value per mode, and the screen paints by role. That is what let dark mode ship without a single new line of component code.
The color page1320×2400The list, generated from the code and not written by hand. Each row is a named role, what it means in one sentence, its value in light mode, its value in dark, and the measured contrast of both. This is the document a new product fills in to get its own identity, and it is what answers why a color exists at all.

D2 · The tree has three tiers, and each one declares only what is its own

Two tiers are enough for three products: the language on top, the product at the bottom. The third appeared when one of the house's products became the base of a family: the white base and the first product's identity lived in the same place, so a new product was born wearing the first one's clothes. The middle tier's rule is one sentence: it declares mechanism, form and composition, and it never declares a value that only one product decides. Raw colour, a scale step, a gradient value, an image path, a font name.

What makes the rule enforceable is the ruler, and I wrote the ruler wrong first. A ruler that looks for a product's NAME cannot see identity that moved up as a value. A list of symbols carrying the brand's name closed at zero while thirteen text steps of that same brand sat inside the base, because a step is a number, not a name. The middle tier's ruler has had two columns ever since.

I gave up: a simpler drawing. Two tiers were easier to explain, and the third exists because the business asked for it, not because the architecture got prettier.

What I learned doing it: the tier had already been operating for three weeks without being written down, and I found that out reading my own record — a line from August was asking on behalf of a product the law did not know about, and a defect from that same week was one brand keeping another brand's artwork, because the key stored the file name and not the brand. A tier that is not written down makes every question arrive as a special case, and nobody counts special cases.

The three tiers, and who declares what1200×1260The three tiers with two columns each: what it declares and what it never declares. The numbers at the bottom are today

D3 · What the product forgets to declare, a report points out

If a product doesn't declare a color, it inherits the system's. That is convenient and dangerous: the screen ends up wrong and nobody notices until someone looks.

So there is a report that lists, for each product, what it declared and what it inherited without declaring.

Why: the model is only safe if the omission is visible. Without the report, the default value is a delayed bomb.

D4 · Extensibility through open pieces, not configuration hooks

When a product needs a component the system doesn't have, it builds that component from the system's pieces, which are public. The structure of a surface, for example, is split into top, content and base, and those three parts can be used separately.

Why: hidden configuration is a contract nobody reads. An open piece is a contract you can see.

What I gave up: control over each product's final result. In exchange, products stop copying the system to change it.

D5 · Done has six places, and the check looks at all six

A component only counts as done when it closes in six places: the written specification, the code, the catalog the team consults, the Figma file, the colors it resolves in each mode, and the way back, which is being able to read a product's own drawing and compare it against what the language dictates. The check runs from the command line and returns a number, not an opinion.

Consequence: in CPF Seguro, the 85 pieces under contract close all six sides, and 38 of them were compared against the actual drawing. The other 47 have a side that does not apply, each with its reason written down.

What I learned doing it: the first number I published was false. The program counting them silently skipped the cases it couldn't classify, so the total looked better than it was. An honest denominator is a prerequisite for any goal.

The specification at the centre1200×1163Five sides are measured against the sixth. The specification is the only one a person writes, and the other five are checked against it by a program. At the bottom, the part I got wrong: for months it was the source and it reached nobody, because it lives at the repository root and what travels to the product is the package.
The state of the six sides1400×1150The page is not written by hand: a program measures the six sides and generates it. "Closed" and "measured" are different words, and reading them as one number is where a green that proves nothing begins. Below, piece by piece, the color the language dictates against the color the product paints, and whose next step it is.

D6 · A product's request becomes a file with a written call, not a conversation

When a product needs something the language doesn't have, it writes a request in a fixed format. Every request goes into a register with four possible answers, the reasoning, and the version it shipped in. That register holds 196 lines today.

The rule it supports is this: a variant only moves up into the system on the second request. Two teams asking for the same thing is a sign the language is missing a word; one team asking is a sign that team's product is missing a piece.

Why: without a record of the first request, the second-request rule never fires, and the decision goes back to depending on someone's memory. A person's memory is not a procedure.

What I gave up: speed on each individual request. Writing the reasoning costs more than answering in a chat thread.

What it changed in me: one line in that register is a product proving I had announced a validation that did not exist, and the answer starts by admitting it. The register has to correct me too, otherwise it is theater.

The request register1400×1120One line per request, with the call made on it and the version it shipped in. The columns never change, and that is what makes it possible to count how many times the same gap appeared before it became a piece of the system. Whoever asked appears under a pseudonym, because the file travels to every product.

D7 · The same change has to be adoptable on any platform

The company's products are not only apps: there is an operations panel and an internet banking surface, both web. So the decision is not "make a web version of the system", it is harder than that: a change to a system piece has to be adoptable in any product using it, regardless of platform, through the same adoption gesture that already exists today.

Measured state: the 93 color primitives already ship to web, and become 162 stylesheet variables. The layer components actually paint with, the 59 roles, is computed in Flutter and therefore does not yet exist outside it. That is the hole, and it is written into the decision instead of being discovered during implementation.

How I sized the step: I measured the operations panel before proposing a timeline. There are 3,746 component instances in the file, 2,689 of them visible, and the count is done by component identifier, never by layer name. Counting by name inflated the number by almost 30%.


D8 · The machine gathers and measures; the deciding is mine

The system has 36 command-line tools, and the whole check runs in five minutes. In August the question came up of splitting that across AI agents to go faster. I timed it before answering: the fourteen tools in the gate take 15.2 seconds. Splitting fifteen seconds buys nothing. The time was in the judgement, and judgement is serial, because every divergence I read changes the question the next one asks.

So the split is not by subject, it is by the right to write. One agent gathers facts and has no permission to conclude. One agent measures a single side and returns a number, and it is born with the defect built on purpose: if it does not go red in front of the defect, it does not ship. A third one is the advocate for the product about to receive the complaint, and it exists to kill the wrong complaint before it leaves the house. Publishing, bumping a version and adopting in the app remain a person's gesture.

I gave up: the speed I had been promised. The engine still takes five minutes, and I wrote that in the document instead of selling the opposite.

Consequence: out of 88 code deliveries in four days, 11 were fixing a measurement of mine that was wrong, 12.5%. None was caught by me re-reading them: every one was caught by a gate that builds the defect, or by a second voice asking.

What each one has the right to write1200×1240The ladder of who writes what, from the agent that only gathers facts to the person who authorises. The four gestures with no undo sit in the box at the bottom, and none of them is automatic: the tool that pins comments in the client

07 Craft

The catalog · the values tab1600×871The catalog the team consults, and it runs: 57 color roles across both modes, each with its measured contrast and a badge saying whether that value is an alias of a brand color or derived from one. The list is not written here, it is the same one the code loads.

loading the system's own pieces…

Screen
Brand color
Mode
what the brand declared
--cps-primary: #003BE0
data-theme:    light
tela:          app
Declaration, liveTwo screens of the product, rebuilt from the geometry read off the Figma tree: signing in with a saved profile, in the app, and the password reset in the admin panel. The pieces are the system's own, not an imitation — the web instance of the language was copied from the repository at version 0.180.0, and the button, the avatar, the field and the spot icon come from it. Switching the brand colour declares another value for the primary variable and the eight derived from it, which is the door for a product born with one colour. The illustration and the decorations are exported from Figma as images, and that is why they do not follow the switch: brand artwork is declared by the product, not by the language, and a new brand brings its own.
The catalog switching modes1500×860The same catalog, one click on the mode button. Recorded with it running, not assembled: the whole inventory repaints because every value is a role, and a role does not know a color. The sample pair at the top stays one light and one dark on purpose, because that pair is what shows both sides at once. The contrast figure beside each value is measured on the spot, not typed in.

Where this does not hold yet: on the components tab the mode switches the frame and the previews stay light. That is a gap in the catalog, not in the language, and it is written here because I found it while recording this figure.

Type scale1400×2267Typography: eleven tokens, and no size outside them. The page is generated from the code, so the "generated from code" badge at the top is not decoration, it is where the data comes from.
Spacing scale1400×1459Scale: nineteen spacing steps, the radius, and the width thresholds. Every bar is bound to its own variable, so the page cannot disagree with the code.
Elevation1400×1203Elevation: nine shadows, each named for where it belongs, with the measurement read from the effect itself. An unnamed shadow becomes a loose value on somebody
Button1400×2084The button has four axes and 192 combinations, every one painted by variable instead of a hand-picked color. Three heights, 32, 40 and 56, and the radius is always half the height.
Input1400×1191The input resolves nine combinations of type and state with a single shell, and that shell is what gives the same border to the picker, the search field, the SMS code and the amount field.
The composition proof786×1704The composition proof, inside the system file itself: a card-entry screen built only from pieces of the language, with nothing drawn on the side. It is the closing criterion, because a screen that needs a one-off piece is a hole in the system.

What exists today, measured on version 0.175.0:

  • 128 pieces with a written contract, one per piece, and the contract travels inside the package instead of living only in the repository.
  • 59 color roles across both modes, 355 icons, and 21 variant groups generated from the definition instead of drawn one by one.
  • 516 entries in the single token list, which is the same color written in the three spellings the team uses: the code one, the open token format one, and Figma's.
  • The four states every screen has to resolve, empty, error, failure and waiting, each with the right piece, plus the rules for artificial intelligence in the interface: mark, explain, verify and authorize, by level of risk.
  • The 216 component names from the two products were compared one by one against the system's 104, and each one got an answer. None was left unanswered.
  • There is a test product, built only to prove the model: an amber palette that depends on nothing but the system. If that product is cheap to create, the model works. It is the closing criterion.
  • The fast check, for anyone touching a screen, runs in 16 seconds. The full one, in 2 minutes.
  • Beyond that, there are tests that compare each screen's image, pixel by pixel, against the approved version. That is what allows the claim "not a pixel changed" without depending on anyone's memory.

08 Result

The update nobody saw. The system jumped thirteen versions at once, touching 1,022 files. In the consuming app: the 24 screenshots stayed identical, the 2,062 tests passed, and static analysis flagged nothing. Not a pixel changed.

That is the result of the system, not a side effect. A design system only circulates if updating is cheap and predictable. When it isn't, the product freezes on an old version and the system dies standing up.

Why these numbers and not others. Two things kill a design system, and both have names. First: nobody knows how much of the product actually uses the system, so adoption becomes a feeling. Second: nobody wrote down who decides what gets in, so the system drifts and turns into a pretty library each team works around its own way. That is exactly why the numbers here are distance in versions and lines in a register. I would rather say one product is 24 versions behind than say I have 128 components: the second number answers no question that the person paying the bill is asking.

Circulation. 263 published versions, and the worst lag today is 24 versions, against the 176 from the July audit. Every version has a changelog. The lag went from 18 to 24 since yesterday, and it is here because a number that only shows up when it improves is not measurement, it is advertising: the product that is behind has its own team and decides when it moves, and what it needs is a week of adoption, not a chart from me.

The check. In the measured product, the 85 pieces under contract close all six sides, and 38 of them were compared against the drawing, not merely declared.

The color ruler. Comparing one product's drawing against what the language dictates, 33 pairs match exactly, 29 diverge, and 22 of those divergences have a field-by-field reading: which color should be there, which one is. A divergence with an address is work; a divergence without one is an opinion.

A new brand. The entry cost of a new product is a list of 55 colors.

Who depends on it. It is not only my own work running through here. Three products consume the language, two of them live under different companies' brands, and the request register holds 196 lines with the product that asked, the call that was made, and the version it shipped in. It is the part of the work that keeps moving when I am not in the room, and that is why it lives in a file instead of in my head.


09 What I would do differently

I published a number before checking how it was counted. The program that measured "components done" silently ignored the cases it couldn't classify. The total looked good and wasn't. Today I write the test for the meter before I publish the measurement.

I trusted checks that measured presence, not the question. There is a file today with twenty numbered cases where a check went green over a real defect, or red over something that wasn't one. The class is always the same: presence is easy to measure and almost never the question. The first case is the clearest: the check looked at the color in the component tree when the question was the pixel, and blue at 18% opacity has a different final value than solid blue. If I went back, I would write the failing case before the check, for every one of them.

I wrote the changelog after I needed it, not before. The audit found 176 versions with no line of explanation. That wasn't pending paperwork, it was the reason the system had stopped reaching the products. Documenting what changes is part of shipping, not a task for later.

Next case
CPF Seguro
  • design systems
  • product design
  • fintech
  • ai in the process
  • multi-brand
  • accessibility
  • research