Let a model choose, never write, your styles
When a language model builds part of your interface, give it a typed menu. Pleat recipes describe their props with Schema, so the model picks among styles that are already compiled, and its answer is decoded before it reaches a view.
Closed-world styling
A model that writes CSS or utility classes can produce anything: rules your stylesheet never shipped, values that break the layout, or text that escapes a declaration. A model that fills in recipe props can only produce combinations that were compiled when the page loaded.
- Every option a model can name already has rules, so generated UI needs no build step and no runtime CSS from model output.
- Props are Effect Schemas, and
Recipe.jsonSchemagives the JSON Schema for structured output and tool calls. - Descriptions on recipes and dimensions travel into the JSON Schema, so the model reads your design guidance next to the options.
- Theme values are checked by their token kinds, so a generated theme can only contain colors, lengths, and durations.
Describe the menu
Compose recipe schemas into the shape you want back. Here a card has content plus props for two recipes: its surface, and a button for its action.
import { Schema } from 'effect'
import { Recipe, Style, When } from '@pleat/core'
import { button, tokens } from '../design.ts'
const { color, radius, space, text } = tokens
// The menu a model chooses from. Every option is compiled before it
// is chosen.
export const surface = Recipe.make({
name: 'Surface',
description: 'How a card sits on the page.',
base: Style.make({
display: 'grid',
borderRadius: radius.lg,
border: '1px solid',
}),
variants: {
tone: {
Plain: {
backgroundColor: color.surface,
color: color.ink,
borderColor: color.line,
},
Accent: {
backgroundColor: color.accentSoft,
color: color.ink,
borderColor: color.accent,
},
Inverted: {
backgroundColor: color.ink,
color: color.canvas,
borderColor: color.ink,
},
},
density: {
Compact: {
gap: space[2],
padding: space[4],
fontSize: text.sm,
},
Roomy: {
gap: space[4],
padding: space[6],
fontSize: text.md,
},
},
isFeatured: {
true: Style.make({
boxShadow: `0 0 0 3px ${color.accent}`,
}).pipe(
Style.when(When.motionSafe, {
transition: 'box-shadow 200ms ease',
}),
),
false: Style.empty,
},
},
defaults: { tone: 'Plain', density: 'Roomy', isFeatured: false },
descriptions: {
tone:
'Plain for most cards, Accent to draw attention, ' +
'Inverted for one hero card.',
density:
'Compact in dense grids, Roomy when the card stands alone.',
isFeatured:
'Rings the card. Use for at most one card on a screen.',
},
})
// What the model returns: content, plus props for recipes it may
// use.
export const CardSpec = Schema.Struct({
title: Schema.String.check(
Schema.isNonEmpty(),
Schema.isMaxLength(60),
),
body: Schema.String.check(Schema.isMaxLength(280)),
surface: surface.schema,
action: Schema.optionalKey(
Schema.Struct({
label: Schema.String.check(
Schema.isNonEmpty(),
Schema.isMaxLength(24),
),
tone: button.schema.fields.tone,
}),
),
}).annotate({
identifier: 'Card',
description: 'One card in a generated dashboard.',
})
export type CardSpec = typeof CardSpec.TypeAsk a model
effect/ai turns the schema into structured output for any provider and decodes the answer. The same schema decodes output from anywhere else, such as a stream or a cache.
import { Effect, Schema } from 'effect'
import { LanguageModel } from 'effect/ai'
import { CardSpec } from '../demo/card.ts'
// Ask any provider's model for structured output that matches the
// schema. Its JSON Schema only admits recipe options that already
// have compiled CSS.
export const generateCard = (request: string) =>
LanguageModel.generateObject({
objectName: 'Card',
prompt: `Design one dashboard card for: ${request}`,
schema: CardSpec,
}).pipe(Effect.map(response => response.value))
// Output from anywhere else (a stream, a cache, a tool call) is
// decoded the same way.
export const decodeCard = Schema.decodeUnknownEffect(CardSpec)Try it
The text below stands in for a model’s answer. Edit it, or pick an example. The view decodes it with CardSpec on every change and renders the card with the surface and button recipes.
Checkout conversion is up 4.2%
The new single-page checkout shipped Tuesday. Mobile drop-off fell the most.