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!

25 Likes