How Polar Built Orbit, an LLM-Safe Design System That Enforces Itself in CI
How do you stop AI-generated UI code from drifting away from your design system? The team at Polar hit this problem head-on, since most of the UI code they ship today is written with an LLM in the loop. Their answer is Orbit, a design system built around one hard rule. If a value is not a design decision the team has actually made, it cannot pass CI.
The mechanics are concrete. A style value outside the token set produces a type error at authoring time. A raw <div> sitting next to the system’s <Box> primitive fails an Oxlint rule in CI. Dark mode needs no second styling pass because every color token carries both themes through the native CSS light-dark() function. Every constraint runs before a pull request merges, with zero reliance on the model remembering a style guide.
This post walks through the reasoning Polar published in June 2026, covering the failure modes they hit with Tailwind, the token architecture they landed on, and the problems they are still working through.

Why Tailwind classes break down when an LLM writes the markup
Tailwind classes are strings, and strings give an LLM infinite room to be slightly wrong in ways lint can never catch. That is the core failure mode Polar ran into.
Their verdict on Tailwind itself stays positive. They call it the most ergonomic utility CSS available, much of their existing codebase runs on it, and they would choose it again on a project where humans type most of the markup. The caveat applies to a narrow scenario. Once the author changes from a person to a model, the same openness that makes Tailwind fast turns into the attack surface.
The root cause is the string itself. className="flex p-4 bg-blue-500" stays plain text until it hits the compiler. That quality makes Tailwind quick to write and equally quick to get subtly wrong in generated code. Polar’s post lists the typical near-misses, all valid, all lint-clean, all rendering correctly:
-
p-4,p-5,p-[17px], andpx-4 py-3produce four different spacing results -
bg-gray-100,bg-zinc-100, andbg-neutral-100are all real grays, and none of them is canonical -
dark:variants depend on the model remembering them, which fails about half the time -
Arbitrary values such as text-[#3b82f6]bypass the palette entirely
Static analysis catches none of this. An LLM has no way to learn that your gray is oklch(0.96 0.003 264) instead of bg-gray-100, because nothing in the type system carries that information. Writing lint rules for strings turns into an endless chase that ends in regex special cases. Props avoid that whole category by construction.
Escape hatches make it worse. The moment an LLM can drop to a raw className or an inline style, every guarantee built on top gets weaker. LLMs reach for escape hatches eagerly because their training data is full of them.
Why documentation fails and CI succeeds as the enforcement layer
Writing the rules down is the obvious first move, and it does not hold. Polar put “use our gray, skip bg-gray-100” into CLAUDE.md, a style guide, and the system prompt. All three versions exist at Polar. None of them stopped the drift.
Anything stored in a document behaves as a probability. The model reads it, weighs it against everything else in context, and follows it most of the time. Across thousands of generations the misses pile up, and review shifts back to scanning every diff for drift by hand.
So Polar drew a harder line, and the rest of Orbit hangs off it. The rules that actually matter live as Oxlint rules running in CI, which produces one deterministic contract. A green PR is safe to merge. Polar also accepted the contrapositive. When something is off and no rule catches it, the gap sits in the rules, and the fix is to write the rule. There is no “but the guidelines said so” fallback.
This shift moves vigilance from authors to tooling. Under the old model, every author, human or LLM, had to remember an opinionated convention on every prompt. Under the new one, the opinion lives inside a check that cannot be forgotten, skipped, or argued out of. The LLM stays free to write anything it wants. Only things the team would happily ship make it past CI.
Why design tokens should be named for intent
Orbit names every token for intent, because a value on its own carries no decision. Step away from the LLM angle and this problem remains, whoever types the code.
A design system is a set of decisions. Cards sit on this surface. De-emphasized text uses this color. Stacked elements keep this gap. The value is the consequence of the decision, and the decision itself lives somewhere else entirely. p-4 states “16 pixels of padding” and stays silent on why, where it is allowed, or what it should match. bg-gray-100 names one specific gray without saying whether that gray is a card, a hover state, a disabled control, or a coincidence.
A CSS variable does not fix this either. --color-gray-100: #f3f4f6 is still a value with a nicer name. It says what the color is and never what it is for.
When code is authored in values, the decision evaporates at the point of use. Six months later a codebase holds 40 call sites using bg-gray-100, with no record of which ones meant “card.” Changing your mind about card backgrounds then means grepping a color instead of editing a decision. The intent was never written down anywhere a tool, a teammate, or a model could read back.
Orbit’s tokens encode the decision directly. background-card records one, the surface a card sits on, while the hex values it resolves to in light and dark mode stay hidden behind the name as implementation details. Spacing follows the same logic. m, l, and xl act as roles on a scale, and two elements using padding="l" declare they made the same decision. That reads very differently from two elements that coincidentally both wanted 16px.
The difference lands hard for an LLM. Hand it bg-gray-100 and it picks one value from hundreds of plausible neighbors, a task that requires taste. Hand it background-card and it picks from a list of decisions already made. The request changes from “have taste” to “name what you are building.”
What Orbit code looks like in practice
Orbit builds on StyleX, Meta’s compile-time, type-safe styling library, with a styling API heavily inspired by Shopify’s Restyle system. The core primitive is a <Box /> component that accepts design tokens as typed props and little else.
Polar’s card layout in Orbit looks like this:
<Box
flexDirection="column"
gap="l"
padding="m"
backgroundColor="background-card"
borderRadius="m"
borderColor="border-primary"
boxShadow="m"
>
<Text variant="heading-xs" color="text-primary">
Card title
</Text>
<Text color="text-secondary">Description</Text>
</Box>
Every value descends from a decision. padding rejects 16px and accepts a predefined set of sizes. backgroundColor rejects hex codes and accepts the names of colors the team has actually defined. The types come straight from the token definitions:
export const spacing = stylex.defineVars({
none: '0',
xs: '4px',
s: '8px',
m: '12px',
l: '16px',
xl: '24px',
'2xl': '32px',
'3xl': '48px',
'4xl': '64px',
'5xl': '96px',
})
export const backgroundColors = stylex.defineVars({
'background-primary': 'light-dark(hsl(233, 4%, 81%), hsl(233, 4%, 3.5%))',
'background-card': 'light-dark(hsl(240, 2.90%, 72.50%), hsl(233, 4%, 9.5%))',
// ...
})
StyleX supplies the mechanism. The design decisions live in one place, and that same place is the only source the prop types accept. An LLM generating Orbit code chooses from a short menu the team wrote instead of the entire space of CSS. Autocomplete surfaces the valid tokens. A typo surfaces as a type error at authoring time, which beats a visual regression discovered three weeks after launch.
Why Polar bans the bare <div>
A typed <Box /> stops nothing while a raw <div> sits beside it. That single observation drove the most contested decision inside Polar, and it is now the one they defend hardest.
A raw <div> accepts any className, any inline style, any attribute. It is a blank canvas, and a blank canvas is exactly what makes off-system code possible. Tokens constrain you only when there is no way around them. A <div> is the way around.
So Orbit removes the untyped container entirely. Box becomes the component you reach for in the exact spot where you used to reach for <div>, with no parallel unconstrained path one keystroke away.
The stakes run higher for LLMs than for humans. A person reads the contribution guide once and internalizes “we don’t write raw divs here.” An LLM rediscovers the codebase on every prompt and defaults to the path of least resistance. If <div className="..."> is available, decades of training data push the model toward it. Moving the default requires making the raw element unavailable, since discouragement alone does not survive a fresh context window.
The obvious objection is semantics. Ban <div>, <section>, <nav>, and <ul>, and do you lose meaningful, accessible HTML? Polar’s answer rests on Box being polymorphic. The list of banned elements is exactly the list Box can render through its as prop:
<Box as="nav" alignItems="center" columnGap="m">…</Box>
<Box as="ul" flexDirection="column" rowGap="s">
<Box as="li">Item</Box>
</Box>
The DOM still receives a real <nav> and a real <ul>, with the right DOM props typed and forwarded. What disappears is the open string surface. as accepts a closed set of allowed elements, while a bare <nav> is an open door.
Enforcement lands in Oxlint:
'polar/no-raw-html-layout': 'error',
The rule message states the intent:
Use <Box /> from @polar-sh/orbit instead of <div />.
This ensures we follow the Orbit design system.
Making the wrong thing fail to compile is, so far, the only instruction Polar has found that survives an LLM’s fresh context window.
When a developer needs a value the design system lacks, Polar treats that as a signal to add a token instead of a reason to bypass the system. Where legitimate escape hatches end and laziness begins is still being drawn, and they expect the line to move as they learn.
How Orbit makes dark mode impossible to forget
Every color token in Orbit carries both theme values in a single definition. Look at light-dark(hsl(233, 4%, 81%), hsl(233, 4%, 3.5%)) from the token file above. The browser’s native light-dark() CSS function resolves the right half at render time.
No dark: variant exists to remember, because no second styling pass exists at all. You write backgroundColor="background-card" once, and the correct value renders in both themes. An LLM cannot ship a component that looks right in light mode and broken in dark mode, because no separate dark-mode code exists to get wrong. The most common class of theming bug becomes inexpressible.
What Polar has learned running Orbit so far
Orbit is early, and Polar presents the results as a direction, with the verdict still out. Code review already feels different. Reviews of generated UI used to hunt for style drift, the wrong gray, the wrong spacing, the forgotten dark mode, the stray arbitrary value. More of that is now correct by construction, so the conversation moves toward behavior and layout.
Not every developer at Polar knows the design system and UX standards, and Polar argues they should not need to. The LLM is no longer trusted to know the palette. It is handed a vocabulary where most of the words it can say are the right ones.
Real problems remain, and Polar names them plainly. The closed token sets run too small for some of the UI they actually build, so tokens are being added weekly while the team watches for the point where the constraint costs more than it saves. Most of the app still runs legacy Tailwind, migrating file by file as they touch things instead of one big rewrite. Every escape hatch allowed is a crack in the guarantee, so oxlint-disable lines get audited, and a growing list is treated as a bug in the design system itself.
Polar compresses the whole bet into one line. A design system used to be a set of decisions you hoped people would follow. Hoping stops being a strategy when most of your code is written by LLMs. A design system has to become a set of decisions that are the only ones expressible.
Implementation checklist
Ready to apply this to your own project? The steps below reproduce the Orbit approach end to end.
-
Inventory your current design decisions. List intents such as “card surface,” “de-emphasized text color,” and “stacked gap,” and give each one a name. -
Define tokens as closed sets with stylex.defineVars, usinglight-dark()so every color carries both themes in one value. -
Build a base Boxcomponent that accepts tokens as typed props and rejects raw numbers and hex codes on styling props. -
Make Boxpolymorphic through anasprop covering every HTML element you plan to ban. -
Write an Oxlint rule such as polar/no-raw-html-layoutat error level to block raw HTML layout elements. -
Wire the rules into CI so a PR only merges when everything is green. -
Audit oxlint-disablelines regularly and treat a growing list as a design-system bug. -
When a token is missing, add the token first. Migrate legacy Tailwind file by file as you touch it instead of attempting a big rewrite.
FAQ
Does Orbit replace Tailwind entirely?
New code uses StyleX and Orbit. Legacy Tailwind code stays in place and migrates file by file. Polar’s assessment of Tailwind itself is positive, and the switch happened because the author changed from human to model.
Why doesn’t CLAUDE.md or a system prompt work as enforcement?
Documented rules behave as probabilities. The model follows them most of the time, and across thousands of generations the misses accumulate into style drift again. CI rules form a deterministic contract where green means merge.
Does banning the raw <div> hurt HTML semantics?
No. Box is polymorphic through its as prop and renders real <nav>, <ul>, and <section> elements with typed, forwarded DOM props. Only the open string surface disappears.
What happens when an LLM mistypes a token name?
TypeScript raises a type error during authoring. The failure moves from a visual regression found weeks after launch to a red squiggle at typing time.
How does Orbit prevent dark mode bugs?
Color tokens use the native CSS light-dark() function and carry both theme values in one definition. backgroundColor="background-card" written once renders correctly in both themes, leaving no separate dark-mode code to get wrong.
What if the design system lacks a needed value?
Polar’s default is to add a token. Each allowed escape hatch weakens the guarantee, and the boundary between legitimate escape and laziness is still being drawn.
What problems does Orbit still have?
Three, by Polar’s own account. Closed token sets run too small for some UI, so tokens are added weekly while the team watches the cost of constraint. Legacy Tailwind migrates file by file. The oxlint-disable list gets audited as a design-system bug.
Is StyleX essential to the approach?
Polar treats StyleX as the mechanism. The core idea is tokens as types, and any styling solution supporting typed props with compile-time checks could carry the same design.

