Elements
Snaps are built from 16 components organized into four categories. Every component lives
in ui.elements as a named entry. The type field names the component; props carries
its configuration; children names child element IDs; on binds event handlers.
"my-element": {
"type": "text",
"props": { "content": "Hello" }
}
| # | Component | Category | Description |
|---|---|---|---|
| 1 | badge | Display | Inline label with color and icon |
| 2 | button | Display | Action button with variants and icon |
| 3 | icon | Display | Standalone icon from curated set |
| 4 | image | Display | HTTPS image with aspect ratio |
| 5 | item | Display | Content row with actions slot |
| 6 | item_group | Container | Groups items into a styled list |
| 7 | progress | Display | Horizontal progress bar |
| 8 | separator | Display | Visual divider |
| 9 | stack | Container | Vertical or horizontal layout |
| 10 | text | Display | Text block with size and weight |
| 11 | bar_chart | Data | Horizontal bar chart with labeled bars |
| 12 | cell_grid | Data | Colored cell grid, optionally interactive |
| 13 | input | Field | Text or number input |
| 14 | slider | Field | Numeric range slider |
| 15 | switch | Field | Boolean toggle |
| 16 | toggle_group | Field | Single or multi-select choice group |
Field components (input, slider, switch, toggle_group) collect user input.
Their values are sent in the POST payload under inputs[name] when a submit action
fires.
badge
Inline label with color and optional icon. Use for metadata, status indicators, and
counts alongside content. "default" (filled) draws attention; "outline" is subtler.
Pair with an icon for scannability.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
label | string | Yes | Display text. Max 30 chars | |
variant | string | No | "default" | "default" (filled) or "outline" (bordered) |
color | PaletteColor | No | "accent" | Badge color |
icon | IconName | No | Leading icon |
{ "type": "badge", "props": { "label": "New" } }
{ "type": "badge", "props": { "label": "Live", "color": "green", "icon": "zap" } }
{
"type": "badge",
"props": { "label": "ERC-20", "variant": "outline", "color": "blue" }
}
button
Fires actions via on.press — the standard way to commit user input. Default variant is
"secondary" (bordered); use "primary" (filled) for the main CTA, typically one per
page. (cell_grid also fires actions, via on.press per cell.) See
Actions for the full list of action types.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
label | string | Yes | Button text. Max 30 chars | |
variant | string | No | "secondary" | Visual style |
icon | IconName | No | Leading icon |
Variants
| Variant | Description |
|---|---|
primary | Solid accent background, white text — primary CTA |
secondary | Accent-colored border, transparent fill |
{
"type": "button",
"props": { "label": "Submit", "variant": "primary" },
"on": {
"press": { "action": "submit", "params": { "target": "https://my-snap.com/" } }
}
}
{
"type": "button",
"props": { "label": "Open" },
"on": {
"press": { "action": "open_url", "params": { "target": "https://example.com" } }
}
}
icon
Standalone icon from the curated set. Best as a visual accent inside item action slots or horizontal stacks. Avoid using icons as standalone content — pair with text or use inside a badge.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | IconName | Yes | Icon identifier | |
color | PaletteColor | No | "accent" | Icon color |
size | string | No | "md" | "sm" (16px) or "md" (20px) |
{ "type": "icon", "props": { "name": "star", "color": "amber" } }
Available Icons
| Category | Icons |
|---|---|
| Navigation | arrow-right arrow-left external-link chevron-right |
| Status | check x alert-triangle info clock |
| Social | heart message-circle repeat share user users |
| Content | star trophy zap flame gift |
| Media | image play pause |
| Commerce | wallet coins |
| Actions | plus minus refresh-cw bookmark |
| Feedback | thumbs-up thumbs-down trending-up trending-down |
image
HTTPS image with fixed aspect ratio. Use "16:9" for hero and banner images, "1:1"
for avatars or thumbnails, "4:3" for general photos, and "9:16" for tall portrait
content.
| Prop | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL. GIFs autoplay and loop. |
aspect | string | Yes | "1:1" "16:9" "4:3" "9:16" |
alt | string | No | Alt text for accessibility |
{
"type": "image",
"props": { "url": "https://example.com/photo.jpg", "aspect": "16:9" }
}
item
The go-to component for structured content rows: leaderboards, settings, key-value info.
Has a title, optional description, and an actions slot on the right side. Put badges,
buttons, or icons in children for the action slot. Items are not interactive — don't
use navigation-style affordances like chevron-right, arrow-right, or external-link
that imply the row itself is clickable.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | Yes | Primary text. Max 100 chars | |
description | string | No | Secondary text below title. Max 160 chars | |
variant | string | No | "default" | Visual style |
Variants
| Variant | Description |
|---|---|
default | No background, no border |
Children
Rendered in the actions slot (right side). Badges, buttons, and icons are all fair
game — the item itself is not clickable, so pick trailing content that reads as content
(status icon, badge, button), not navigation. Avoid chevron-right, arrow-right, or
external-link on a plain item; they imply the row itself navigates.
"score": {
"type": "item",
"props": { "title": "Engagement Score", "description": "Based on 24h activity" },
"children": ["score-badge"]
},
"score-badge": { "type": "badge", "props": { "label": "92", "color": "green" } }
"share": {
"type": "item",
"props": { "title": "Share this Snap", "description": "Pre-fill the composer" },
"children": ["share-btn"]
},
"share-btn": {
"type": "button",
"props": { "label": "Share", "icon": "share" },
"on": { "press": { "action": "compose_cast", "params": { "text": "Check out Snaps!" } } }
}
item_group
Wraps related items for visual grouping. Use separator: true for settings-style lists
and border: true for card-like sections.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
border | boolean | No | false | Show border around the group |
separator | boolean | No | false | Show divider lines between items |
gap | string | No | Spacing between items: "none" "sm" "md" "lg" |
Children: item elements only.
"results": {
"type": "item_group",
"props": {},
"children": ["r1", "r2", "r3"]
},
"r1": { "type": "item", "props": { "title": "First place", "description": "Alice" } },
"r2": { "type": "item", "props": { "title": "Second place", "description": "Bob" } },
"r3": { "type": "item", "props": { "title": "Third place", "description": "Charlie" } }
progress
Horizontal progress bar for completion, scores, or any bounded numeric value. Always uses the theme accent color. The label appears above the bar — use it for context like "78%" or "Level 3 of 5".
| Prop | Type | Required | Description |
|---|---|---|---|
value | number | Yes | Current value (0 to max, finite) |
max | number | Yes | Maximum value (must be > 0, finite) |
label | string | No | Label text above the bar. Max 60 chars |
{ "type": "progress", "props": { "value": 65, "max": 100, "label": "Upload progress" } }
separator
Visual divider between logical sections of a page. Most snaps use 2-4 separators. Overusing them creates visual clutter.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
orientation | string | No | "horizontal" | "horizontal" or "vertical" |
{ "type": "separator", "props": {} }
stack
Layout container for arranging children. Every page starts with a vertical stack as
root. Use horizontal stacks for button rows, badge groups, or side-by-side cards.
justify: "between" is useful for navigation bars.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
direction | string | No | "vertical" | "vertical" or "horizontal" |
gap | string | No | "md" | Spacing between children: "none" "sm" "md" "lg" |
justify | string | No | Content alignment: "start" "center" "end" "between" "around" |
"page": {
"type": "stack",
"props": {},
"children": ["header", "content", "actions"]
}
"row": {
"type": "stack",
"props": { "direction": "horizontal", "gap": "sm" },
"children": ["b1", "b2", "b3"]
}
text
The primary content element. Use weight: "bold" for headings and emphasis. Use
size: "sm" for captions, timestamps, and secondary info.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
content | string | Yes | Text content. Max 320 chars | |
size | string | No | "md" | "md" (body), "sm" (caption) |
weight | string | No | "normal" | "bold" "normal" |
align | string | No | "left" | "left" "center" "right" |
{ "type": "text", "props": { "content": "Welcome to Snaps", "weight": "bold" } }
{
"type": "text",
"props": { "content": "Last updated 2 hours ago", "size": "sm", "align": "center" }
}
bar_chart
Horizontal bar chart for displaying ranked or comparative data. Each bar shows a label on the left, a colored fill bar, and a numeric value on the right. Use for poll results, leaderboards, or any ranked values.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
bars | object[] | Yes | 1–6 bar entries (see below) | |
max | number | No | max value | Upper bound for bar scale |
color | PaletteColor | No | "accent" | Default bar color |
Bar Object
| Prop | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Bar label. Max 40 chars |
value | number | Yes | Bar value (≥ 0) |
color | PaletteColor | No | Per-bar color override |
{
"type": "bar_chart",
"props": {
"bars": [
{ "label": "Poblano", "value": 42 },
{ "label": "Negro", "value": 38 },
{ "label": "Verde", "value": 15, "color": "green" }
]
}
}
cell_grid
Grid of colored cells for pixel art, game boards, color pickers, and small data matrices. Cells are defined sparsely — only specify cells that have color or content.
There are two interaction modes, and they are mutually exclusive:
- Press to act — leave
selectat"off"and bindon.press. Each press writes"row,col"toinputs[name]and fires the bound action (e.g.submit). No selection ring; cells behave like a grid of buttons. - Press to select — set
select: "single"or"multiple". Each press accumulates selection state with a visual ring; nothing is posted. Pair with a separatebuttonthat submits the accumulated selection.
Don't combine on.press with a non-"off" select mode — the auto-fire would defeat
the accumulating selection. The component ignores on.press whenever select is on.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | No | "grid_tap" | POST inputs key for the tapped or selected cells |
cols | number | Yes | Column count (2–32) | |
rows | number | Yes | Row count (2–16) | |
cells | object[] | Yes | Sparse cell definitions (see below) | |
gap | string | No | "sm" | Cell spacing: "none" (0px) "sm" (1px) "md" (2px) "lg" (4px) |
rowHeight | number | No | 28 | Pixel height per row (8–64). Grid height = rows × rowHeight |
select | string | No | "off" | Selection mode: "off" (use with on.press) "single" "multiple" |
Events
| Event | Fires when |
|---|---|
press | A cell is pressed, only when select is "off". inputs[name] is set to "row,col" before the action runs, so a bound submit POST includes the pressed cell. |
Cell Object
| Prop | Type | Required | Description |
|---|---|---|---|
row | number | Yes | Row index (0-based) |
col | number | Yes | Column index (0-based) |
color | PaletteColor | No | Cell fill color |
content | string | No | Cell text content |
Press to act — select: "off" (default) + on.press. Each press fires immediately:
{
"type": "cell_grid",
"props": {
"name": "color_grid",
"cols": 4,
"rows": 4,
"cells": [
{ "row": 0, "col": 0, "color": "red" },
{ "row": 0, "col": 3, "color": "blue" }
]
},
"on": {
"press": { "action": "submit", "params": { "target": "https://my-snap.com/" } }
}
}
Press to select — select: "single" or "multiple". Presses update the selection
without posting; pair with a button that submits when the user is done:
{
"type": "cell_grid",
"props": {
"cols": 4,
"rows": 4,
"cells": [
{ "row": 0, "col": 0, "color": "red" },
{ "row": 0, "col": 3, "color": "blue" },
{ "row": 1, "col": 1, "color": "green", "content": "X" },
{ "row": 3, "col": 3, "color": "purple" }
],
"select": "multiple"
}
}
input
Text or number input field for short free-text entry. Set type: "number" for numeric
input (changes the mobile keyboard). Always provide a label for accessibility. Value
is collected in POST inputs under name.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | Input name (POST inputs key) | |
type | string | No | "text" | "text" or "number" |
label | string | No | Label text above input. Max 60 chars | |
placeholder | string | No | Placeholder text. Max 60 chars | |
defaultValue | string | No | Pre-filled value | |
maxLength | number | No | Max character count (1–280) |
POST value: string.
{
"type": "input",
"props": { "name": "email", "label": "Email", "placeholder": "you@example.com" }
}
slider
Numeric range slider for bounded choices like ratings, quantities, or percentages.
Always set meaningful min/max values and add a label so users know what they're
adjusting. Value is collected in POST inputs under name.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | Slider name (POST inputs key) | |
min | number | Yes | Minimum value (must be ≤ max) | |
max | number | Yes | Maximum value (must be ≥ min) | |
step | number | No | 1 | Increment step (must be > 0, finite) |
defaultValue | number | No | midpoint | Initial value (must be between min and max) |
label | string | No | Label text above slider. Max 60 chars | |
showValue | boolean | No | false | Display the current value next to the label |
POST value: number.
{
"type": "slider",
"props": {
"name": "rating",
"label": "Rating (1–10)",
"min": 1,
"max": 10,
"showValue": true
}
}
switch
Boolean toggle for binary preferences (on/off, yes/no). Good for settings-style pages —
the label should describe the enabled state ("Enable notifications", not "Notifications
toggle"). Value is collected in POST inputs under name.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | Switch name (POST inputs key) | |
label | string | No | Label text beside the switch. Max 60 chars | |
defaultChecked | boolean | No | false | Initial checked state |
POST value: boolean.
{
"type": "switch",
"props": { "name": "notifications", "label": "Enable notifications" }
}
toggle_group
Choice group for selecting between 2-6 discrete options. Prefer this over multiple
buttons when the choices are parallel and exclusive. Use multiple: true for
multi-select scenarios like tags or interests. Value is collected in POST inputs under
name.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | Group name (POST inputs key) | |
options | string[] | Yes | Choice labels. Min 2, max 6. Each max 30 chars | |
multiple | boolean | No | false | Allow multiple selections |
orientation | string | No | "horizontal" | "horizontal" or "vertical" |
defaultValue | string or string[] | No | Pre-selected option(s) | |
variant | string | No | "default" | "default" (solid) or "outline" (bordered) |
label | string | No | Label text above the group. Max 60 chars |
POST value: the selected option string (or string[] when multiple is true).
{
"type": "toggle_group",
"props": {
"name": "plan",
"label": "Choose a plan",
"options": ["Free", "Pro", "Team"]
}
}
{
"type": "toggle_group",
"props": {
"name": "interests",
"label": "Select interests",
"multiple": true,
"orientation": "vertical",
"options": ["Dev", "Design", "Data", "Product"]
}
}