Components
Complete reference of all built-in Hypen components
Components
Hypen provides a set of built-in components for building UIs. This guide covers all available components.
Layout Components
Column
Arranges children vertically.
Column {
Text("First")
Text("Second")
Text("Third")
}
.gap(12)Row
Arranges children horizontally.
Row {
Text("Left")
Spacer()
Text("Right")
}
.gap(12)Box / Container
Generic container with z-axis stacking (overlay).
Box {
Image("background.jpg")
Text("Overlay")
}Center
Centers content both horizontally and vertically.
Center {
Spinner()
}
.fillMaxSize(true)Stack
Overlays children on the z-axis.
Stack {
Card { Text("Base") }
Badge("New")
}Grid
Arranges a bound collection in a grid — data-driven, like List.
Grid(@state.cells, key: "id") {
Card { Text("@{item.label}") }
}
.gridColumns(2)
.gap(16)List
Scrollable, data-driven list — shorthand for
ForEach(items: @state.items, key: "id").
List(@state.items) {
Text("@{item.title}")
}Spacer
Flexible empty space.
Row {
Text("Left")
Spacer()
Text("Right")
}Divider
Visual separator line.
Column {
Text("Section 1")
Divider()
Text("Section 2")
}SafeArea
Full-size vertical container that pads its content by the device safe-area insets (notch, status bar, home indicator). Use it as the outermost container of a screen.
SafeArea {
Column {
Text("Clear of the notch and the home indicator")
}
}Pass edges to inset only some sides — omitted or empty means all four:
SafeArea(edges: ["top"]) {
Text("Only the top inset is applied")
}Embedders can override the insets per edge; unspecified edges keep the platform
default. See Layout → SafeArea for the per-renderer
override APIs and the iOS Safari viewport-fit=cover caveat.
Content Components
Text
Displays text.
Text("Hello World")
.fontSize(18)
.fontWeight("bold")
.color("#333333")Heading
Heading text with larger default styling.
Heading("Page Title")
.fontSize(32)Paragraph
Paragraph text with appropriate line spacing.
Paragraph("Long text content that spans multiple lines...")
.lineHeight(1.6)Image
Displays an image.
Image("https://example.com/photo.jpg")
.width(200)
.height(200)
.borderRadius(8)Input Components
Button
Clickable button.
Button {
Text("Submit")
}
.onClick(@actions.submit)
.backgroundColor("#3B82F6")
.padding(12)
.borderRadius(6)Input
Single-line text input.
Input(placeholder: "Enter your name")
.value(@state.name)
.onInput(@actions.updateName)
.padding(12)
.borderRadius(4)Textarea
Multi-line text input.
Textarea(placeholder: "Enter message...")
.onInput(@actions.updateMessage)
.height(100)Checkbox
Toggle checkbox.
Checkbox {}
.checked(@state.agreed)
.onChange(@actions.toggleAgreed)Switch
Toggle switch.
Switch {}
.checked(@state.darkMode)
.onChange(@actions.toggleDarkMode)Select
Dropdown selection.
Select {}
.value(@state.country)
.onChange(@actions.updateCountry)Slider
Range slider.
Slider {}
.value(@state.volume)
.onChange(@actions.updateVolume)Display Components
Card
Container with elevation and rounded corners.
Card {
Column {
Text("Card Title")
Text("Card content")
}
.padding(16)
}Badge
Small label indicator.
Badge("3")
.backgroundColor("#EF4444")Avatar
User avatar display.
Avatar("https://example.com/user.jpg")
.size(48)
.borderRadius(24)Spinner
Loading indicator.
Spinner()
.size(24)ProgressBar
Progress indicator.
ProgressBar(value: @state.progress)
.height(4)Icon
SVG icon rendered from a registered resource. Icons are registered server-side
via resources() or resourcesDir() and resolved at render time — the client
receives pre-resolved SVG path data, not raw SVG strings.
Icon(@resources.heart)
.size(24)
.color("#ed4956")Reference a resource by name with the @resources prefix:
Icon(@resources.search)
.size(20)
.color("#8e8e8e")
Icon(@resources.plus-square)
.size(28)You can also use a static string (resolved against the same registry):
Icon("heart")
.size(24)Props:
@resources.nameor"name"— resource name (positional)size— icon size in pixels (default: 24)color— icon color (default: "currentColor")
Registering resources (server-side):
// TypeScript
const server = new RemoteServer()
.resources({
heart: '<svg viewBox="0 0 24 24"><path d="M20.84 4.61..."/></svg>',
})
// Or load from a JSON file (name → SVG map)
await server.resourcesFile("./resources.json")
// Or load all .svg files from a directory
await server.resourcesDir("./icons/")Media Components
Video
Video player. Streams a resolved URL — or an ordered playlist of URLs with
auto-advance — using the platform's native media stack. Media payloads never
cross the wire, only URLs.
Video("https://example.com/video.mp4")
.width(640)
.height(360)Props: src (or positional), playlist, startIndex, startPosition,
poster, controls, autoplay, loop, muted, preload (web hint),
headers (auth-protected streams), and title (accessible label). Optional
event actions onPlay, onPause, onEnded, onTrackChange, and onError
receive the track's src and playlist index (plus status/code/message
on errors).
Video(
playlist: ["https://cdn/ep1.mp4", "https://cdn/ep2.mp4"],
poster: "https://cdn/cover.jpg",
controls: true,
title: "Season 1",
onTrackChange: @actions.trackChanged,
onEnded: @actions.playbackDone,
onError: @actions.playbackFailed,
)startPosition seeks once, the moment the source first becomes seekable —
"resume where you left off" without any further wiring. It re-arms when the
source configuration (src, playlist, headers) changes.
Playback control:
.bind(@state.playback) is the media analogue of Input.bind — it keeps a
playback struct in sync with the player:
| Field | Type | Access | Meaning |
|---|---|---|---|
playing | boolean | read-write | Play/pause. Writing true while ended restarts from 0. |
position | number | read-write | Seconds. Writing seeks. |
duration | number | read-only | Seconds, 0 until known. |
state | string | read-only | idle, loading, playing, paused, ended, or error. |
The module must initialize the struct in defineState — binding writes into a
missing parent path are dropped:
export default app
.defineState({
streamUrl: "",
playback: { playing: true, position: 0, duration: 0, state: "idle" },
})
.onAction("togglePlay", ({ state }) => {
state.playback.playing = !state.playback.playing;
});Reports flow the other way too. position updates at most every 250 ms while
playing, so it is cheap to render; playing, state, and duration report
immediately on every transition (play, pause, seek completion, track change,
ended, error). A position write is only applied as a seek when it differs
from the player's real position by more than a second, so the renderer's own
progress reports never echo back as seeks.
Composition slots:
Children tagged with .slot(name) compose into the player chrome instead of
being laid out as ordinary children. Slot content is normal Hypen UI —
applicators, @actions, and @{state.*} bindings all work — the renderer just
overlays it full-bleed on the video surface:
Video(src: "@{state.streamUrl}", title: "@{state.title}", autoplay: true) {
Row {
Button {
Text("⏯")
}
.onClick(@actions.togglePlay)
.label("Play or pause")
Scrubber()
.tw("flex-1 ml-3")
}
.tw("px-4 pb-4 items-center self-end w-full")
.slot("controls")
Column {
Spinner()
Text("Buffering…")
}
.tw("items-center justify-center w-full h-full")
.slot("loading")
}
.bind(@state.playback)
.tw("w-full aspect-video rounded-2xl overflow-hidden")The four slots are controls, loading, error, and poster, and the player
state decides which are visible: poster shows in idle/loading/ended,
loading only while loading or rebuffering, controls in idle, playing,
paused, and ended, and error only in error. A present slot replaces the
built-in for that concern — a controls slot suppresses native chrome
regardless of the controls prop — and visibility is show/hide, not
mount/unmount, so slot subtrees keep their state across transitions.
Fullscreen:
Fullscreen can't ride the normal action → module → state round trip: browsers
only honor a fullscreen request inside the user's click, and a network hop can
lose that activation. .videoIntent("fullscreen") marks an element inside a
Video (typically a controls-slot button) as a renderer-local toggle — the
tap is handled on-device, no module involvement:
Button { Icon(@resources.fullscreen) }
.videoIntent("fullscreen")
.label("Toggle fullscreen")What goes fullscreen is the video container, never the raw platform video
element — so composition slots stay overlaid and custom controls keep working;
handing the element itself to the platform would swap in native chrome. Per
platform: web DOM fullscreens the wrapper via the Fullscreen API, canvas
fullscreens its host element, Android presents an immersive dialog hosting the
same player, iOS uses a fullScreenCover with the same AVPlayer, and
desktop fullscreens the window. Playback state, the bind, and slot visibility
are unaffected — fullscreen is presentation only. .videoIntent composes with
.onClick: both fire, and outside a Video subtree the applicator is inert.
There is no mute or volume intent — muting has no user-gesture requirement, so
it stays declarative: bind the muted prop to state and toggle it from an
action (muted: "@{state.muted}").
Scrubber
Media timeline for a Video's controls slot. Inside a Video it wires itself to
the enclosing player renderer-side: the thumb tracks playback at frame rate
without touching module state, dragging previews locally, and only the release
commits — so scrubbing stays responsive even when a state round trip costs a
network hop. It carries the slider accessibility role with live timeline
values, and where the platform gives it keyboard focus, arrow keys seek
±5 seconds and commit immediately.
Scrubber()
.bind(@state.playback)The commit goes to the Scrubber's own .bind(...) if it has one, otherwise to
the enclosing Video's bind, otherwise to its onSeek action (payload
{ type: "seek", position }). The local seek applies either way, so the
playhead moves even with no bind at all. Outside a Video, Scrubber renders
inert.
Audio
Audio player.
Audio("https://example.com/audio.mp3")Navigation Components
Link
Navigation link.
Link(href: "/about") {
Text("About Us")
}Router / Route
Declarative routing driven by module state. Routes are matched by the platform renderer based on state.location. For a complete routing guide, see Routing.
Router {
Route(path: "/home") {
HomePage()
}
Route(path: "/users/:id") {
UserProfile(userId: @state.params.id)
}
Route(path: "/settings") {
SettingsPage()
}
Else {
NotFoundPage()
}
}Router Arguments:
value(optional): State binding that drives the active route (defaults to@state.location). The initial route, path persistence, and module lifecycle are configured on the host SDK'sManagedRouter, not in the DSL.
Route Arguments:
path: URL pattern (supports:paramsegments)
An Else { ... } child renders when no route matches.
Control Flow Components
Control flow components are first-class constructs in the engine. They don't create DOM elements — their children render directly into the parent container. For a complete guide with patterns, performance tips, and end-to-end examples, see Control Flow.
ForEach
Iterates over a collection and renders children for each item. Use @item to reference each item, and always provide key for dynamic lists.
ForEach(items: @state.todos, key: "id") {
Row {
Checkbox {}
.checked(@item.completed)
.onChange(@actions.toggleTodo)
Text(@item.title)
.fontSize(16)
.color("@{item.completed ? '#9CA3AF' : '#111827'}")
.flex(1)
Button { Text("Delete") }
.onClick(@actions.deleteTodo)
.color("#EF4444")
}
.padding(12)
.gap(12)
.verticalAlignment("center")
}Arguments:
| Argument | Required | Default | Description |
|---|---|---|---|
items or in | Yes | — | Array binding from state |
key | No | index | Property name for stable identity across updates |
as | No | "item" | Custom variable name for each element |
When
Pattern matching on a value with multiple cases. Great for loading/error/success states.
When(value: @state.status) {
Case(match: "loading") {
Center { Spinner() }
}
Case(match: "error") {
Column {
Text("Something went wrong")
Button { Text("Retry") }
.onClick(@actions.reload)
}
}
Case(match: "success") {
ContentView()
}
Else {
Text("Unknown status")
}
}Arguments:
value: The value to match against
Children:
Case(match: ...): One or more case branchesElse: Optional fallback when no Case matches
Match Patterns:
- Exact values:
"loading",200,true - Multiple values (OR):
[200, 201, 204] - Wildcards:
"_"or"*" - Expressions:
"@{value >= 90}"
If
Boolean conditional for simple true/false branching. Under the hood it's syntactic sugar over When.
If(condition: @state.isLoggedIn) {
ProfileMenu()
Else {
LoginButton()
}
}
// Without Else — just hide/show
If(condition: @state.hasNotifications) {
Badge("New")
.backgroundColor("#EF4444")
}Arguments:
condition: Boolean binding or expression
Children:
- Main content (rendered when truthy)
Else: Optional fallback (rendered when falsy)
Charts
Chart opens a coordinate space; its children (Line, Area, Bars,
Points, Axis, Rule, Marker, Path) are marks positioned in data
units, and their events carry the datum rather than a pixel. See the
Charts guide.
Chart(y: [0, 100]) {
Axis(x)
Axis(y)
Bars(data: @state.units, x: "month", y: "count").onClick(@actions.selectMonth)
}
.height(220)Platform Support
| Component | Web | Android | iOS |
|---|---|---|---|
| Column, Row, Box | Yes | Yes | Yes |
| Center, Stack, Grid | Yes | Yes | Yes |
| List, Spacer, Divider | Yes | Yes | Yes |
| SafeArea | Yes | Yes | Yes |
| Text, Heading, Paragraph | Yes | Yes | Yes |
| Image | Yes | Yes | Yes |
| Button, Input, Textarea | Yes | Yes | Yes |
| Checkbox, Switch, Select | Yes | Yes | Yes |
| Slider | Yes | Yes | Yes |
| Card, Badge, Avatar | Yes | Yes | Yes |
| Spinner, ProgressBar | Yes | Yes | Yes |
| Video, Audio | Yes | Yes | Yes |
| Video playback bind, slots, Scrubber | Yes | Yes | Yes |
| Link, Router, Route | Yes | Yes | Yes |
| ForEach, When, If | Yes | Yes | Yes |
| Chart, Axis, Line, Area, Bars, Points, Rule, Marker, Path | Yes | Yes | Yes |
Video's playback bind, composition slots, and Scrubber ship on all five
renderers — web DOM, web canvas, Android, iOS, and desktop. On desktop the
playback-dependent parts need the video cargo feature (GStreamer); the slots
themselves work without it for the idle and error states.
Next Steps
- Child Slots — Build reusable components that accept children with
Children()and named slots - Control Flow — Deep dive into ForEach, When, If with patterns and performance tips
- Routing — Navigation, route guards, and nested routing
- Layout — Layout components and alignment
- Styling — Complete applicator reference
- Inputs — Forms and user interaction