DEV Community

Cover image for AI-written styles typecheck. Some still render nothing
Maxim Procopenco
Maxim Procopenco

Posted on Originally published at box-kite.dev

AI-written styles typecheck. Some still render nothing

Box Kite's MCP server runs your AI assistant's props through the real styling engine mid-task, and names each value that would render nothing. Below: the mistakes it catches, a worked session with every answer pasted from the server, and what it can't know about your project. It documents @box-kite/mcp 2.2.12.

The examples here are screenshots; they run live in the original article.

The problem

Three props an assistant wrote for a settings row, handed to the server:

check_styles { "props": { "jc": "between", "fontSize": 4, "lineHeight": 1.5 } }

Some of these write no CSS, or CSS they almost certainly did not mean.

- ❌ `jc` does not accept "between" — no rule and no class name were written. It takes start, end, flex-start, flex-end, center, left, right, space-between, space-around, space-evenly, stretch, safe center, … (17 in all — call `get_props`).
- ⚠️ `fontSize` → `.fontSize-4{font-size:0.25rem}` — fontSize={4} is 4px text: fontSize's divider is 16, not the spacing scale's 4, so the number is the pixel size. For 16px write fontSize={16}.
- ⚠️ `lineHeight` → `.lineHeight-1\.5{line-height:1.5px}` — lineHeight={1.5} is a 1.5px line: lineHeight is direct pixels, not the unitless multiple CSS also takes. Write the pixels (lineHeight={24} is 24px), lineHeight="font-size" for a multiple of 1, or css={{ lineHeight: 1.5 }} for any other multiple.
…
Enter fullscreen mode Exit fullscreen mode

And here is what those props draw:

The three props as the engine draws them, each next to what was meant: the row stays bunched, the text is 4px, and the lines overlap.

The two numbers typecheck, the build passes, and nothing fails loudly. In development the engine logs a [box-kite] warning once the component renders. A production build says nothing.

Why a server and not a docs file

Box Kite's prop names look like the utility classes you already type, so an assistant brings its habits along. Three of them fail:

  • Each prop takes a closed set of values. Anything else writes no rule and no class name, so a typo can't become a broken declaration.
  • Numbers mean different units per prop. p={4} is 1rem (÷4), fontSize={14} is 14px (÷16), b={1} and lineHeight={24} are direct pixels.
  • HTML attributes go in props. A top-level href or data-* typechecks and is then dropped.

The package ships a rules file, but an agent reads it once, at the start of a task. It can ask a server at the moment the question comes up. No document can answer "does this exact value work?" either. The server bundles the engine of its own release, and check_styles runs each prop on a fresh engine of its own.

The four moments in a task when an agent asks the server, and the tool for each.

Install

Node 22 or newer. No key, no network. In Claude Code:

claude mcp add box-kite -- npx -y @box-kite/mcp
Enter fullscreen mode Exit fullscreen mode

Cursor, VS Code, Codex, Zed and any other stdio client:

{
  "mcpServers": {
    "box-kite": { "command": "npx", "args": ["-y", "@box-kite/mcp"] }
  }
}
Enter fullscreen mode Exit fullscreen mode

On connect, the server also hands the model the library's four core facts (the dividers, no style, the element comes from the component, attributes in props), plus "before believing a value, call check_styles".

A settings panel, start to finish

1. Find the component.

search_docs { "query": "toggle switch" }

- **Switch** (component) — The APG switch: an on/off control drawn as a track and a thumb.
…
Enter fullscreen mode Exit fullscreen mode

2. Read its contract. get_component returns props, parts, keyboard map and ARIA. Two rows change the code:

get_component { "name": "Switch", "include": ["props", "a11y"] }
…
| `label` | `React.ReactNode` | — | The text beside the switch, rendered inside a label that wraps the input. |
| `labelProps` | `BoxProps<'label'>` | — | Styles for the wrapping `<label>`: the row's layout, not the track's own appearance. |
…
Enter fullscreen mode Exit fullscreen mode

So the row needs no Flex and Span of its own. The label names the switch and makes the whole row clickable.

3. The first draft, written from habit:

import Flex from '@box-kite/react/components/flex';
import { H2, Span } from '@box-kite/react/components/semantics';
import Switch from '@box-kite/react/components/switch';

export function NotificationSettings() {
  return (
    <Flex d="column" p={6} rounded="xl" shadow="md" bgColor="white" color="gray-900" borderColor="gray-150">
      <H2 fontSize={5} fontWeight="semibold">
        Notifications
      </H2>
      <Flex jc="between" ai="center" py={3} borderBottom={1} data-setting="email">
        <Span lineHeight={1.5}>Email me about new comments</Span>
        <Switch name="comments" defaultChecked />
      </Flex>
    </Flex>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Check it, one check_styles call per element. The verdicts, summarized:

Draft Verdict Fix
rounded="xl" ❌ no prop of that name borderRadius={3}
borderColor="gray-150" ❌ not a palette step "gray-200"
fontSize={5} ⚠️ 5px text fontSize={20}
fontWeight="semibold" ❌ takes 100 to 900 fontWeight={600}
jc="between" ❌ not accepted jc="space-between"
borderBottom={1} ❌ no prop; suggests borderColor bb={1}
data-setting="email" ⚠️ HTML attribute, dropped props={{ 'data-setting': 'email' }}
lineHeight={1.5} ⚠️ a 1.5px line lineHeight={20}

One of those answers, as the agent sees it:

check_styles { "props": { "jc": "between", "ai": "center", "py": 3, "borderBottom": 1, "data-setting": "email" } }

Some of these write no CSS, or CSS they almost certainly did not mean.

- ❌ `jc` does not accept "between" — no rule and no class name were written. It takes start, end, flex-start, flex-end, center, left, right, space-between, space-around, space-evenly, stretch, safe center, … (17 in all — call `get_props`).
- ✅ `ai` → `.ai-center{align-items:center}`
- ✅ `py` → `.py-3{padding-block:0.75rem}`
- ❌ `borderBottom` no prop of that name. Try: `borderColor`.
- ⚠️ `data-setting` an HTML attribute, not a style prop. Written at the top level it typechecks and is then dropped — it goes in `props={{ "data-setting": … }}`.
…
Enter fullscreen mode Exit fullscreen mode

TypeScript catches the ❌ rows, one error per element, so it takes a few rounds. It never catches the ⚠️ rows. Two fixes came from elsewhere: search_docs for "rounded corners" returns borderRadius first, but nothing led to bb. Once you know the name, get_props confirms that bb={1} writes border-bottom-width:1px.

5. The second draft. Every call now answers "Every prop writes CSS.":

The first draft as the engine renders it, beside the version that passed check_styles.

import Flex from '@box-kite/react/components/flex';
import { H2 } from '@box-kite/react/components/semantics';
import Switch from '@box-kite/react/components/switch';

const row = {
  width: 'fit',
  d: 'row-reverse',
  jc: 'space-between',
  gap: 3,
  py: 3,
  bb: 1,
  borderColor: 'gray-200',
  fontSize: 14,
  lineHeight: 20,
} as const;

export function NotificationSettings() {
  return (
    <Flex d="column" p={6} borderRadius={3} shadow="md" bgColor="white" color="gray-900" b={1} borderColor="gray-200">
      <H2 fontSize={20} fontWeight={600} mb={2}>
        Notifications
      </H2>
      <Switch name="comments" defaultChecked label="Email me about new comments" labelProps={row} props={{ 'data-setting': 'email' }} />
      <Switch name="digest" label="Weekly digest" labelProps={row} props={{ 'data-setting': 'digest' }} />
    </Flex>
  );
}
Enter fullscreen mode Exit fullscreen mode

d: 'row-reverse' puts the text first, because the label renders the input before it.

To skip the typing altogether, get_blocks has a ready settings panel: get_blocks { "name": "settings-form" } answers with npx shadcn@latest add https://www.box-kite.dev/r/settings-form.json.

Field notes from a production app

From a team running the server on a real app, each checked against the source.

Pin the server next to the library. Each release builds @box-kite/mcp from the same commit, at the same version, with that release's engine inlined. npx -y @box-kite/mcp runs whatever version npx resolves, not the one in your lockfile. So the team installed it as a dev dependency at the library's version (npm install -D @box-kite/mcp@2.2.12) and pointed the project's .mcp.json at its bin:

{
  "mcpServers": {
    "box-kite": { "command": "npx", "args": ["box-kite-mcp"] }
  }
}
Enter fullscreen mode Exit fullscreen mode

Your own tokens look wrong to it. The app's colours came from Box.extend():

check_styles { "props": { "bgColor": "ui-primary/20", "borderColor": "ui-primary" } }

Some of these write no CSS, or CSS they almost certainly did not mean.

- ❌ `bgColor` does not accept "ui-primary/20" — no rule and no class name were written. It takes currentColor, transparent, green, red, blue, gray, black, white, vi, slate-50, slate-100, slate-200, … (326 in all — call `get_props`).
…
Enter fullscreen mode Exit fullscreen mode

The server can't load your configuration. It reads no config file, environment variable or argument, so read its rejection of your own token as "unknown", not "wrong". What still works:

  • Every other prop in the bag is judged on its own, so those verdicts stand.
  • Your app's engine knows your tokens. With Box.extend({ 'ui-primary': '#3355ff' }, {}, {}), bgColor="ui-primary/20" renders with no warning, and the typo ui-primry/20 gets a [box-kite] … wrote no CSS warning in development. For your own tokens, the console is the check.
  • A design that only recolours the components can use Box.palette(). Its accent-* and base-* tokens are built in, and check_styles accepts them.

get_blocks goes through the shadcn CLI. The server installs nothing. The CLI copies a block's files into your project as code you own. That suits starting a new page, not editing an existing one. It needs a components.json (setup on the registry page), and installed blocks don't update.

The honest limits

  • Values, not layouts. jc="space-between" passes whether or not the element is a flex container.
  • Not your project. Your Box.extend() tokens and props and your Box.components() designs are invisible to it.
  • Keyword search. "on/off toggle" didn't find Switch, and "border bottom" never found bb. Searching by the CSS property name works best.
  • Gaps in coverage. VisuallyHidden and the SVG elements have no get_component entry, and the tool says so.
  • It can't make the agent ask. Ask for check_styles in your prompt or your rules file.

Try it

Built for AI runs check_styles in your browser on the same engine, before you install anything. Then:

claude mcp add box-kite -- npx -y @box-kite/mcp
Enter fullscreen mode Exit fullscreen mode

Originally published on box-kite.dev. Box Kite is open source under MIT — if it is useful to you, a star on GitHub helps other people find it. Install it with npm install @box-kite/react.

Top comments (0)