Elements and nesting
A SayShape spec is a list of lines. Each line is one element: its type, then optional quoted text, then name=value props.
button "Save changes" variant=secondary on=emit:saveProps
A prop value can be written three ways:
| Form | Example | Use it for |
|---|---|---|
| Bare | variant=secondary | Single words and numbers |
| Quoted | label="Water (L)" | Text with spaces or symbols |
| Expression | value={price * qty} | Anything computed from state (Expressions) |
Quotes inside quoted text are escaped with a backslash. Quoted text can also include {expressions}, which are filled in from state:
state guests=2 nights=3
text "Cabin for {guests} guests, {nights} nights"
text "She said \"see you there\"" tone=mutedEach component accepts the props listed in the component reference, and every element also accepts if= (Conditional visibility) and tip=. An unknown prop or a value outside a prop's allowed list is a warning: the element still renders and the bad part is ignored.
Tips
Every element accepts tip=, a short explanation shown in a small bubble over it on hover, on keyboard focus, or on a tap on a touch screen. Escape hides it. An element that isn't pressable becomes a Tab stop when it has a tip, so the keyboard can reach it, and screen readers read the tip after the element's name. Use tips for what a label or a number means, not for anything people must read to use the screen.
state open=14
card
row
stat label="Open orders" value={open} tip="Paid, not yet handed to the carrier"
icon name=info tip="Counts refresh every 5 minutes"
button "Ship all" on=set:open:0 tip="Marks every open order shipped"Nesting
Indent children under their container. Two spaces is the convention; a tab counts as two spaces.
card
text "Your order" style=title
row
button "Cancel" variant=secondary
button "Pay"The top level of a spec behaves like a column: unindented elements stack vertically. Some elements only go inside a particular parent, like list-item inside list or tab inside tabs; the reference says where each one can go. Elements can nest at most 100 levels deep.
Indentation is the only structure. There are no closing lines, which is what lets a half-written spec render: every line is complete on its own (Streaming).
Lines that aren't elements
| Line | Meaning |
|---|---|
state name=value … | Declares state (State) |
derive name={expression} … | Declares a derived value (Derived values) |
each items=… as=… | Repeats what's indented under it (Lists) |
if={condition} | Shows what's indented under it only while the condition holds (Conditions) |
# a comment | Ignored |
# Starting values come first.
state ready=false
card
check "Ready to go" bind=ready
text "Let's go!" tone=good if=readyNo colors, fonts or sizes
Specs never set colors, fonts or pixel sizes. They choose roles instead, and the theme decides what each role looks like:
text "Section" style=title
text "Saved" tone=good
text "Card declined" tone=bad
button "Delete" variant=danger
badge "New" tone=accentA few layout props take numbers (gap=, pad=, size=, min= on a grid), which are spacing in pixels within the theme's design, not styling. This keeps every generated screen consistent with your app, and it's why a request for "a big red button" can't be followed literally.