teamon

teamon

headless - Headless (unstyled) UI components for Phoenix (with Alpine)

After spending some time recently researching various ways of building modern UIs I came to one sad conclusion: if one wants to build a modern interactive application in Phoenix (+ LiveView) there are not many choices of complex components, like for example a dropdown with search. Incorporating existing solutions requires data manipulation that are not easy to keep track of especially in LiveView context.

That’s how headless was born - a collection of unstyled, UI components for Phoenix and Phoenix LiveView.

https://github.com/teamon/headless

Goals & Rules

  • Provide unstyled Phoenix components as building blocks for your own UI components

  • If something can be achieved with HTML and CSS only, it should be done with HTML and CSS only (no-JS)

  • Where JS is required, use Alpine.js

  • Use Alpine.data() instead of inline markup

  • Components must work with standard Phoenix controllers (dead views)

  • Components must work with Phoenix LiveView

  • Components must work with standard Phoenix forms

  • Components must be accessible (aria attributes, keyboard navigation, focus, etc.)

Non-goals

Providing 100+ HTML components - headless should focus only on tricky components that are non-trivial to implement correctly, something like Headless UI for React and Vue.

Future goals

In the perfect world there would be a collection of headless components and then some UI libraries built on top of that providing elegant styling.

Status

Very very early, Proof of Concept stage.

So far I did an example <.toggle> component that uses a checkbox under the hood and my nemesis <.combobox/> that tried to leverage all the things that we get with Phoenix & LiveView and provide a good client-side experience on top of that.

I would love to hear any feedback on the idea itself, the implementation, etc.

Most Liked Switch mode

sodapopcan

sodapopcan

It’s good to see more effort in this space! Are you aware of Doggo? It’s already quite far along though still a work in progress. A lot of the more complex JS has not been implemented yet. This is not my project so I can’t speak to plans there but I am using it in a personal project and currently converting a production app over to it.

For myself, the Alpine dependency makes Headless a non-starter. I’m not recommending you don’t use it, but I feel JS commands+hooks are a better route in terms of longevity within the ecosystem. Using Alpine isn’t a bad thing per se as lots of Phoenix-folks still use it, but you’re boxing out the dependency-conscious among us… which is also not necessarily a bad thing per se as it’s providing some variety. Good work, overall!

greven

greven

To add to what @sodapopcan said, great initiative, we need more in the community! But I agree with the Alpine sentiment. I really like Alpine but it has a big drawback for me is the CSP limitation: CSP — Alpine.js

I rather deal hooks that having problems and workaround the CSP. Even though as a library AlpineJS is pretty nice regarding DX. :slight_smile:

teamon

teamon

After researching more into ways of bulding a headless library I decided to go with “api-only” approach like Sprout UI did.

And there’s a demo website now: https://headless.fly.dev

Last Post!

teamon

teamon

Update

After trying multiple approaches to provide the headless API-only components I decided to go with standard Phoenix components and an optional optimization compiler.

The are two problems with API-only components when used like this:

  import Headless

  def avatar(assigns) do
    ~H"""
    <.use_avatar :let={a} src={@src}>
      <div {a.root}>
        <img {a.image} alt={@alt} />
        <div {a.fallback}><%= @initials %></div>
      </div>
    </.use_avatar>
    """
  end
  1. There is no compile time validation for a.root etc. calls. Using a wrong {a.wrong} key will result in runtime error
  2. Since LiveView don’t know what a.root and others will contain it can’t properly track changes and has to mark all these places as dynamic rerender and send them on every update.

This could be solved with component macros but we don’t have them (yet :wink: ).

Instead I wrote a proof of concept “preprocessor”. The only change in client code is to replace def with defc, like this:

  import Headless

  defc avatar(assigns) do
    ~H"""
    <.use_avatar :let={a} src={@src}>
      <div {a.root}>
        <img {a.image} alt={@alt} />
        <div {a.fallback}><%= @initials %></div>
      </div>
    </.use_avatar>
    """
  end

Headless compiler will discover all known use_* calls and inline attributes, resulting in a flattened HEEX template like this:

  import Headless

  def avatar(assigns) do
    ~H"""
    <div x-data="hs_avatar">
      <img x-ref="image" data-src={@src} alt={@alt} />
      <div x-ref="fallback"><%= @initials %></div>
    </div>
    """
  end

This gives us the best of both worlds - easy to use function components with a standard syntax and compile-time validation plus performance improvement.

Demo is now a standard phoenix app so you can see how headless could be used as a dependency.

Where Next?

Trending in Announcing Top

bluzky
You may know https://ui.shadcn.com/, a UI component library for React. I really love it’s design style and components. I’ve built some co...
385 14863 120
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
shahryarjb
The Chelekom project is a library of Phoenix and LiveView components generated via Mix tasks to fit developer needs seamlessly. One of i...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
quatermain
Hello, I’m sharing my plugin here in forum after some time so it has time to mature and proof yourself. I use Claude Code daily on a pr...
New

Other Trending Topics Top

type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
bjorng
We want to introduce a new native datatype to Erlang: native records. Although replacing all tuple records with native records is not our...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
yureehuh
Introduction Founded in 2017 by landscape ecologist and fire mitigation expert Harry Statter, Frontline developed the first fully integra...
New

We're in Beta

About us Mission Statement