Emerge
Write native GUI directly from Elixir using declarative API.
Installation
Add :emerge to your dependencies:
defp deps do
[
{:emerge, "~> 0.1.0"}
]
endThen run:
mix deps.getQuick example
defmodule MyApp.View.Counter do
use Emerge
use Solve.Lookup
@impl Viewport
def mount(opts), do: {:ok, Keyword.merge([title: "Counter"], opts)}
@impl Viewport
def render() do
counter = solve(MyApp.State, :counter)
row(
[
Background.color(color(:slate, 800)),
Font.color(color(:white)),
spacing(12),
padding(12)
],
[
my_button([Event.on_press(event(counter, :increment))], text("+")),
el([padding(10)], text("Count: #{counter.count}")),
my_button([Event.on_press(event(counter, :decrement))], text("-"))
]
)
end
# Reusable "component" is just plain elixir function
def my_button(attrs, content) do
Input.button(
attrs ++ [
padding(10),
Background.color(color(:sky, 500)),
Border.rounded(8)
],
content
)
end
@impl Solve.Lookup
def handle_solve_updated(_updated, state), do: {:ok, Viewport.rerender(state)}
endEasy reuse
Reuse in Emerge is just Elixir. Build data, map over it, and extract helpers that return UI trees.
defmodule MyApp.UI do
use Emerge.UI
def overview do
column(
[
width(fill()),
padding(20),
spacing(12),
Background.color(color(:slate, 900)),
Border.rounded(12)
],
[
el([Font.size(22), Font.color(color(:white))], text("Overview")),
row([spacing(12)], Enum.map(summary_stats(), &stat_card/1))
]
)
end
defp summary_stats do
[
{"Open", "12"},
{"Closed", "34"},
{"Owners", "5"}
]
end
defp stat_card({label, value}) do
el(
[
width(fill()),
padding(12),
Background.color(color(:slate, 800)),
Border.rounded(8)
],
column([spacing(4)], [
el([Font.color(color(:slate, 300))], text(label)),
el([Font.size(20), Font.color(color(:white))], text(value))
])
)
end
endThere is no separate component model to learn. If a function returns UI, you can compose it like any other Elixir function.
State management
Emerge is designed with Solve as a state management solution to keep complex UI apps sane. It keeps shared application state and rerender coordination outside the viewport process while Emerge stays focused on rendering.
Emerge does not depend on Solve. You can use another state management approach if it fits your app better.
Try it out
Try the standalone demo app in emerge_demo.
Features
-
Build layout and styling in one declarative tree with
el/2,row/2,column/2, and related helpers -
Reuse UI with ordinary Elixir functions, data transforms, and
Enum - Handle buttons, text input, keyboard, pointer events, and interactive states
- Render images, SVGs, backgrounds, borders, text, and font assets
- Use scroll containers, nearby overlays, paint-time transforms, and animation
- Run the same renderer on Wayland, DRM, and raster backends with high-DPI rendering and efficient tree updates
Backends
- Wayland for desktop Linux windows
- DRM for embedded, kiosk, and Nerves deployments
- Raster for offscreen rendering and tests (this backend doesn't work with viewport for now)
For runtime backend selection and multi-backend setup, see Set up a viewport.
Requirements
- Elixir 1.19+
- Linux (Wayland session or DRM for on-screen backends)
- Rust toolchain (if rustler precompiled doesn't cover your combination)
Documentation
API reference and tutorials are published on HexDocs.
Key guides:
Run mix docs to build the full docs locally.
Attribution
Emerge's UI API is heavily inspired by elm-ui by Matthew Griffith.
Third-Party Assets
Bundled third-party asset notices are documented in THIRD_PARTY_ASSETS.md. Package/runtime-relevant notices are summarized in NOTICE.
Packaged/runtime-relevant asset groups:
-
Inter default fonts in
native/emerge_skia/src/fonts- SIL Open Font License 1.1 -
Mocu DRM cursor SVGs in
native/emerge_skia/src/backend/drm/cursors/mocu_black_right- CC0 1.0 Universal
If you redistribute Emerge inside an application or firmware image, include the applicable notice files.