My role
Role and collaboration
I was the Lead Product Designer and co-creator of the Onion Design Framework.
The work began when I asked whether we could extract a design system from a Rails codebase itself instead of translating one from Figma. I partnered with Alicia Rojas, a Senior Software Engineer, to test that idea in working code.
Our responsibilities were different, but intentionally connected:
| The track | The driving | |
|---|---|---|
| Who | Alicia | Arely |
| What | Harness commit | Interface overhaul |
| Lines | 569 | 6,771 |
| Share | Engineering guardrails | 94% of the branch's additions |
Alicia built the engineering track around the work: branch protection rules, CI, Bash hooks, and permission settings. I drove inside those boundaries, using Claude to design and ship the interface changes directly in the Rails codebase.
The split is the story. One person built the track. The other drove.
From the experiment to the framework
Together, Alicia and I co-created the initial framework structure, its five layers, and the protected workflow. After the original experiment, I continued developing Onion independently: I built the first complete reference implementation in React, translated the methodology back into Rails, and designed the experiments used to evaluate it.
From How a defect travels inward onward, the work was mine: the inward-fix method, enforcement rings, React and Rails reference implementations, fidelity tests, analysis, and subsequent findings were my responsibility.
Engineering continued to own backend architecture, data modelling, security, and permissions. My role was to determine how far design could safely travel into code — and which decisions could become structural, which still required interpretation, and which continued to need human judgement.
The shift
The design system's reader changed
Design systems were built primarily for people to interpret and use.
A designer could read the guidelines, inspect a component, and ask a question when something was unclear. A developer could compare an example with the existing interface, compose the implementation, and rely on human review before the work shipped.
AI coding agents changed that relationship. The agent does not only read the design system. It uses the system to generate the screen.
When a decision remains open, the agent resolves it during generation. It may invent a value, ignore an existing pattern, or interpret a written rule differently from the team. By the time a person reviews the work, those decisions are already inside the code.
The design system could no longer be only a resource that described the interface. It needed to become part of the environment in which the agent built it.
Your design system's main reader is no longer a person. It is an agent, and it generates the screen.
The workflow before and after
| Traditional design system | Onion Design Framework |
|---|---|
| A system people interpret | An environment the agent builds inside |
| Guidelines, tokens, components and tools | Tokens, primitives, patterns, rules and gates |
| Designers and developers interpret, compose and implement | The agent generates with structural constraints and written guidance |
| People build and review the screen | People evaluate the generated experience and evolve the system |
The original problem
Figma spoke design. The codebase spoke Rails.
When I started experimenting with the Figma MCP in Claude, I wanted to know whether Claude could use our existing Figma design system to help me design directly in code.
The first results looked promising. Claude could access the visual language in Figma and generate interfaces that appeared consistent with it.
But Figma and the Rails codebase did not describe the interface in the same way.
Figma knew the colours, typography, components, and layouts. Rails knew which shared partial appeared across dozens of views, which form-builder constant controlled every label, how themes were assigned to individual pages, and which implementation patterns the codebase already depended on.
application.css, holding the same colours and fonts as plain values.The agent could see how the interface was supposed to look, but it could not see all the decisions required to build safely inside the codebase. Whenever the two sources did not align, it had to translate, infer, or invent.
The problem was not that we lacked a design system. The system the agent could read was not the system the code executed.
That changed the question:
Could we extract the design system from the codebase itself — and turn it into an environment both people and AI agents could build from?
It worked. But it was a small surface with a forgiving stack, and I wanted to know what held up on something harder — a more complex product, and less of me in the loop.
The first experiment
I brought that question to Alicia.
Instead of importing the Figma system into Rails, we started extracting the design decisions that already existed in the code. We identified reusable values and patterns, built a protected workflow around the repository, and experimented with propagating one design change across multiple screens.
These tools reduce handoff and improve reuse. When several valid paths remain, the human or agent still chooses how to compose them.
The experiment placed the agent, extracted decisions, existing code and enforcement inside the same working environment.
The result was one Rails pull request:
- Lines added
- +7,158
- Lines deleted
- −1,920
- Files touched
- 153
| Measure | Value |
|---|---|
| Lines added | 7,158 |
| Lines deleted | 1,920 |
| Files touched | 153 |
| Dev-only catalog | 64% of the lines added, never served to a user |
| New production markup | 102 lines |
| Harness in production | 68 lines |
| CI checks | 4, all green |
| Review | 1, by an engineer |
| Commits | 2, by two authors |
What ships as the harness is the part worth naming, because it is the part that is small:
- 46 lines of tokens and base styles.
- One 12-line partial, with 50 call sites.
- A 10-line contract in
CLAUDE.md.
Two commits, two authors. Alicia's is the harness commit, at 569 lines. The additions are mine: 6,771 of them, about 94% of everything the branch added, and almost all of it the catalog.
The harness made a change of that size safe enough to merge, and the new structure substantially reduced what the agent invented. Across the affected views, hard-coded hex values fell from 119 to 24. The agent introduced no new arbitrary spacing and no invented components.
But four defects still escaped.
| Checkable in the diff | Visible only in the rendered experience |
|---|---|
| 24 hard-coded hex values remained. A check could have caught them, but that check did not exist yet. | Every form label became smaller. The change was token-compliant and CI remained green. |
| A duplicate canonical tag reached the new page. It rendered as nothing and was found in an SEO audit. | One page rendered white navigation over a white background. Only a person looking at the page caught it. |
The experiment worked, but not in the way a perfect demo would have worked. It proved that an agent could ship a large design change inside a protected Rails codebase. It also exposed the boundary between engineering safety and design fidelity.
Some decisions could be checked mechanically. Others were technically valid and still wrong for the experience.
That changed the question again:
If the agent could build inside the codebase, where should each design decision live so the repeatable ones no longer depended on interpretation?
The hypothesis
Reduce the decision surface, not the model's variability
The first experiment showed that documentation, components, and green CI were not enough to guarantee a correct experience.
An LLM is non-deterministic. More documentation or a longer prompt cannot guarantee that it will make the same decision every time. But many interface decisions do not need to remain model decisions.
A colour can become a token. A recurring interaction can become a primitive. A solved layout can become a composed pattern. Only the decisions that depend on the context of a specific screen should remain open to interpretation.
Our hypothesis was:
If every design decision lived in the innermost layer capable of enforcing it, the agent would have fewer opportunities to invent — and its output would be more likely to belong to the design system.
This was not an attempt to make the LLM deterministic. It was an attempt to make fewer interface decisions depend on its non-determinism.
The operating rule became: a fix belongs in the innermost layer it can possibly live in — not the layer where the defect was noticed.
The question we needed to test was simple: could an agent with no prior context generate a system-consistent screen from a minimal prompt if the framework had already made the repeatable decisions for it?
The framework
Five layers, built from the inside out
Onion organises design decisions by how enforceable they are. The inner layers remove repeatable decisions from the agent. The outer layers handle the context that code alone cannot decide.
Every layer reads the one inside it. The outer layer — the screen — is the one we can throw away and generate again.
| Layer | What lives there | What it prevents |
|---|---|---|
| 1. Tokens | Raw values for colour, spacing, typography, radius, elevation and motion | Invented values and visual drift |
| 2. Primitives | Base components such as Button, Input, Card, Dialog and Table | Re-deriving behaviour, accessibility and states on every screen |
| 3. Composed patterns | Solved interface structures such as PageHeader, DetailPanel and TopBar | Rebuilding recurring layouts and interactions from scratch |
| 4. Written rules | Contextual decisions that no token or component can force | Applying valid components in the wrong interface context |
| 5. Screens | The assembled experience | Nothing — the screen is where the system is tested, not where repeatable fixes should live |
How a defect travels inward
Defining the five layers gave the framework its structure. I then developed the operating discipline for deciding how a defect should move through them.
When something looks wrong on a generated screen, I ask these questions in order:
- Is a token wrong or being misused?
- Can a primitive fix the problem once for every screen?
- Can a composed pattern absorb the decision?
- Only if none of those can solve it: what written rule is missing, and why can the inner layers not enforce it?
If a generated screen uses an incorrect button state, fixing that single screen is the weakest intervention. Fixing the primitive means the correction travels to every existing screen and every screen generated later.
The enforcement rings
The five layers organise decisions. Four enforcement rings make the inner decisions difficult to bypass:
| Ring | Role |
|---|---|
| 1. A primitive that refuses | The component rejects invalid composition — for example, an icon-only button without an accessible name. |
| 2. The editor gate | A hook checks each relevant write and returns the exact line and reason when it finds a violation. |
| 3. The local gate | Fast, deterministic checks verify tokens, text rules and theme parity before work leaves the machine. |
| 4. CI | The same checks run where skipping them is no longer an option, including runtime accessibility checks. |
In the Rails implementation, a primitive refuses by raising a rendering exception before it can produce invalid markup. For example, this simplified guard inside ButtonComponent rejects an icon-only button without an accessible name:
if icon_only? && aria_label.blank?
raise ArgumentError,
"An icon-only ButtonComponent needs an aria-label—its glyph is not a name."
endThe agent receives the reason and required correction immediately. It cannot continue by silently rendering an inaccessible version of the component.
The rule itself appears once, but it can be enforced at multiple points. That matters because a slow or noisy check eventually gets ignored, and a rule duplicated across tools eventually drifts.
The framework is not a document the agent is expected to remember. It is a system that decides which choices the agent is allowed to make freely.
From React to Rails
First, I formalised the method in React
The first complete implementation of Onion used React, Tailwind and shadcn/Radix. It served as a reference environment for testing how agents interpreted the layers.
Before building the system, I tested three ways of presenting the same tokens and rules to three cold agents:
| Variant | Format | Result |
|---|---|---|
| A. Strict schema | A rigid specification and terse checklist | Cleanest and least expensive |
| B. README style | Tokens plus short human-readable do-and-don't guidance | Tied on correctness, with slightly more context cost |
| C. Lint-annotated | Every token paired with its failure mode and fix | The only variant to invent a value, miss part of a touch target and cost the most |
The most detailed document should have won. It did not.
The test showed that machine-readable constraints and human-readable reasoning should remain separate layers. Dense explanations did not strengthen the schema; they buried it.
I then built roughly 50 React components and used cold-agent fidelity tests to expose gaps. Those runs found missing toast guidance, undocumented patterns, and placeholder code inside catalog examples. The agent copied the placeholder faithfully — because the usage example, not the component source, was what it treated as the instruction.
The fix was not another reminder. I created a deterministic placeholder scan that failed the build.
Then, I translated the methodology to Rails
To learn whether Onion described a methodology or merely a React implementation, I rebuilt it in a separate Rails 8 application created specifically for measurement.
| React implementation | Rails implementation |
|---|---|
| CSS tokens | Tailwind v4 tokens with automatic theme-parity checks |
| shadcn/Radix primitives | 61 ViewComponents plus native platform behaviour and Stimulus |
| Radix accessibility behaviour | Native dialog, Popover API, real form controls and browser-based accessibility gates |
| README principles | Usage rules attached to the component catalog |
| Build-time placeholder scan | Editor hook, local verification and CI |
| React composed patterns | The same PageHeader, DetailPanel and TopBar patterns rebuilt in Rails |
The change of stack made the component layer more expensive because Rails did not have Radix underneath it. But the values, reasoning, composed patterns, and layer boundaries transferred.
The Rails version made several ideas measurable that React had only stated. It included 131 colour tokens present in both light and dark themes, 61 component files, and four enforcement rings verified against deliberate defects.
Onion survived the stack change because its layers describe design decisions, not implementation contexts.
Validation
Forty-one runs, one deliberately minimal prompt
I evaluated Onion through 41 agent runs across React and Rails:
| Environment | Runs | Purpose |
|---|---|---|
| React | 3 | Compare document structures using the same tokens and task |
| React | 2 | Expose gaps in the design system through cold-agent fidelity tests |
| Rails | 5 | Test structural and written constraints in the first measurement round |
| Rails | 10 | Compare sharpened prose with an optional composed component |
| Rails | 17 | Test six successive attempts to correct the same cards-first behaviour with Claude |
| Rails | 1 | Run the same screen task with the design system accessible but without the harness |
| Rails | 3 | Repeat the same screen task with the harness through Codex's orchestrated workflow |
| Total | 41 | Claude and Codex across two stacks |
For the Rails experiments, every run started from an isolated copy of the repository with a seeded database. Agents had no shared conversation history. I recorded the model before launching each run and kept the prompt constant within each comparison.
The prompt did not mention Onion, cards, tables, tokens, components, or layout. Its public wording is domain-anonymised while preserving the original scope and level of detail:
Build the records page for this app. Someone using it needs to see the records, edit one, and delete one.
If the prompt had repeated the rules, I would only have measured instruction following. I wanted to know what a builder with no prior context could recover from the environment itself.
The decision I intended the framework to carry
I also wanted a decision the agent would not arrive at on its own. A list of records has an obvious answer — a table — and an agent reaching for it proves nothing about the environment, because it is what many builders would reach for by default.
The intended design decision was less obvious: the collection should open as product cards, with the table available as another view of those same records. The prompt never said that. I believed Layer 4 did.
At this point in the experiment, I treated the cards-first behaviour as a test of whether the agent could recover that decision from the environment. A later audit showed that this assumption was itself part of the bug.
Experiment 1: structural consistency worked
In the first five Rails runs, the agent-authored code produced zero real violations across 30 pre-registered checks after adjudication. The agents reused existing components, avoided raw CSS literals, preserved the type scale, and included feedback for committed actions.
Two runs even discovered that the application shell was missing the region required to render toasts and repaired the baseline itself.
That was encouraging, but incomplete. All five agents still chose a familiar records-management pattern: a table, a row menu, and a modal. None opened on cards or used the pushing detail panel I expected from the framework.
The system was producing consistent code without necessarily producing the intended interaction behaviour.
Experiment 2: prose versus structure
I sharpened the rules and created a CollectionBrowser component that packaged the intended cards, table switcher, and detail panel into a single call.
Ten new agents received the improved rules. Five also had the composed component. My prediction was that the structural arm would make the intended pattern nearly automatic.
The result inverted the prediction:
- The application shell appeared in 10 of 10 runs.
- The cards-first behaviour appeared in 1 of 10 runs — and that run belonged to the prose-only arm.
- The optional composed component was used in 0 of 5 runs that had it available.
The component worked if an agent selected it. Nothing forced the selection.
An optional component is still documentation. Real structure is the constraint an agent cannot route around.
Experiment 3: the failure survived six interventions
I tested six possible explanations. I rewrote the prose more imperatively, moved guidance closer to the component, removed a misleading table example, made cards as cheap to build as tables, and made the table component demand an explicit reason for bypassing the intended pattern.
The output remained remarkably stable. Across 17 Claude runs, every agent built a table, a row menu, and a modal.
The transcripts showed that the relevant design-system material was being found. Seven of ten comparison agents had the detail-panel guidance in context, and all ten opened the relevant catalog and table component.
At the time, I interpreted that as evidence that the agent could read a rule and still route around it. The escape hatch seemed to reinforce that interpretation because agents produced circular justifications for the table they had already chosen.
That interpretation turned out to be incomplete.
I had been treating the output as a compliance failure without first checking whether the Layer 4 rule actually expressed the decision I thought it did.
Experiment 4: the defect was a sentence
The Claude-versus-Codex comparison initially looked like a reader problem. The same screen task run three times through Codex produced cards, a pushing detail panel, and no modal, while the earlier Claude runs had repeatedly converged on a table-and-modal pattern.
But while investigating that discrepancy, a Claude agent inspected the Layer 4 rule itself and surfaced a more fundamental problem.
The rule said:
“When a record has a photo, cards are the default and the table is the alternative.”
That sentence did not encode the decision I thought I was testing.
It made cards conditional on a record having a photo. It did not say that the collection should open as cards. It also did not explain the relationship between the two representations.
Claude had not necessarily been refusing the rule. It had a literal, valid path through it.
The irony matters: Claude had written the original sentence, and another Claude agent later exposed the ambiguity in it.
I rewrote Layer 4 as:
“A record list opens as cards, and the table is those same records seen another way.”
That version expressed the interaction directly. Cards became the initial representation of the collection; the table became an alternative view of the same records.
From that correction forward, every subsequent recorded run opened with cards.
This changes how the earlier experiments should be read. The 17 table results are still real observations, but they are not clean evidence that the agent “read the rule and ignored it.” The specification itself was part of the failure.
The Codex comparison still matters, but it cannot be used as a clean Claude-versus-Codex result because the reader architecture also differed. What it did was create the discrepancy that forced a closer audit of the system.
The failure was not only in the generated screen. It was in the sentence that was supposed to govern it.
And the fix followed the Onion rule exactly: the defect appeared in Layer 5, but the correction belonged in Layer 4.
Same prompt. Same repo. The harness changed what the agent could recover.
After correcting the Layer 4 rule, I ran a direct four-run comparison using the same products-page prompt and the same base repository.
One run had access to the repository and could inspect the design system, but did not have the harness. The other three ran with the harness and the design system available.
The results were visually consistent within each condition.
| Design system accessible, no harness | Design system + harness |
|---|---|
| The agent produced a generic products table with default-looking edit and delete actions. | The agents produced the application shell, product cards, governed states, and a dedicated non-modal detail panel. |
Same prompt. Same repo. Same design system. Different operating environment.
Making the design system available was not the same as making it operational.
This is the visual comparison I would use as the payoff of the validation section, not as proof of a universal causal effect. It shows what happened in these four runs: access to the repository alone did not produce the same interface as an environment in which the design system had been made discoverable, actionable, and checkable through the harness.
The experiment also had to test its own specification
The measurement system failed more than once. One regex flagged correct component usage. Another scorer defined compliance through a component available to only one experimental arm, making a correct alternative look like failure.
I kept the original scores, recorded the adjudications separately, and rewrote later conditions around observable behaviour instead of a preferred implementation.
The Layer 4 discovery was the same kind of failure at a different level. I had been scoring “table” as evidence against the agent while assuming that the specification unambiguously required “cards.” It did not.
That means the experiment was testing two things at once:
- whether the agent could recover and follow the design system;
- whether the design system had expressed the intended decision clearly enough to be followed.
Every generated screen became evidence about both the reader and the system being read.
That is not a footnote to Onion. It is one of the strongest findings: an agent can expose ambiguity in the design system itself, but the experiment has to be willing to revise its own interpretation when that happens.
Results and limits
What Onion improved
Onion gave me a repeatable way to move design decisions out of prompts and individual screens and into the code environment.
- Hard-coded hex values across the affected views fell from 119 to 24 in the original Rails experiment.
- The Rails implementation maintained 131 colour tokens in both light and dark themes through an automatic parity check.
- The methodology transferred from React to a system of 61 Rails component files without changing its five-layer model.
- The first five measured Rails runs produced zero real violations across 30 structural checks after adjudication.
- Cold agents reused components, preserved governed values, and found missing infrastructure without being told the name of the framework.
- An optional composed component was not enough to guarantee selection: it was used in 0 of 5 runs where it was available.
- A repeated cards-versus-table failure exposed a defect in the Layer 4 wording rather than only a failure in the generated screen.
- After Layer 4 was rewritten from a photo-conditioned rule to an explicit cards-first interaction, every subsequent recorded run opened with cards.
- In the direct four-run comparison, the no-harness run produced the generic table shown above, while the three harnessed runs produced the system-shaped cards/detail-panel experience.
The most important result was not that Onion made every output identical. It was that it made inconsistency diagnosable.
The question stopped being only “why did the agent make the wrong screen?” and became:
- Is a governed value missing?
- Can a primitive prevent the defect?
- Can a composed pattern absorb the decision?
- Is the written rule missing, ambiguous, or expressing the wrong reasoning?
- Is the remaining judgement something only a person reviewing the experience can decide?
The cards-first failure is the clearest example. The defect appeared on the screen, but the most useful correction lived in Layer 4.
What Onion could not guarantee
The experiments also set clear boundaries around the framework.
- It did not produce identical executions.
- It did not guarantee that an agent would discover or choose an optional component.
- It did not guarantee that a written rule would express the intended decision correctly; the specification itself can be ambiguous or wrong.
- It did not make every model or orchestration strategy behave the same way.
- Structural enforcement can prevent known invalid choices, but it cannot decide whether the underlying design decision is good.
- It cannot determine whether the team framed the right user problem.
- It does not supply taste, design strategy, user research, or final experience review.
- A design system can make a bad decision repeatable just as efficiently as a good one.
The four-run no-harness/harness comparison is strong visual evidence for this repository and task, but it is still a small comparison, not a replicated effect-size study across models and environments. The study can show what changed in those runs, how decisions propagated through Onion, and how the framework responded when its own rule was defective. It cannot claim that every future agent will make the same choice.
Likewise, the fact that every recorded run after the Layer 4 rewrite opened with cards is strong evidence that the corrected rule mattered in this environment. It is not proof that prose alone guarantees compliance.
Onion reduces the surface on which the agent improvises. It does not eliminate improvisation — and it does not turn consistency into quality.
Repeatable is not the same as good.
What changed
The repetitive process became replaceable. The designer did not.
I began this work by asking whether AI could help me design directly in code. I ended it with a different understanding of what design needs to become when code can be generated on demand.
The repetitive parts of interface production no longer need me every time. Known patterns can be assembled, tokens can be applied, and familiar states can be generated from the system.
That gives design more room to move toward the decisions that should not be automated blindly.
| AI made easier | Still requires design judgement |
|---|---|
| Assemble the interface | Understand users |
| Apply known patterns | Frame the right problem |
| Maintain governed values | Decide what the intended behaviour actually is |
| Follow existing decisions | Apply taste |
| Propagate a component or rule fix | Decide when the system should change |
| Generate another screen | Evaluate the complete rendered experience |
The Layer 4 defect changed this conclusion in an important way.
It would be too simple to say that the agent can generate screens while the designer alone catches the reasoning problems. A Claude agent was the one that surfaced the defective sentence. The agent helped audit the design system.
But finding an ambiguity is not the same as owning the design intent.
The sentence said that cards were the default when a record has a photo. The system could identify that this condition did not necessarily imply “open the collection as cards.” The design judgement was deciding what the interaction should actually mean:
A record list opens as cards, and the table is those same records seen another way.
That distinction matters.
The valuable human work is not manually producing every repeatable interface decision. It is deciding which behaviour should exist, expressing that reasoning in the right layer, evaluating whether the resulting experience is good, and changing the system when new evidence says it should evolve.
The design system became something the agent can audit
I no longer treat a design system as a finished library waiting for people or agents to obey it.
It is a living system. Every generated screen tests it.
When an agent repeatedly produces an unexpected result, there are now at least two questions:
Why did the agent produce this?
Did the system actually say what we thought it said?
In this case, the second question changed the interpretation of the first three experiments.
The AI was not only a consumer of the design system. It became one of the instruments for auditing it. It could surface ambiguity, compare the literal rule with the rendered result, and point to the sentence that did not encode the intended behaviour.
Human judgement still owns a different layer of responsibility: user needs, product intent, taste, trade-offs, and the decision about what the system should become next.
That is a more useful division of labour than “AI builds, designer fixes.”
The repetitive process? Yes, increasingly replaceable. Design judgement? Still the thing that decides what should become repeatable.
Public framework
Onion is becoming a public resource
This case study explains how Onion was created. A framework page is being written to state the method on its own terms, for anyone who wants to use it beyond this project.
Together with the research log, it will cover:
- The five-layer method and the rule for moving fixes inward.
- A practical audit for deciding where an existing design decision belongs.
- Reference implementations in React and Rails.
- Examples of primitives that refuse invalid composition.
- Templates for written rules, local gates, and CI checks.
- A protocol for running cold-agent fidelity tests.
- The complete research log, including model details, contaminated runs, corrections, and open control arms.
Onion was co-created by Arely Rivera and Alicia Rojas. I independently developed the inward-fix discipline, enforcement rings, cross-stack reference implementations, and experimental research that followed. It remains a living framework: each new implementation, stack, and model is another test of which decisions can become structural and which still require people.

