TunkShif

TunkShif

Sprout UI - unstyled and accessible components for Phoenix Framework

Sprout UI provides a collection of common UI components. All of these components are completely unstyled, and they are built following the WAI-ARIA guide to be accessible.


Currently, it is not ready for a stable release yet, but I just can’t wait to share the progress I’ve made and get some feedback. Check out the links below for more information.

Website (including documentation and component demos) | Github Repo


Design goals for Sprout UI:

renderless function component

If you’ve never heard about this concept, it’s highly recommended to check out this informative blog post by @LostKobrakai . Also check out libraries like Downshift JS and Zag JS in front-end world.

A renderless function component does not render any visible HTML tags, but just provides necessary context information for rendering. And it is the user’s responsibility to decide what component structure should be rendered like.

For example, a simple dialog component is used like this:

<.dialog :let={api}>
  <button {api.trigger_attrs}>Open modal</button>

  <div {api.container_attrs}>
    <div {api.backdrop_attrs}></div>
    <div {api.panel_attrs}>
      <h2 {api.title_attrs}>Dialog</h2>
      <p {api.description_attrs}>...</p>
    </div>
  </div>
</dialog>

The component itself won’t render any HTML structures for your dialog, but just takes care of all the needed attributes for user interactions and accessibility.

no unnecessary round-trip to server for user interactions

Under the hood, Sprout UI components are using custom elements for handling user interactions like clicking or keyboard events. You won’t need to communicate to the server-side just for opening a modal or toggling an element.

And it can be used with LiveView or just plain server-side rendered templates withour LiveView.

styling and transition made easy

Generally, a Sprout UI component is composed of multiple component parts. For example, the dialog example above may eventually rendered like some HTML structures like this:

<sp-dialog data-state="open">
  <button data-part="trigger">Open modal</button>

  <div data-part="container">
    <div data-part="backdrop"></div>
    <div data-part="panel" aria-attributes>
      <h2 data-part="title">Dialog</h2>
      <p data-part="description">...</p>
    </div>
  </div>
<sp-dialog>

sp-dialog is a custom element for handling dialog opening/closing interactions and managing focus. Each component part is rendered with a data-part attribute. And for components with multiple UI states, like dialog can be open or closed, the state is presented in a data-state attribute as well. You can use CSS attribute selector to style a component part or a specific state.

[data-part="backdrop"] {
  background-color: #000;
  opacity: 0.6;
}

:where([data-state="open"]) [data-part="backdrop"] {
  /* ... */
}

Also, Sprout UI comes with a Tailwind CSS plugin that makes it easier for styling different UI states using variants. So you can just do things like ui-open:opacity-100 ui-closed:opacity-0.

And Sprout UI provides a transition utility to help you add transitions when a component part entering or leaving in a declarative way. Here is an example of how you can add transitions for a dialog backdrop:

  <div
    data-transition
    data-enter="transition duration-300 ease-in-out"
    data-enter-from="opacity-0"
    data-enter-to="opacity-100"
    data-leave="transition duration-300 ease-in-out"
    data-leave-from="opacity-100"
    data-leave-to="opacity-0"
    {api.backdrop_attrs}
  >
  </div>

Currently I’m still having lots of work to do: writing tests and docs, fixing some existing issues with these components, implementing more common component patterns.

So feel free to try it out yourself and share your ideas!

Most Liked

teamon

teamon

I took great inspiration from Sprout when deciding on how to build headless, thank you @TunkShif :purple_heart:

Where Next?

Popular in Libraries Top

tmbb
I’ve been working on two packages (not on hex.pm yet) to build admin interfaces for phoenix apps: bureaucrat - which contains a bunch ...
New
pkrawat1
Hey guyz We at @aviabird are working on a payment library in elixir/phoenix. We are targeting March 2018 to add 56 Gateways to it. Have...
New
oltarasenko
Dear Elixir community, After a year of development, bug fixes, and improvements, we are proudly ready to share the release of Crawly 0.1...
New
josevalim
EDIT: since Ecto 3.0 final version is out, this post was amended to use the final versions in the instructions below. Hi everyone, We a...
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
New
Crowdhailer
I have been updating a library that allows you to pipe between functions that use the erlang result tuple convention. Assuming you have...
New
martinthenth
Hello everybody :wave: Recently, some of my colleagues talked about database ids and uuids and their problems, and I remembered the pain...
New
zorbash
I created Kitto a framework for dashboards inspired by Dashing. [demo] The distributed characteristics of Elixir and the low memory foo...
New
Eiji
ExApi is a library that I’m developing now and hope release soon This library will allow to: list all apis list all api implementation...
New

Other popular topics Top

vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

Sub Categories:

We're in Beta

About us Mission Statement