Surface: a compile-time component layer bolted onto Phoenix LiveView
A server-side rendering component library for Phoenix
At a glance
- What is it?
- Surface is a server-side rendering component library that puts declared properties, slots, contexts and compile-time checking in front of Phoenix LiveView. The gain is a component layer the compiler polices; the cost is a dependency that assumes LiveView is already working in your app.
- Who is it for?
- Adopt Surface when your Phoenix app already runs on LiveView and you want declared components whose props, slots and events are checked while the project compiles. Leave it alone when your team wants the same markup without another dependency in mix.exs.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 7 months ago.
- What is it written in?
- Mainly Elixir, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Built on LiveView instead of replacing it
Elixir developers running Phoenix tend to accumulate HTML across template files and helper functions, where the inputs a piece of markup accepts are implicit. Surface was written to answer that, with a component layer where a component is an Elixir module and the template language stays HTML-centric rather than becoming a new syntax to learn.
Its scope is narrow, and the README is explicit about the boundary. The project targets Phoenix LiveView applications rather than the whole BEAM web stack, and it assumes LiveView stays in place as the runtime. Surface is built on top of Phoenix LiveView and its component API, and the stated goal is rich interactive user interfaces while writing minimal custom JavaScript. Phoenix v1.7 applications come with built-in support for LiveView, so for a new app the layer underneath is already there before Surface arrives.
That makes plain Phoenix LiveView a real alternative worth naming, with one dependency fewer. The difference in approach is what Surface adds rather than removes: declared properties, slots, contexts and compile-time checking of template structure. The README does not document what the LiveView component API covers or omits on its own, so that difference can be described here but not measured.
Components are Elixir modules with declared properties
A component definition opens with `use Surface.Component`, which pulls in the rendering macros. Inputs come next: `@doc` carries the description and `prop` declares each input with a type and whether it is required. Then `def render(assigns)` returns a `~F` sigil, and inside that sigil the markup is HTML with interpolation.
# Defining the component
defmodule Hello do
use Surface.Component
@doc "Someone to say hello to"
prop name, :string, required: true
def render(assigns) do
~F"""
Hello, {@name}!
"""
end
endCompile-time checking covers component properties, so a caller that leaves out a required prop is reported while the project compiles rather than at request time. The declared type `:string` is the other half of the same check, since the type travels with the declaration instead of being inferred at the call site.
Using the component is a tag, and the attribute value becomes the prop:
# Using the component
defmodule Example do
use Surface.Component
def render(assigns) do
~F"""
<Hello name="John Doe"/>
"""
end
endWorth noticing: the 2 modules above differ in one respect only, since the first declares a prop and the second passes 1. A page that does nothing but arrange other components is still a Surface component itself, so composition costs a module with a render function instead of a separate partial file plus a helper.
Three steps from a fresh Phoenix app to a rendered component
There is no separate installer. The path runs through the Phoenix generator, the dependency list, and one Mix task.
mix phx.new my_appWith `{:surface, "~> 0.12.0"}` added to the dependency list in `mix.exs`, the requirement stays inside the 0.12 line:
def deps do
[
{:surface, "~> 0.12.0"}
]
endFetching the dependencies and letting the task write the files it needs are two Mix invocations:
mix deps.get
mix surface.init --demo --catalogueWithout options, `surface.init` only updates the necessary files in the project. With `--demo` it also generates a sample component and a LiveView that uses it, reachable at the `/demo` route. With `--catalogue` it sets up a Surface Catalogue at `/catalogue`, and running both together also produces two catalogue examples and a playground for the sample component. The whole path is 1 generated app, 1 dependency entry and 1 init task. Starting the server comes next:
mix phx.serverThe application answers on port 4000, so the generated demo lives at http://localhost:4000/demo. The full option list is one command away, which matters because the README summarises the flags rather than enumerating them:
mix help surface.initCompile-time checking moves template failures into the build
Template structure, component properties, slots and events are all checked while the project compiles. That is the mechanism underneath every other Surface feature, and it is the one worth judging the library by.
A component that references a property nobody declared, or a slot a parent never fills, is reported at compile time, so a typo in a template stops the build instead of reaching a browser. The cost sits on the far side of the same ledger: those checks run inside Surface's own template compiler, so they cover the structure that compiler parses. The README does not document which runtime errors stay outside that boundary, nor how a component behaves when a value arrives from a client rather than from a parent template.
So a Surface codebase ends up with 2 places to look. One is the build, where structure errors cluster. The other is the LiveView socket and the browser, where values that arrived over the wire behave. Knowing which side a defect belongs to is the practical habit this library asks for, and the README does not spell out how to tell the 2 apart.
Slots and contexts move content and data downward
A Surface component is one of 4 kinds: stateless, stateful, renderless or compile-time. That choice decides where data lives, not how markup is written, because all 4 kinds still return markup from a render function.
Slots handle content. A component declares a placeholder and the parent fills it with custom content, so the child controls position and structure while the caller controls what sits inside. Contexts handle data. A parent shares data with children without passing it as properties, which keeps a deep tree from turning into a long chain of explicit attributes.
The 2 are easy to confuse because both move something downward. Slots answer where content goes. Contexts answer where data comes from. If a parent passes a value through 2 properties and the child never alters it, a context removes the plumbing. If a child needs its own markup in the middle of a table row, a property cannot carry that and a slot can. Both mechanisms ship inside the core package, and the README mentions no configuration switch to turn either one off.
For a team weighing the library, this pairing is the strongest argument for it. Projects that need custom content in the middle of a component usually end up inventing a parallel convention, and Surface gives that convention a declared name that the compiler knows about.
The dependency snippet omits the runtime Surface builds on
This is the detail to check before adding Surface to anything that already exists. The dependency list in the installation section holds a single entry, `{:surface, "~> 0.12.0"}`, and nothing in it names LiveView, the layer the README says Surface is built on.
For an app generated by `mix phx.new`, the omission costs nothing, because Phoenix v1.7 apps ship with built-in LiveView support. For an older app, the same README carries a note: if LiveView is not properly installed, follow the installation instructions for Phoenix LiveView instead. The documented path therefore splits in two. The fresh-app path is complete in the README, and the existing-app path stops at a pointer to hexdocs.pm/phoenix_live_view/installation.html.
Manual installation is also not spelled out in the README. It is deferred to the Getting Started guide at surface-ui.org/getting_started, alongside the live examples and full documentation on the project site.
The practical consequence for an existing Phoenix codebase is that the first thing to verify is not the Surface version. It is whether that codebase already has a working LiveView setup, since a project without one has to solve that before Surface becomes installable at all.
Auto-completion is the one editor feature marked unfinished
The feature list names 4 editor capabilities, namely warnings and errors, syntax highlighting, jump-to-definition and auto-completion. The last of those carries a parenthetical reading soon, which makes it the only unfinished item written anywhere in the README.
Exactly 2 tools are named for those 4 capabilities. The Surface Formatter sits in its own repository, surface-ui/surface_formatter, and formats Surface code. The Surface package for VS Code is described as syntax highlighting support for Surface/Elixir, a narrower claim than the feature list, because that description names highlighting and nothing else. No third tool is named for the warnings, the errors or jump-to-definition that the list also promises, so for those 3 capabilities the editor story is described without an attributed source.
If your editor is a main reason to want a component library, this is the part to test first: run the formatter on a project, then open a Surface template and see which of the promised checks the extension actually reports.
The repository root lists files the README never explains
A library at this stage raises 2 upgrade questions quickly. The first is version movement, and the repository root answers it: a migration guide sits at MIGRATING.md beside `CHANGELOG.md`, and the README sends previous-version users to that guide instead of describing upgrade steps inline. Nothing in the README states a support window for the 0.12 line, so an upgrade decision means reading the migration guide first.
The second question is what the build itself runs. The root listing includes mix.exs, mix.lock, .formatter.exs, config/, lib/, test/, priv/, assets/, images/, .github/, .tool-versions, .gitignore, blend.exs and blend/. 2 entries there, blend.exs and blend/, appear nowhere in the README, so their role cannot be established from the documentation. The presence of .formatter.exs at the root is more useful: that is the configuration file the Surface Formatter reads, and a team that skips the formatter will find its formatting diverges from everyone else's within a few commits.
Surface's source is under the MIT License with copyright (c) 2020, Marlus Saraiva, the repository publishes no GitHub releases, and the last push was on 2026-03-25. An adopter therefore has nothing published to subscribe to and no recent commit trail to read as a maintenance signal, which pushes the upgrade question onto the migration guide and onto reading the code.
Editorial conclusion
Adopt Surface when your Phoenix app already runs on LiveView and you want declared components whose props, slots and events are checked while the project compiles. Leave it alone when your team wants the same markup without another dependency in mix.exs. Verify 2 things first: that the target codebase has a working LiveView setup, since the dependency list adds Surface alone, and that the formatter and the VS Code package behave as you need, since auto-completion is the one editor feature still written as coming.
Frequently asked questions
Do I need Phoenix LiveView installed before I add Surface to an existing Phoenix app?
For anything other than a fresh app, yes. Phoenix v1.7 applications generated with mix phx.new come with built-in LiveView support, and the README points apps without a working LiveView setup to the installation instructions for Phoenix LiveView rather than listing a dependency that would add it.
What does mix surface.init do, and what do the --demo and --catalogue options add?
The task updates the necessary files in your project. The --demo option also generates a sample component and a LiveView that uses it at the /demo route, --catalogue sets up a Surface Catalogue at /catalogue, and using both together also generates two catalogue examples and a playground for the sample component.
How do I declare the inputs a Surface component accepts?
Inside a module that calls use Surface.Component, each input gets a prop declaration with a type and a required flag, as in prop name, :string, required: true. Compile-time checking covers component properties, so a caller that omits a required property is reported while the project compiles.
Which editor and tooling support does Surface have today?
A formatter maintained in the surface-ui/surface_formatter repository, and a Surface package for VS Code described as syntax highlighting support for Surface/Elixir. Auto-completion appears in the feature list marked as coming, and the README names no tool for the warnings, errors or jump-to-definition that the same list mentions.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/surface-ui-surface)Was this useful?