Number 8Oct 2026

KAIROS is a design system for consoles where a person supervises autonomous aircraft and approves or rejects what an AI recommends, often against a clock. I built it object first: define what things are, then let every component follow from that. This is how the grammar came together and what it changed on screen. Pinned to October 2026.

Devon Kim Building an Object-Oriented Design System

Building an Object-Oriented Design System


Conditions

  • A component is defined by an object and five values, not by its look
  • The same object shows the same id and state on every surface
  • Rules are enforced by checks, not by a style guide
  • Every design decision is numbered and written down (D1–D73)
THE OBJECTAircraft · HERON 2id UAV-02 · state UnreliableIts alert · its decisionobjects too, each with an idFIVE SURFACESMap markerwhere it isList rowwhat needs youInspectoreverything about itTimeline itemwhat happened whenChipnamed inside a sentenceEvery surface shows the same id and the same state. Selecting it anywhere selects it everywhere.
Fig. 1one object, five surfaces, the same id and state on each

Why components weren't enough

A component library tells you what a button, a card and a badge look like. It doesn't tell you what the thing inside them is.

In an operations console that gap is the whole problem. The operator keeps asking the same questions about every number on screen. Did a sensor measure this, or did the AI guess it? Is this already happening, or is it only a suggestion? Can I act on it alone, or does someone else have to sign? A card component has no answer, so every screen answers differently, and the operator has to learn each one.

So I started somewhere else. Before any component, I wrote down the objects the operator deals with and the few properties every one of them has to declare.

Start from the object

The objects come first: an aircraft, an alert, a recommendation, a decision, a contact at sea, a search-and-rescue case. Each has an id and a state, and each can appear in more than one place.

A screen is a projection of those objects. The same aircraft is a marker on the map, a row in the list, a page in the inspector, an item in the event timeline, and a chip inside a sentence. Two rules hold across all of them:

  1. 01Same identity — every surface shows the same id and the same state marker
  2. 02Shared selection — select it anywhere and every surface follows

This sounds obvious. It is here because the failure it prevents is easy to build by accident: the list says one thing, the map says another, and the operator reconciles them in their head.

Five axes

Every component is a function of five values

render(object, projection, epistemic, attention, authority)

  1. 01Object — what it is: aircraft, alert, recommendation, decision
  2. 02Projection — where it is shown: marker, row, inspector, timeline, chip
  3. 03Epistemic — how it is known: observed, derived, inferred, proposed, authorized, executed
  4. 04Attention — how loud it may be: ambient, aware, attention, interrupt
  5. 05Authority — who may act on it: sensor, system, AI, operator

The epistemic axis has a known flaw. It answers two questions at once: where a fact came from (observed, derived, inferred) and how far an action has got (proposed, authorized, executed). One item can be inferred by the AI and authorized by an operator, or sent without being confirmed, and a single value can't say both. The next version splits it into source and stage.

The look follows from the values. Nobody picks "the red one" or "the one with the dashed border" by hand. If two components show the same epistemic stage, they show it the same way, because the stage is the input.

Interaction states like hover and focus still exist. They are the substrate, not the point.

render(object, projection, epistemic, attention, authority)OBJECTwhat is itAircraftAlertRecommendationDecisionContactPROJECTIONwhere is it shownmarkerrowinspectortimelinechipEPISTEMIChow is it knownobservedderivedinferredproposedauthorizedexecutedATTENTIONhow loud may it beambientawareattentioninterrupt · max 1AUTHORITYwho may actSENSORSYSTEMAIOPERATOR · L1–L3A component is a function of five values. Its look follows from them; nobody picks a style by hand.
Fig. 2

Known, inferred, proposed

The epistemic axis turned out to matter most. The AI in KAIROS can be wrong, and the operator has to be able to tell at a glance what the sensors saw from what the AI concluded.

The rules

  1. 01Observed and inferred never share a style in the same container
  2. 02A proposal never looks like something already approved
  3. 03Anything inferred says who produced it and when
  4. 04Confidence is a category plus evidence counts, never a bare percentage

One rule changed along the way. Early on, inferred blocks carried a diagonal hatch. It looked distinctive and it read as noise. Now the stage is said in words ("inferred", "not executed", "AUTHORIZED") and the colour only repeats it. A screen reader gets the same information, and so does anyone who can't tell violet from blue.

SEENTHE AI'SA PERSON'Sobserveda sensor reported itplain · data agederivedcomputed from those∆ markedinferredthe AI's hypothesisviolet · italic · “inferred”proposedthe AI's suggestion“not executed”authorizeda person signedsolid fill · signer idexecutedit is happeningphase · outcomeThe stage is said in words. Colour and ground only repeat it.Observed and inferred never share a style. A proposal never looks approved.
Fig. 3

One thing interrupts, and who may act

Attention is a budget. At most one thing on screen may interrupt, and at most three may ask for attention. When more happens at once, the console aggregates instead of adding another red banner.

What counts as critical took a while to get right. The first version promoted anything with a deadline, and a simulated hour of one scenario produced 27 critical interrupts. The rule now is narrower: critical means a deadline and a harmful default, so doing nothing would make things worse. The same hour produces three.

Authority is graded by consequence

  1. 01L1 — reversible, like holding position; the operator decides alone
  2. 02L2 — changes the route; the Mission Lead has to concur
  3. 03L3 — ends the mission; concurrence plus a written reason

Authorize only appears after the operator has previewed the outcome. If there isn't time to wait for concurrence, the operator can act alone, and the decision is flagged for review. Every step, from preview to outcome, writes one audit entry in the same operation as the state change, so there is no path that skips the log.

BY CONSEQUENCEL1reversible · e.g. holdPreview outcomeAuthorizeL2changes the routePreview outcomeRequestLead concursL3ends the missionPreview outcomeReasonRequestLead concursAuditone entry per stepEmergency sole authoritytime left < expected concurrence · post-reviewAuthorize appears only after a preview. The default is the safe act.
Fig. 4

Tokens named by meaning

The tokens follow the same idea. They use the DTCG format and come in three layers:

  1. 01Primitive — the raw palette and scale; components never touch it
  2. 02Semantic — named by an axis: epistemic.inferred.fill, severity.critical.fg, authority.ai.fg
  3. 03Component — only when a component really needs its own value

A name says what a colour means, not what it looks like. There is no purple-dashed token, only an inferred one.

The build checks the rules instead of trusting them. Text contrast has to reach 4.5:1, warning and critical have to stay different colours (orange and red; a critical also gets the countdown and the banner position), and a raw rgb() shadow or a pixel radius in a stylesheet fails the build. The same source also generates a QML theme, so the grammar isn't tied to the web.

PRIMITIVEpalette.violet.darkpalette.red.darkspace.3radius.panelnever used by a componentSEMANTIC · BY AXISepistemic.inferred.fillseverity.critical.fgauthority.ai.fgsalience.interrupt.motionnamed by meaning, not lookCOMPONENTAiCardCriticalBannerAssetRowAuthorityZonereads semantic tokens onlyCHECKED ON EVERY BUILDtext contrast ≥ 4.5:1no raw rgb() or pxcritical red = warning redDTCG source → CSS variables and a QML theme. A failing check fails the build.
Fig. 5

Critique, then redesign

A grammar is easy to admire and hard to use, so I had it critiqued twice from a human factors point of view. The second review found 26 issues. Most were about honesty: the console promising more than it could know, or making one choice easier than another for reasons that had nothing to do with the situation.

The biggest redesign came after that, and it was about hierarchy, not new features. The same critical alert showed up in five places and its countdown three times. The inspector was 1,658 px tall, with the decision at the bottom.

What changed

  1. 01One owner per repeat — the big countdown lives in the banner only
  2. 02Tabs — Decision, Telemetry and Log; the decision tab opens with a one-line summary of what happens if nobody decides
  3. 03One AI card — the proposal, the numbers that decide it, why, and the strongest evidence against it
  4. 04Quiet things fold — healthy flights take one line, the event log takes one line

The decision tab is now 626 px and fits a 1440×900 screen without scrolling.

What the checks can't tell me is whether a person decides better with this. A user test is next: twelve participants, six conditions, half of them with the AI wrong. It is exploratory. It can find where people misread the console, not prove it safe, and it counts needless rejections when the AI is right as well as errors missed when it is wrong.


Sources