HypenHypen
Guide

Layout

Layout components for structuring UI elements in Hypen

Layout

Layout components are the foundation for creating structure and arrangement of UI elements. Every layout component can contain children that are rendered according to the layout's rules.

Column

Arranges children vertically from top to bottom.

Column {
    Text("First")
    Text("Second")
    Text("Third")
}

Alignment

// Center children horizontally
Column {
    Text("Centered")
}
.horizontalAlignment("center")

// Distribute children vertically
Column {
    Text("Top")
    Text("Middle")
    Text("Bottom")
}
.verticalAlignment("space-between")

Spacing

Column {
    Text("Item 1")
    Text("Item 2")
    Text("Item 3")
}
.gap(16)

Platform Support: Web, Android, iOS

Row

Arranges children horizontally from left to right.

Row {
    Text("Left")
    Text("Center")
    Text("Right")
}

Alignment

// Center children vertically
Row {
    Text("Vertically centered")
}
.verticalAlignment("center")

// Distribute children horizontally
Row {
    Text("Left")
    Spacer()
    Text("Right")
}
.horizontalAlignment("space-between")

Flexible Children

Use weight to make children fill available space:

Row {
    Text("Fixed")
    Box {
        Text("Flexible")
    }
    .weight(1)
    Text("Fixed")
}

Platform Support: Web, Android, iOS

Box / Container

A generic container that positions children using z-axis stacking. Children are overlaid on top of each other, with later children appearing above earlier ones.

Box {
    Image("background.jpg")
        .fillMaxSize(true)

    Column {
        Text("Overlay content")
    }
    .padding(16)
}

Use Container as an alias for Box:

Container {
    Text("Content")
}

Platform Support: Web, Android, iOS

Center

Centers its children both horizontally and vertically within its bounds.

Center {
    Spinner()
}
.fillMaxSize(true)

Useful for centering loading indicators, modals, or any content that needs to be visually centered.

Platform Support: Web, Android, iOS

Stack

Similar to Box, stacks children on the z-axis for overlapping layouts. Useful for creating layered UI elements like badges on cards.

Hypen has no CSS positioning — absolute/relative/top-*/inset-* (and the .position() applicator) don't exist on native renderers, and the Tailwind parser rejects them with an error. Place overlaid children with the Stack's .horizontalAlignment() / .verticalAlignment() and margins instead:

Stack {
    Card {
        Text("Background card")
    }

    Badge("New")
        .margin(8)
}
.horizontalAlignment("end")
.verticalAlignment("start")

Platform Support: Web, Android, iOS

Grid

Arranges a bound collection in a grid. Like List, Grid is data-driven: pass an array binding and an item template, and one child is rendered per record. A static Grid { ... } with hand-written children renders an error — use nested Rows/Columns for fixed layouts.

Grid(@state.cells, key: "id") {
    Card { Text("@{item.label}") }
}
.gridColumns(3)
.gap(16)

Grid Configuration

// Fixed number of columns
Grid(@state.cells, key: "id") {
    Card { Text("@{item.label}") }
}
.gridColumns(2)

// Custom column template (Web)
Grid(@state.cells, key: "id") {
    Card { Text("@{item.label}") }
}
.gridTemplateColumns("1fr 2fr 1fr")

// Separate row and column gaps
Grid(@state.cells, key: "id") {
    Card { Text("@{item.label}") }
}
.rowGap(16)
.columnGap(8)

Items can span columns via .gridColumn(...) on the template — the value is bindable per record, e.g. .gridColumn("@{item.span}") with span: "span 2" in the data (see the calculator example's double-width 0 key).

Platform Support: Web, Android, iOS

List

A scrollable container optimized for displaying collections of items. On Android, uses virtualized rendering (LazyColumn) for performance with large lists.

List(@state.items) {
    Text("@{item.title}")
}

List(@state.items) is shorthand for ForEach(items: @state.items, key: "id") in a scrollable container — the array binding is required, and records need an id field (or pass key: explicitly).

List Direction

// Vertical list (default)
List(@state.items) {
    Text("@{item.title}")
}

// Horizontal list
List(@state.items) {
    Text("@{item.title}")
}
.flexDirection("row")

Scrolling

List(@state.items) {
    Text("@{item.title}")
}
.scrollable(true)
.height(400)

Platform Support: Web, Android, iOS

Spacer

Creates flexible empty space between components. Expands to fill available space within a Row or Column.

Row {
    Text("Left aligned")
    Spacer()
    Text("Right aligned")
}

Multiple spacers distribute space evenly:

Row {
    Text("Left")
    Spacer()
    Text("Center")
    Spacer()
    Text("Right")
}

Platform Support: Web, Android, iOS

Divider

A thin horizontal line for visual separation between content sections.

Column {
    Text("Section 1 content")
    Divider()
    Text("Section 2 content")
}

Platform Support: Web, Android, iOS

SafeArea

A full-size vertical container whose content is padded by the device safe-area insets — the notch and status bar at the top, the home indicator at the bottom, and the cutout gutters at the sides. Use it as the outermost container of a screen so content never sits under system chrome.

SafeArea fills its parent (100% width and height) and stacks children vertically, exactly like Column. The inset is padding on the container itself, so a background applied with applicators still runs full-bleed under the insets while the content stays inside them.

SafeArea {
    Row()
        .padding(16)
        .backgroundColor("#111827") {
        Text("Header")
    }

    Column()
        .flex(1) {
        Text("Content")
    }

    Row()
        .padding(16) {
        Text("Bottom bar")
    }
}

edges

The optional edges prop selects which sides are inset. It takes a list of "top", "right", "bottom", "left". Omitting it (or passing an empty list) insets all four edges; unknown strings are ignored.

SafeArea(edges: ["top", "bottom"]) {
    Text("Horizontal edges run to the screen edge")
}

Applicators apply as they do on any container, and safe-area padding combines additively with a user .padding(). Nesting SafeAreas simply applies the insets again on the inner one.

Overriding the insets

The effective inset for an edge is the embedder-supplied value for that edge if one was given, otherwise the platform default. Overrides are per-edge and merge over the defaults — supplying only bottom: 0 zeroes the bottom inset while top, left, and right keep their platform values.

RendererDefault insetsEmbedder override
Web DOMenv(safe-area-inset-*, 0px)safeAreaInsets option on the DOM renderer options — a partial per-edge record of numbers (CSS px) or CSS length strings
Web Canvasprobed once from env(safe-area-inset-*); 0 where unsupportedsafeAreaInsets option on the Canvas renderer options (numbers, CSS px)
AndroidWindowInsets.safeDrawingLocalHypenSafeAreaInsets composition local, or the insets parameter on the HypenApp entry composable; null falls back to safeDrawing
iOS / SwiftUIthe hosting view's real safe areathe \.hypenSafeAreaInsets environment value (HypenSafeAreaInsets?); nil falls back to the real safe area
Desktopzeros, except under the macOS unified titlebar, where the window-controls bar (close/minimize/maximize, 28 logical px) drawn over the content becomes the top insetthe safe_area_insets field on the renderer/app config struct

Values use each platform's logical unit: CSS px on web, dp on Android, points on iOS, logical px on desktop.

Web caveat — iOS Safari: env(safe-area-inset-*) resolves to 0 unless the page opts into the full viewport. Without viewport-fit=cover in the viewport meta tag, SafeArea renders like a plain Column on the web:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

Platform Support: Web, Android, iOS, Desktop

Size

Understanding Size Units

Hypen supports multiple size units that work consistently across all platforms:

UnitExampleDescription
(number)200Density-independent points (default)
px"200px"Absolute pixels (converted to points on mobile)
dp / pt"16dp"Density-independent points (equivalent)
%"50%"Percentage of parent element
vw"100vw"Viewport width (100vw = full screen width)
vh"50vh"Viewport height (50vh = half screen height)

Keywords:

  • "fill" or "match_parent" → expands to 100%
  • "wrap" or "auto" → shrinks to fit content

How Sizing Works Per Platform

The units dp and pt are equivalent — both represent density-independent points that scale appropriately with the device's screen density:

PlatformBare Numberpx Conversiondp/pt
Androiddppx / densityNative dp
iOSptpx / scaleNative pt
WebCSS px1:1CSS px
  • Android: Uses displayMetrics.density (typically 2-3x on modern phones)
  • iOS: Uses UIScreen.main.scale (2x on Retina, 3x on Super Retina)
  • Web: CSS pixels are already density-independent in browsers

Example: .width(100) renders as:

  • Android: 100dp → 200-300 physical pixels on a 2-3x density screen
  • iOS: 100pt → 200-300 physical pixels on a 2-3x scale screen
  • Web: 100px → browser handles scaling automatically
// Using different units
Box { }
    .width(200)           // 200 points (density-independent)
    .height("50%")        // 50% of parent
    .minWidth("320px")    // 320 absolute pixels
    .maxWidth("100vw")    // full viewport width

// Responsive full-width container
Box { }
    .width("fill")        // same as "100%"

// Content-sized element
Text("Hello")
    .width("wrap")        // same as "auto"

width / height

Sets explicit dimensions for a component.

Box { }
    .width(200)
    .height(100)

Accepts numbers (density-independent points) or strings with units:

Box { }
    .width("50%")
    .height("100vh")

Platform Support: Web, Android, iOS

minWidth / maxWidth / minHeight / maxHeight

Sets size constraints, allowing the component to flex within bounds.

Box { }
    .minWidth(100)
    .maxWidth(500)
    .minHeight(50)
    .maxHeight(300)

Platform Support: Web, Android, iOS

size

A convenience applicator that sets both width and height to the same value. Useful for square elements like avatars and icons.

Avatar("user.jpg").size(48)
Icon("star").size(24)

Platform Support: Web, Android, iOS

fillMaxWidth / fillMaxHeight / fillMaxSize

Expands a component to fill available space in its parent container.

// Fill entire width
Box { }.fillMaxWidth(true)

// Fill with a fraction (0.5 = 50% of available space)
Box { }.fillMaxWidth(0.5)

// Fill both width and height
Box { }.fillMaxSize(true)

Platform Support: Web, Android, iOS

aspectRatio

Maintains a consistent width-to-height ratio regardless of container size. Useful for images and video containers.

// 16:9 widescreen ratio
Image("photo.jpg")
    .aspectRatio("16/9")

// Square
Image("avatar.jpg")
    .aspectRatio(1)

Platform Support: Android, iOS (Web: use width/height)

Layout Applicators

These applicators work with layout components to control spacing, alignment, and flexibility.

Spacing Applicators

ApplicatorDescriptionExample
paddingInternal spacing.padding(16)
marginExternal spacing.margin(8)
gapSpace between children.gap(12)
rowGapVertical gap in grid.rowGap(16)
columnGapHorizontal gap in grid.columnGap(8)

Padding Variants

// Uniform padding
Box { }.padding(16)

// Horizontal and vertical
Box { }.padding(horizontal: 16, vertical: 8)

// Individual sides
Box { }.padding(top: 16, right: 12, bottom: 16, left: 12)

Alignment Applicators

ApplicatorDescriptionValues
horizontalAlignmentHorizontal alignment of childrenstart, center, end, space-between, space-around
verticalAlignmentVertical alignment of childrenstart, center, end, space-between, space-around

Flex Applicators

ApplicatorDescriptionExample
weightFlex grow factor.weight(1)
flexCSS flex shorthand.flex(1)
flexGrowGrow factor.flexGrow(1)
flexShrinkShrink factor.flexShrink(0)

Layout Examples

Row {
    Text("Logo")
        .fontWeight("bold")

    Spacer()

    Row {
        Link(href: "/home") { Text("Home") }
        Link(href: "/about") { Text("About") }
        Link(href: "/contact") { Text("Contact") }
    }
    .gap(24)
}
.padding(16)
.backgroundColor("#ffffff")

Card Grid

Grid(@state.products, key: "id") {
    Card {
        Column {
            Image("@{item.image}")
            Text("@{item.name}")
            Text("@{item.price}")
        }
    }
}
.gridColumns(3)
.gap(24)
.padding(16)

Centered Loading

Center {
    Column {
        Spinner()
            .size(48)
        Text("Loading...")
            .color("#666666")
    }
    .gap(16)
}
.fillMaxSize(true)
Row {
    // Sidebar
    Column {
        Text("Menu")
            .fontWeight("bold")
        Divider()
        Link(href: "/dashboard") { Text("Dashboard") }
        Link(href: "/settings") { Text("Settings") }
    }
    .width(250)
    .padding(16)
    .backgroundColor("#f5f5f5")

    // Main content
    Column {
        Text("Main Content")
    }
    .weight(1)
    .padding(24)
}
.fillMaxSize(true)

Next Steps