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.

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

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

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.

teamon

teamon

Thank you for the feedback.
I’ve seen doggo, but I think the goals are different (see Non-goals above)

I personally find JS commands a bit hard to read and understand, especially with all the interpolations but I do get the point about being dependency-less. I’m coming from the need to replace “full” SPA framework like Vue/React with Phoenix and just a little of JavaScript and Alpine seems to be a sensible solution.

As for the CSP so far I’ve used the mentioned Alpine.data() and x-bind heavily. I haven’t tried it but there’s a chance it even current version of combobox would work with CSP version of Alpine.

Where Next?

Popular in Libraries Top

zoltanszogyenyi
Hey everyone :wave: Excited to join this forum - I am one of the founders and current project maintainers of a popular and open-source U...
New
woylie
I released Doggo, a collection of unstyled Phoenix components. Features Unstyled Phoenix components. Storybook that can be added to...
New
markmark206
simple_feature_flags is a tiny package that lets you turn features on or off based on which environment (e.g. localhost, staging, product...
New
kelvinst
Hey everyone! Well, we made this lib a while ago and now we decided to finally go out and public with it! It’s a tool for creating and m...
New
mcrumm
If you would like to migrate away from node/npm/webpack while still using sass, the dart_sass package provides a installer and runner for...
New
danschultzer
In short Plug n’ play OAuth 2.0 provider library. Just set up a resource owner schema with Ecto (your user schema), install the dependen...
New
MRdotB
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
gabrielpoca
Hello everyone! I want to share with you something that I’m really proud of: https://stillstatic.io/ Still is a static site builder for...
New
ostinelli
Let’s write a database! Well not really, but I think it’s a little sad that there doesn’t seem to be a simple in-memory distributed KV da...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Sub Categories:

We're in Beta

About us Mission Statement