"Petal Components" package - a set of Tailwind styled components written in HEEX

With HEEX released we decided to start a components library using Tailwind CSS - check it out here: Petal Components.

We also have a boilerplate project if you want to get up and running quickly - it’s just a fresh install of Phoenix with Tailwind/Alpine and petal_components included.

Petal Components allows you to get up and running with a suite of styled components eg:

  <Typography.h1>Petal Components</Typography.h1>
  <Button.a href="/" label="a" />
  <Button.button label="Button" />
  <Button.patch href="/" label="Live Patch" />
  <Button.redirect href="/" label="Live Redirect" />

  <!-- Includes every single heroicon -->
  <Heroicons.Solid.home class="w-5 h-5" />

  <!-- Includes all the basic form elements. eg: -->
  <Form.text_input form={:user} field={:name} placeholder="eg. John" />
  <Form.textarea form={:user} field={:description} />

  <!--Plus much more -->

We plan to add more components over time and eventually add a “pro” (paid) version that adds things like styled authentication pages / Stripe integration / email templates etc… Does this sound like something you guys would be interested in?

We welcome your thoughts or any ideas for components. Cheers


I for one would be terribly interested in this sort of thing, as prophesied by Chris McChord.

Would be interesting to hear your thoughts on the terms of the license of Tailwind UI (haven’t reviewed these yet, but I have secured the full license in any event).

Is there any way to achieve your goals without Alpine?

Kind regards


We’re pretty much building our components from scratch (taking influence from a number of sources). As far as we’re aware none of our components contravene any licensing terms.

And yes we’re going to try remove the Alpine dependency. eg. with the dropdown component I tried to use the new Phoenix.LiveView.JS lib but the click.away event was giving me headaches. Seems like Chris has fixed that now so I’ll try again.


This is super great! I have a bunch of similar components in my own apps. My one big suggestion would be to consolidate the modules. You could likely have a single module with the current surface area. One of the goals of function components was to push folks away from the one-function-per-module for component pattern. By grouping the functions, they are more discoverable, doc’d in one place, etc. For example, folks could import PetalComponent (or single alias) and have most core UI components available as <.link, <.button. I’m not saying everything must be define under the same namespace, but the current surface area would be really well suited and would be nice in user land. Awesome work!


This looks pretty awesome :heart:

And your website https://petal.build/ looks pretty slick as well :smiley:


Thanks Chris! That makes sense - I’ll try and group the functions. I had them written in Surface previously so was wondering how to do it.


Thanks Joe - haha good find - yeah eventually I’ll put a demo there and also have a pro version. It’s still a WIP, but people can sign up now and I’ll send out an email to users once it’s complete.


Very interested in using something like this. Coming from a React background it makes the dev feel more familiar. Wondering if you plan to remove Node/NPM totally (not sure how is that is with tailwind), having that removed and Alpine removed would make for a great setup, less is better if the same functionality can be achieved.

Side note: I did not realise that you setup github as a template repo till I saw you github link, something I have an immediate use for, so glad I took a look.


Unfortunately Tailwind needs Node for the tailwindcss CLI to work. You could try just loading the entire Tailwind CSS lib in your <head> but it’d be massive. Using their CLI means your css file only has css definitions for the classes you actually use.

Yep I haven’t really used Github templates but for anyone else you can click the “Use template” button on the boilerplate project to get up and running quickly. There’s also a script in there to rename your project to whatever.


Tailwind will get a CLI without dependencies with their 3.0 release, so you can scrap the JS runtime at that point.


this is just ggggGGREATTtt. :smiley:

humble thanks from the bottom of my heart!

1 Like

Great work!

BTW, I encountered an error when mix setup. Is the version0.2.1 not updated?

== Compilation error in file lib/petal_boilerplate_web/live/page_live.ex ==
** (UndefinedFunctionError) function PetalComponents.__using__/1 is undefined or private
    (petal_components 0.2.1) PetalComponents.__using__([])
    lib/petal_boilerplate_web/live/page_live.ex:2: (module)
    (stdlib 3.15.2) erl_eval.erl:685: :erl_eval.do_apply/6
    (elixir 1.12.3) lib/kernel/parallel_compiler.ex:319: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7

I forced the version to 0.3.2 and it passed. But the assets failed:

19:31:23.673 [info]  Migrations already up
npm WARN read-shrinkwrap This version of npm is compatible with lockfileVersion@1, but package-lock.json was generated for lockfileVersion@2. I'll try to do my best with it!
npm ERR! code EACCES
npm ERR! syscall open
npm ERR! path /home/xxx/.npm/_cacache/index-v5/0c/ba/3efdcf9368b37769f14ae73c1feb025960f50c09d844da4be8d3e295b485
npm ERR! errno -13
npm ERR! 
npm ERR! Your cache folder contains root-owned files, due to a bug in
npm ERR! previous versions of npm which has since been addressed.
npm ERR! 
npm ERR! To permanently fix this problem, please run:
npm ERR!   sudo chown -R 1001:1001 "/home/xxx/.npm"

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/xxx/.npm/_logs/2021-11-15T11_31_38_208Z-debug.log
** (exit) 243
    (mix 1.12.3) lib/mix/tasks/cmd.ex:64: Mix.Tasks.Cmd.run/1
    (mix 1.12.3) lib/mix/task.ex:394: anonymous fn/3 in Mix.Task.run_task/3
    (mix 1.12.3) lib/mix/task.ex:452: Mix.Task.run_alias/5
    (mix 1.12.3) lib/mix/cli.ex:84: Mix.CLI.run_task/2
    (elixir 1.12.3) lib/code.ex:1261: Code.require_file/2

Thanks for this.

I updated mix.exs to point to the latest version of petal_components to fix that first issue (will keep that up to date in future).

But I’m not sure about the npm issue - it works fine for me. Maybe try updating node/npm or running that command there in the error message sudo chown -R 1001:1001 "/home/xxx/.npm".

1 Like

Project update:

  • The dropdown now can utilise Phoenix.Liveview.JS if you don’t want to use Alpine. However, this only works in live views or live components (dead views will need Alpine).
  • Function components are imported so you don’t need the module anymore - eg <.button> instead of <Button.button>

New components

Form fields

You can write form fields like this:

<.form_label form={f} field={:name} />
<.text_input form={f} field={:name} placeholder="eg. John" />
<.form_field_error form={f} field={:name} class="mt-1" />

Or as one component that includes a label and error automatically:

  placeholder="eg. John"


<.alert state="success" label="This is a success state" />



<.dropdown label="Dropdown" js_lib="alpine_js | live_view_js">
  <.dropdown_menu_item type="button">
    <Heroicons.Outline.home class="w-5 h-5 text-gray-500" />
    Button item with icon
  <.dropdown_menu_item type="a" href="/" label="a item" />
  <.dropdown_menu_item type="live_patch" href="/" label="Live Patch item" />
  <.dropdown_menu_item type="live_redirect" href="/" label="Live Redirect item" />



<.breadcrumbs separator="chevron" links={[
  %{ label: "Link 1", to: "#" },
  %{ label: "Link 2", to: "#", link_type: "live_patch" },
  %{ label: "Link 3", to: "#", link_type: "live_redirect" },

New version release (v0.5)

New form field types

Now every form field type is supported.

<.email_input form={f} field={:email_input} />
<.number_input form={f} field={:number_input} />
<.password_input form={f} field={:password_input} />
<.search_input form={f} field={:search_input} />
<.telephone_input form={f} field={:telephone_input} />
<.url_input form={f} field={:url_input} />
<.time_input form={f} field={:time_input} />
<.time_select form={f} field={:time_select} />
<.datetime_local_input form={f} field={:datetime_local_input} />
<.datetime_select form={f} field={:datetime_select} />
<.color_input form={f} field={:color_input} />
<.file_input form={f} field={:file_input} />
<.range_input form={f} field={:range_input} />

Progress bars

<.progress color="primary" value={15} max={100} class="max-w-xl" />
<.progress color="secondary" value={30} max={100} class="max-w-xl" />
<.progress color="info" value={45} max={100} class="max-w-xl" />
<.progress color="success" value={60} max={100} class="max-w-xl" />
<.progress color="warning" value={75} max={100} class="max-w-xl" />
<.progress color="danger" value={90} max={100} class="max-w-xl" />


Links can be live_patch or live_redirect. Automatically adds the ellipsis so you can have unlimited pages (inspired by Material UI).


<.pagination link_type="a" class="mb-5" path="/:page" current_page={1} total_pages={10} />
<.pagination link_type="live_patch" class="mb-5" path="/:page" current_page={5} total_pages={10} />
<.pagination link_type="live_redirect" class="mb-5" path="/:page" current_page={10} total_pages={10} />

New update! - v0.6.1

New component: Modals

# Live view

@impl true
def mount(_params, _session, socket) do
  {:ok, assign(socket, :show_modal, false)}

@impl true
def handle_params(_params, _uri, socket) do
  case socket.assigns.live_action do
    :index ->
      {:noreply, assign(socket, show_modal: false)}
    :show ->
      {:noreply, assign(socket, show_modal: true)}

# This event is emitted by the component and must be catched
@impl true
def handle_event("close_modal", _, socket) do
  {:noreply, push_patch(socket, to: "/index")}

@impl true
def render(assigns) do
  <.button label="Show modal" link_type="live_patch" to="/show" />

  <%= if @modal do %>
    <.modal max_width="sm | md | lg | xl | 2xl | full" title="Modal">

      Modal contents goes here

      <div class="gap-5 text-sm">
        <div class="flex justify-end">
          <.button label="close" phx-click={PetalComponents.Modal.hide_modal()} />
  <% end %>

Looks like it might be time to get back to some UI projects after a loooong absence. No more node.js required with Tailwind CSS:

Particularly happy to see some support for RTL and LTR layouts!


hi !

thanks for your component library.

FYI: i just mentioned your post in an issue i had with live_view:

i’m just curious, as you spent some time with that:

would you rather like to use

  • dash case consistently ( link-type="live_patch")
  • snake_case consistently (phx_click="foo")
  • keep mixing snake/dash case like now


Hi everyone!

I’m the other developer contributing to Petal. We’re pleased to announce our new docs page has gone live!

You can check it out now here: petal.build

There’s live demo’s of all the components and code snippets you can directly copy and paste into your projects - soon you’ll be able to download the boilerplate (hopefully over the coming week or two). We’d really welcome your feedback on both the new site and boilerplate (when the latter becomes available).

New update to Petal Components - v0.9.0