# Prompt to Blade UI

**AI design agent · Razorpay AI × Design Challenge · 2026 · Arunkumar Alagarsamy**

> Autonomy bounded by the system, not by the prompt.

Type your prompt → Claude creates a JSON description of a screen → real Blade
components rendered on screen. Built for Razorpay's Blade Build Challenge, in about
two hours.

## Project links

- Live demo: https://razorpay-challenge.vercel.app/
- GitHub: https://github.com/arun-dev-des/prompt-to-blade-ui
- Blog: https://www.linkedin.com/pulse/how-i-got-ai-turn-prompt-razorpays-blade-ui-without-ever-alagarsamy-ywwac/
- Schema playground: https://razorpay-challenge.vercel.app/schema-playground.html
- Deployed on Vercel

## Reach

| Metric | Value |
|---|---|
| Impressions | 30K+ |
| Reached | 19K+ |
| Tried it live | 593 |

## The problem

By default, an LLM is a free-form text generator. Ask it to "design a payment
screen" and it might hand back code with hard-coded colours like `#00ff00`, random
pixel sizes, a layout that ignores your design system, or a component that doesn't
exist. Often useful — but unpredictable. You can't guarantee it'll be on-brand.

## The technique

Don't let it write freely — make it fill a form. The Anthropic API has a feature
called **structured outputs**: you hand the model a schema (a rulebook), and the
reply is forced to match it. Claude chooses what goes in the blanks, but it can't
add a blank that isn't on the form — the API rejects anything off-shape.
**The constraint, not the prompt, is what keeps the output on-system.**

`UI_SCHEMA` is that strict form — the rulebook the reply is forced to match.
Everything Claude is allowed to return is defined in it, and anything off-shape is
rejected.

## What's inside UI_SCHEMA

`UI_SCHEMA` is a component library written as rules — a fixed set of elements, each
with a fixed set of props, and `additionalProperties: false` at every level.

```ts
export const UI_SCHEMA = {
  type: 'object',
  additionalProperties: false,          // anything off-shape is rejected outright
  required: ['title', 'sections'],
  properties: {
    title:    { type: 'string' },
    subtitle: { type: 'string' },
    sections: { type: 'array', items: {
      additionalProperties: false,
      required: ['elements'],
      properties: {
        variant:  { enum: ['card', 'plain'] },
        layout:   { enum: ['stack', 'grid'] },
        heading:  { type: 'string' },
        elements: { type: 'array', items: { anyOf: [

          // every element Claude may place — and nothing else is valid
          { kind: 'heading',   text,  size?  small | medium | large },
          { kind: 'text',      text,  tone?  normal | subtle | muted },
          { kind: 'amount',    value, label?, currency? },
          { kind: 'statTile',  label, value, currency?, delta?, up? },
          { kind: 'badge',     text,  color? positive | negative | notice |
                                        information | neutral | primary },
          { kind: 'button',    text,  variant? primary | secondary | tertiary, icon? },
          { kind: 'input',     label, placeholder?, inputType? text | email | password | textarea },
          { kind: 'switchRow', title, description?, on? },
          { kind: 'person',    name,  subtitle? },
          { kind: 'feature',   text },
          { kind: 'divider' },

        ]}},
      },
    }},
  },
} as const;

// note: even 'color' is a semantic token — positive, notice — never a hex.
```

## Blade Schema Playground

Explaining a guardrail is weaker than letting someone hit one — so I built a
playground. You assemble a screen the way Claude has to, by filling the fixed form;
the exact JSON spec builds alongside; then you try to add a raw colour and feel
`additionalProperties: false` push back. It runs offline, embedded in the deck.

## The renderer — Claude's JSON becomes real Blade

The validated spec never touches the DOM directly. A deterministic renderer walks it
and swaps each `kind` for a real `@razorpay/blade` component — every colour, space
and radius resolved through Blade tokens. An unknown `kind` is skipped, so schema
drift can't crash the screen.

```tsx
// SchemaRenderer.tsx  —  a validated UIScreen  →  real @razorpay/blade components

const Element = ({ el }: { el: UIElement }) => {
  switch (el.kind) {
    case 'heading':   return <Heading size={el.size ?? 'small'} color="surface.text.gray.normal">{el.text}</Heading>;
    case 'text':      return <Text color={TEXT_COLOR[el.tone ?? 'subtle']}>{el.text}</Text>;
    case 'amount':    return <Amount value={el.value} currency={el.currency ?? 'INR'} type="heading" size="large" />;
    case 'statTile':  return <Box backgroundColor="surface.background.gray.subtle" borderRadius="medium" padding="spacing.5"> … <Amount/> <Badge/> </Box>;
    case 'badge':     return <Badge color={el.color ?? 'neutral'} emphasis="intense">{el.text}</Badge>;
    case 'button':    return <Button variant={el.variant ?? 'primary'} icon={ICONS[el.icon]} isFullWidth>{el.text}</Button>;
    case 'input':     return el.inputType === 'password' ? <PasswordInput label={el.label} /> : <TextInput label={el.label} type={el.inputType} />;
    case 'switchRow': return <Box …><Text weight="semibold">{el.title}</Text><Switch defaultChecked={el.on} /></Box>;
    case 'person':    return <Box …><Avatar name={el.name} color="primary" /><Text>{el.name}</Text></Box>;
    case 'feature':   return <Box …><CheckCircleIcon color="interactive.icon.positive.normal" /><Text>{el.text}</Text></Box>;
    case 'divider':   return <Divider />;
    default:          return null;   // unknown kind → skipped, the screen never crashes
  }
};

export const SchemaRenderer = ({ screen }: { screen: UIScreen }) => (
  <Box maxWidth="640px" marginX="auto" display="flex" flexDirection="column" gap="spacing.6">
    <Heading size="large">{screen.title}</Heading>
    {screen.subtitle && <Text color="surface.text.gray.subtle">{screen.subtitle}</Text>}
    {screen.sections.slice(0, 4).map((section, i) => <Section key={i} section={section} />)}
  </Box>
);
```

The design line: the model owns content and arrangement, the design system owns
styling — the schema is where that line is drawn.

## Reference

[Anthropic, "Building effective agents"](https://www.anthropic.com/engineering/building-effective-agents)
