Emerge.UI (Emerge v0.2.1)

Copy Markdown View Source

Declarative UI tree API.

Emerge.UI is the root DSL for building the element tree returned from render/0 or render/1.

These examples assume use Emerge.UI.

Tree Model

UI is expressed as a tree of elements.

Each element has:

  • a constructor such as el/2, row/2, column/2, or text/1
  • attrs that configure layout, styling, behavior, or media
  • zero or more child elements

el/2 accepts exactly one child. Layout containers such as row/2, wrapped_row/2, column/2, text_column/2, and paragraph/2 accept child lists. Leaf elements such as text/1, image/2, svg/2, video/2, and none/0 have no children.

Core Constructors

The root module provides the core element constructors:

This small tree uses column/2 as the root, el/2 for framed content, and row/2 for a horizontal action area.

column(
  [
    width(px(380)),
    height(fill()),
    padding(16),
    spacing(14),
    Background.color(color(:slate, 900)),
    Border.rounded(14)
  ],
  [
    el([Font.size(20), Font.color(color(:slate, 50))], text("Project Alpha")),
    el(
      [Font.color(color(:slate, 300))],
      text("A small UI tree built from column/2, el/2, row/2, and text/1.")
    ),
    row([spacing(12)], [
      el(
        [
          padding_xy(12, 8),
          Background.color(color(:slate, 50)),
          Border.rounded(8),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("4 tasks")
      ),
      el(
        [
          padding_xy(12, 8),
          Background.color(color(:sky, 500)),
          Border.rounded(8),
          Font.color(color(:slate, 50))
        ],
        text("Open")
      )
    ])
  ]
)
Rendered basic Emerge.UI tree example

Choosing A Layout

Choose the container that matches the child flow:

  • el/2 when there is exactly one child
  • row/2 when children stay on one horizontal line
  • wrapped_row/2 when horizontal content should wrap onto additional lines
  • column/2 when children stack vertically
  • text_column/2 and paragraph/2 when the content is prose rather than generic layout blocks

This comparison shows the difference between row/2, wrapped_row/2, and column/2.

column(
  [
    width(px(420)),
    height(fill()),
    padding(16),
    spacing(14),
    Background.color(color(:slate, 900)),
    Border.rounded(14)
  ],
  [
    el([Font.size(14), Font.color(color(:slate, 50))], text("row/2 keeps children on one line")),
    row([spacing(8)], [
      el(
        [
          padding_xy(10, 6),
          Background.color(color(:slate, 50)),
          Border.rounded(999),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("One")
      ),
      el(
        [
          padding_xy(10, 6),
          Background.color(color(:slate, 50)),
          Border.rounded(999),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Two")
      ),
      el(
        [
          padding_xy(10, 6),
          Background.color(color(:slate, 50)),
          Border.rounded(999),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Three")
      )
    ]),
    el(
      [Font.size(14), Font.color(color(:slate, 50))],
      text("wrapped_row/2 wraps horizontal content onto new lines")
    ),
    wrapped_row([width(px(320)), spacing_xy(8, 8)], [
      el(
        [
          padding_xy(10, 6),
          Background.color(color(:slate, 50)),
          Border.rounded(999),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Docs")
      ),
      el(
        [
          padding_xy(10, 6),
          Background.color(color(:slate, 50)),
          Border.rounded(999),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Layout")
      ),
      el(
        [
          padding_xy(10, 6),
          Background.color(color(:slate, 50)),
          Border.rounded(999),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Nearby")
      ),
      el(
        [
          padding_xy(10, 6),
          Background.color(color(:slate, 50)),
          Border.rounded(999),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Animation")
      ),
      el(
        [
          padding_xy(10, 6),
          Background.color(color(:slate, 50)),
          Border.rounded(999),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Input")
      ),
      el(
        [
          padding_xy(10, 6),
          Background.color(color(:slate, 50)),
          Border.rounded(999),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Scroll")
      )
    ]),
    el(
      [Font.size(14), Font.color(color(:slate, 50))],
      text("column/2 stacks children vertically")
    ),
    column([spacing(8)], [
      el(
        [
          padding(10),
          Background.color(color(:slate, 50)),
          Border.rounded(10),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Header")
      ),
      el(
        [
          padding(10),
          Background.color(color(:slate, 100)),
          Border.rounded(10),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Body")
      ),
      el(
        [
          padding(10),
          Background.color(color(:slate, 50)),
          Border.rounded(10),
          Border.width(1),
          Border.color(color(:slate, 300)),
          Font.color(color(:slate, 800))
        ],
        text("Footer")
      )
    ])
  ]
)
Rendered Emerge.UI layout comparison

wrapped_row/2 wraps against the width it resolves from its parent, including nested fill-driven layouts. Its height grows to match the tallest child on each wrapped line, so multiline or reflowed children push following content down instead of clipping. Horizontal child alignment also stays line-local after wrapping, which means center_x/0 and align_right/0 position children within the remaining width of their wrapped line rather than across the full container width.

use Emerge.UI

use Emerge.UI imports:

It also aliases the grouped helper modules:

  • Animation
  • Background
  • Border
  • Event
  • Font
  • Input
  • Interactive
  • Nearby
  • Svg
  • Transform

Using use Emerge for a viewport also calls use Emerge.UI.

Top-Level Attrs

The root module also defines a small set of attrs that are not grouped into submodules:

Input.text([key(:search), focus_on_mount()], state.query)

image([width(px(160)), height(px(96)), image_fit(:cover)], "images/hero.jpg")

Submodules

The rest of the API is organized by concern:

As trees grow, extract regular Elixir functions that return Emerge.UI.t().

Summary

Types

A single public UI attribute tuple.

A list of public UI attribute tuples.

A single child element.

A list of child elements.

An Emerge UI element.

Image fit modes accepted by image_fit/1.

An image source accepted by image/2 and svg/2.

A stable key used to retain identity among siblings.

t()

A tree of UI returned by DSL helpers.

A video target accepted by video/2.

Functions

Import the root element DSL and the most common UI helper modules.

Clip nearby escape overlays attached under this host.

Lay out children vertically.

Build a single-child container.

Focus this element once when it is first mounted into the tree.

An image element.

Set image fit mode for image/2 and video/2.

Provide a stable sibling key for identity in lists.

Build an empty element that takes up no space.

A paragraph lays out inline text children with word wrapping.

Lay out children horizontally in one line.

An SVG element.

Build a text leaf element.

A text column lays out paragraph-oriented content vertically.

A video element backed by a renderer-owned video target.

Lay out children horizontally and wrap onto new lines as needed.

Types

attr()

@type attr() :: {atom(), term()}

A single public UI attribute tuple.

attrs()

@type attrs() :: [attr()]

A list of public UI attribute tuples.

child()

@type child() :: element()

A single child element.

children()

@type children() :: [child()]

A list of child elements.

clip_nearby_attr()

@type clip_nearby_attr() :: {:clip_nearby, true}

element()

@type element() :: Emerge.Engine.Element.t()

An Emerge UI element.

focus_on_mount_attr()

@type focus_on_mount_attr() :: {:focus_on_mount, true}

image_fit_attr()

@type image_fit_attr() :: {:image_fit, image_fit_mode()}

image_fit_mode()

@type image_fit_mode() :: :contain | :cover

Image fit modes accepted by image_fit/1.

image_source()

@type image_source() ::
  binary()
  | atom()
  | {:id, binary()}
  | {:path, binary()}
  | Emerge.Assets.Ref.t()

An image source accepted by image/2 and svg/2.

key()

@type key() :: term()

A stable key used to retain identity among siblings.

key_attr()

@type key_attr() :: {:key, key()}

t()

@type t() :: element()

A tree of UI returned by DSL helpers.

video_target()

@type video_target() :: EmergeSkia.VideoTarget.t()

A video target accepted by video/2.

Functions

__using__(opts)

(macro)
@spec __using__(term()) :: Macro.t()

Import the root element DSL and the most common UI helper modules.

This imports the core constructors from Emerge.UI and the frequently used helpers from Color, Size, Space, Scroll, and Align. It also aliases the grouped styling and behavior modules such as Background, Border, Font, Input, Event, and Nearby.

clip_nearby()

@spec clip_nearby() :: clip_nearby_attr()

Clip nearby escape overlays attached under this host.

Use this on hosts or scroll containers when nearby content should be clipped to the host bounds instead of escaping freely.

column(attrs, children)

@spec column(attrs(), children()) :: t()

Lay out children vertically.

Use column/2 for stacks of content where each child sits below the previous one.

Example

column([spacing(10)], [
  text("Line 1"),
  text("Line 2")
])

el(attrs, child)

@spec el(attrs(), child()) :: t()

Build a single-child container.

el/2 is the fundamental framing element. Use it when you need one child and want to apply styling, alignment, nearby elements, or sizing around that child.

Font attrs applied to an el/2 are inherited by text descendants.

Example

el(
  [
    padding(12),
    Background.color(color(:slate, 900)),
    Border.rounded(12),
    Font.color(color(:slate, 50))
  ],
  text("Hello")
)

focus_on_mount()

@spec focus_on_mount() :: focus_on_mount_attr()

Focus this element once when it is first mounted into the tree.

The focus request is tied to first mount, not to every rerender.

image(attrs, source)

@spec image(attrs(), image_source()) :: t()

An image element.

source can be a verified ~m"..." reference, logical asset path, runtime file path, or {:id, image_id}.

Use image_fit/1 to choose between :contain and :cover.

Example

image(
  [width(px(160)), height(px(96)), image_fit(:cover)],
  "images/hero.jpg"
)

image_fit(mode)

@spec image_fit(image_fit_mode()) :: image_fit_attr()

Set image fit mode for image/2 and video/2.

  • :contain keeps the full source visible inside the element bounds
  • :cover fills the bounds and crops if necessary

key(value)

@spec key(key()) :: key_attr()

Provide a stable sibling key for identity in lists.

Use key/1 when sibling order can change and an element should retain its identity across inserts, removals, or moves.

none()

@spec none() :: t()

Build an empty element that takes up no space.

Use none/0 for conditional branches that should render nothing.

paragraph(attrs, children)

@spec paragraph(attrs(), children()) :: t()

A paragraph lays out inline text children with word wrapping.

Children should be text/1 elements or el/2-wrapped text elements. Words flow left-to-right and wrap at the container width.

Example

paragraph([width(px(220))], [
  text("A paragraph wraps "),
  el([Font.semi_bold()], text("inline")),
  text(" text children.")
])

row(attrs, children)

@spec row(attrs(), children()) :: t()

Lay out children horizontally in one line.

Use row/2 when the children should stay on the same horizontal track. Add spacing/1 or space_evenly/0 from Emerge.UI.Space to control the gaps.

Example

row([spacing(20)], [
  el([], text("A")),
  el([], text("B")),
  el([], text("C"))
])

svg(attrs, source)

@spec svg(attrs(), image_source()) :: t()

An SVG element.

Preserves the SVG's original colors by default. Use Svg.color/1 to apply template tinting to all visible pixels.

Example

svg([width(px(24)), height(px(24))], "icons/check.svg")

text(content)

@spec text(String.t()) :: t()

Build a text leaf element.

It can live on its own as a content leaf, but it does not wrap by default.

Use paragraph/2 or text_column/2 for wrapped text flows.

Example

text("Status: ready")

text_column(attrs, children)

@spec text_column(attrs(), children()) :: t()

A text column lays out paragraph-oriented content vertically.

It behaves like a column but comes with document-friendly defaults:

  • width(fill())
  • height(content())

You can override these by passing explicit width/height attributes.

Example

text_column([spacing(12)], [
  paragraph([], [text("First paragraph of copy.")]),
  paragraph([], [text("Second paragraph of copy.")])
])

video(attrs, target)

@spec video(attrs(), video_target()) :: t()

A video element backed by a renderer-owned video target.

video/2 behaves like an image element whose pixels are provided by an owned target instead of a file source.

wrapped_row(attrs, children)

@spec wrapped_row(attrs(), children()) :: t()

Lay out children horizontally and wrap onto new lines as needed.

Use wrapped_row/2 for chips, tags, and other horizontal content that should continue onto additional lines when it no longer fits the available width.

Example

wrapped_row([spacing_xy(12, 12)], [
  el([], text("One")),
  el([], text("Two")),
  el([], text("Three"))
])