Tailwind not seeing used classes. Phoenix 1.7.1

I’m trying to restore Heroicons with a new CSS var- style introduced in 1.7.1 using a script to auto-generate all the functions. The result looks like this, aliased in html_helpers:

defmodule ProjectWeb.Heroicons do
  use Phoenix.Component

  import ProjectWeb.CoreComponents

  defp concat_name(name, solid, mini) do
    "hero-" <>
      case solid do
        "" ->
          case mini do
            "" -> name
            _ -> name <> "-mini"
          end
        _ -> name <> "-solid"
      end
  end

  attr :rest, :global,
  doc: "the arbitrary HTML attributes for the svg container",
  include: ~w(fill stroke stroke-width)

  attr :outline, :boolean, default: true
  attr :solid, :boolean, default: false
  attr :mini, :boolean, default: false

  # academic-cap.svg

  def academic_cap(assigns) do
    ~H"""
    <.icon name={concat_name("academic-cap", @solid, @mini)} {@rest}/>
    """
  end

  # ... same for all the rest

and the script generating this posted here: heroicons_gen for Phoenix 1.17.1 · GitHub

Well, of course Tailwind now doesn’t want to recognize any of the attempts to use new Heroicons classes
So for instance, this code:

<Heroicons.x_mark solid class="w-10 h-10 block text-black" />
<Heroicons.academic_cap solid class="w-10 h-10 block text-black " />

The x_mark is seen, since it’s used somewhere else. But the newly generated academic_cap is not.

I’m just hoping to get some insight on how to actually make Tailwind know about such classes, and possibly turn it into a universal approach.

I don’t understand your question. Can you explain a bit better what the issue is? What does tailwind have to do with heroicons?

By seen I mean visually seen on the page, with the usage of a unique class name, specifically made for each icon through the usage of new .icon component. However when I use .icon indirectly, through the generated Heroicons module, the classes are not added to the app.css

Is your ProjectWeb.Heroicons file path listed in tailwind.conf.js’s purge option?

edit: actually its not called purge anymore, its content.

Just a regular originally generated one.

// See the Tailwind configuration guide for advanced usage
// https://tailwindcss.com/docs/configuration

const plugin = require("tailwindcss/plugin")
const fs = require("fs")
const path = require("path")

module.exports = {
  content: [
    "./js/**/*.js",
    "../lib/*_web.ex",
    "../lib/*_web/**/*.*ex"
  ],
  theme: {
    extend: {
      colors: {
        brand: "#FD4F00",
      }
    },
  },
  plugins: [
    require("@tailwindcss/forms"),
    // Allows prefixing tailwind classes with LiveView classes to add rules
    // only when LiveView classes are applied, for example:
    //
    //     <div class="phx-click-loading:animate-ping">
    //
    plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])),
    plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
    plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
    plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),

    // Embeds Hero Icons (https://heroicons.com) into your app.css bundle
    // See your `CoreComponents.icon/1` for more information.
    //
    plugin(function({matchComponents, theme}) {
      let iconsDir = path.join(__dirname, "../priv/hero_icons/optimized")
      let values = {}
      let icons = [
        ["", "/24/outline"],
        ["-solid", "/24/solid"],
        ["-mini", "/20/solid"]
      ]
      icons.forEach(([suffix, dir]) => {
        fs.readdirSync(path.join(iconsDir, dir)).map(file => {
          let name = path.basename(file, ".svg") + suffix
          values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
        })
      })
      matchComponents({
        "hero": ({name, fullPath}) => {
          let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
          return {
            [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
            "-webkit-mask": `var(--hero-${name})`,
            "mask": `var(--hero-${name})`,
            "background-color": "currentColor",
            "vertical-align": "middle",
            "display": "inline-block",
            "width": theme("spacing.5"),
            "height": theme("spacing.5")
          }
        }
      }, {values})
    })
  ]
}

The Tailwind compiler only finds explicit class names. It can’t “see” the classes generated thru your concat_name function.

Hope that helps!

I’ve solved this issue in two different ways:

  1. Add a comment with the full class name.

  2. Using safelist feature in tailwind.config.js:

2 Likes

This documents the behaviour.

1 Like

Well, built-in

def icon(%{name: "hero-" <> _} = assigns) do
    ~H"""
    <span class={[@name, @class]} />
    """
end

works like a charm, so there’s definitely something more to it.

I really can’t trace how exactly Chris McCord made it work! :smiley:

I think this is the part you’re looking for - it pre-generates the needed classes:

1 Like

@fceruti where (js, ex) did you place the comment and in what form?

Edit: Actually, it seams that a literal <.icon name="hero-arrow-path-solid" {@rest}/> leaves the class intact. This is promising!

Thank you, @mcrumm, @fceruti, @LostKobrakai

It works, well, almost

Now this is what the script generates:

  attr :rest, :global,
  doc: "the arbitrary HTML attributes for the svg container",
  include: ~w(fill stroke stroke-width)

  attr :outline, :boolean, default: true
  attr :solid, :boolean, default: false
  attr :mini, :boolean, default: false

  @doc """
  academic-cap.svg
  """
  def academic_cap(%{solid: true} = assigns) do
    ~H"""
    <.icon name={"hero-academic-cap-solid"} {@rest}/>
    """
  end

  def academic_cap(%{mini: true} = assigns) do
    ~H"""
    <.icon name={"hero-academic-cap-mini"} {@rest}/>
    """
  end

  def academic_cap(assigns) do
    ~H"""
    <.icon name={"hero-academic-cap"} {@rest}/>
    """
  end

and here’s the usage:

<Heroicons.bolt outline class="w-10 h-10 block text-black" />
<Heroicons.bolt solid class="w-10 h-10 block text-black" />
<Heroicons.bolt mini class="w-10 h-10 block text-black" />

However, this code retains ALL .hero-* classes in the app.css. Well, I guess, I’m “using” the classes.

Updated gist: heroicons_gen for Phoenix 1.17.1 · GitHub

Boy, this Tailwind CSS thing brings in other hassles!

:point_up_2: The Tailwind JIT just scans your source code for Tailwind classes. You can literally put “text-green-500” in a comment and it will be in the generated Tailwind CSS.

I put them in CoreComponents

defmodule CoreComponents do
  @moduledoc """
    These are core components.
  """

  def flash(assigns) do
    # This components needs these Daisy/Tailwind classes: alert-info alert-success alert-warning alert-danger
    ~H"""
    <div class={"alert alert-#{@type}"}><%= @msg %></div>
    """
  end
end
1 Like

tailwind needs to be able to see the class (or class match prefix) in order to bake the styles into the app css bundle. The plugin approach we shipped sees "hero-cake" from <.icon name="hero-cake" /> and bakes .hero-cake { ... } css rule into app.css. It matches on the hero- prefix, so to generate a module/func interface that also let tailwind see the names and bake the minimal set into the bundle, you’d need to change the plugin matcher to either look for your Heroicons prefix or go with a full class-name-in-usage approach like we do with name="hero-cake-solid". If you choose the fancier module or function based approach you’ll need to make it work for both local imports and full module calls, so your best bet is something like Heroicons.icon_cake_solid and have the plugin match on icon_. Hope that helps!

3 Likes

Thank you Chris! This is valuable, I’m gonna try this approach out

Heh, one thing that is now definitely possible is simply use class="hero-*" classes directly in any DOM tag, instead of <.icon name="hero-* which doesn’t offer auto-completion on the name attribute.

That’s actually all I needed, so I can ditch my function-based approach all-together!