helix-ui prompt DSL
JSX is verbose. When a model is emitting helix-ui UI inside a chat reply, every angle bracket eats tokens that could go to actual reasoning. The helix-ui prompt DSL exists to make UI cheap to describe in prose while staying losslessly convertible to JSX.
It is not a templating language and not a runtime. It is a parser + emitter validated against the same components-manifest.json that powers /components.md. If a component, prop, or modifier doesn’t exist in helix-ui, the parser errors out with a line:column.
A side-by-side
DSL:
Stack gap=4 Text.lg.semibold "Welcome back" Text.muted "Sign in to continue." Form TextInput name=email label=Email TextInput name=password label=Password type=password Button.brand.lg type=submit "Sign in"JSX (emitted):
<Stack gap={4}> <Text size="lg" weight="semibold">Welcome back</Text> <Text tone="muted">Sign in to continue.</Text> <Form> <TextInput name="email" label="Email" /> <TextInput name="password" label="Password" type="password" /> <Button tone="brand" size="lg" type="submit">Sign in</Button> </Form></Stack>How modifiers work
.brand.lg means “set whichever prop’s enum contains brand, then whichever contains lg”. The DSL reads each component’s prop types from the manifest and decides — tone="brand", size="lg". If a value is ambiguous (multiple enums claim it), the parser errors and asks you to spell the prop out.
Children, slots, and booleans
- Indentation = nesting (2 spaces per level).
- Dotted sub-components (
Sidebar.Item,Tabs.Panel) are written verbatim. - A bareword that matches a known boolean prop becomes
prop={true}. Common ones (active,disabled,isOpen) work everywhere. - A trailing
"..."becomes the text child. A trailing{js}becomes a JSX expression child.
CLI
# DSL → JSXpnpm --filter @helix-ui/prompt run parse <file.dsl># ornode packages/prompt/bin/cli.mjs parse <file.dsl>
# JSX → DSL (lossy, for compact display)node packages/prompt/bin/cli.mjs reverse <file.tsx>Pipe a snippet through stdin: echo 'Button.brand "Save"' | helix-ui-prompt parse -.
Programmatic API
import { dslToJsx, jsxToDsl, parse, emit } from '@helix-ui/prompt';
const jsx = await dslToJsx('Button.brand "Save"');// → <Button tone="brand">Save</Button>
const dsl = await jsxToDsl('<Stack gap={4}><Text>Hi</Text></Stack>');// → Stack gap={4}// Text "Hi"
// Parse + emit separately if you want to walk / transform the AST.const ast = await parse(source);const code = emit(ast, { includeImport: true });When not to use it
- For production code, write JSX. The DSL is for prompts and inline conversation.
- For anything you’ll read tomorrow, write JSX. The terseness costs readability after the fact.
- For components that aren’t in helix-ui, write JSX — the DSL only validates against helix-ui’s manifest.
Errors you’ll see
| Code | Cause |
|---|---|
UnknownComponent | Not in components-manifest.json. |
UnknownProp | The component (without ...rest) doesn’t declare that prop. |
AmbiguousModifier | A .foo modifier matches multiple enums on the component. Spell it out. |
BadValue | A value doesn’t match the declared type. |
BadIndent | Indent jumped more than one level, or wasn’t a multiple of 2. |
Roundtrip examples
The reference roundtrip set is in packages/prompt/test/parse.test.mjs. Every component’s prompt_examples block in spec.md is also a roundtrip-compatible JSX seed.