CI Version

Compile-time SVG inliner for Phoenix components.

Stencil reads SVG files from a directory at compile time and embeds them into a Phoenix component you can call from HEEx. The running app performs no disk I/O because the raw SVG bytes are baked into the module's beam file. New or changed files re-compile automatically when an SVG changes on disk.

This is a port of Lucky Framework's implementation.

Installation

Add stencil to your mix.exs:

def deps do
  [
    {:stencil, "~> 0.3.0"}
  ]
end

Usage

Put your SVGs under priv/svg/ in your app, then create a thin wrapper module that uses Stencil.Component. The conventional location is lib/my_app_web/components/inline_svg.ex:

defmodule MyAppWeb.InlineSVG do
  use Stencil.Component
end

To make <.inline_svg> available in every HEEx template, import the wrapper inside MyAppWeb's html_helpers (in lib/my_app_web.ex) alongside MyAppWeb.CoreComponents:

defp html_helpers do
  quote do
    # ...
    import MyAppWeb.CoreComponents
    import MyAppWeb.InlineSVG
    # ...
  end
end

Now call it from any controller view, LiveView, or component template:

<.inline_svg path="check" />
<.inline_svg path="arrow-right" class="icon" />
<.inline_svg path="icons/star" />
<.inline_svg path="logo" strip_styling={false} />

The path attribute is the file name (or nested path) under priv/svg, without the .svg extension.

Options

use Stencil.Component, dir: "priv/icons"
  • :dir: directory to scan, relative to the project root (or an absolute path). Defaults to "priv/svg".

Styling

By default, Stencil strips the SVG's own class, fill, stroke, stroke-width, and style attributes so page CSS controls the visual. Any attributes you pass to <.inline_svg> are forwarded onto the wrapping <svg> tag:

<.inline_svg path="check" class="w-6 h-6 text-emerald-500" />

Pass strip_styling={false} to keep the file's original attributes:

<.inline_svg path="logo" strip_styling={false} />

Stencil also adds a marker attribute so you can target inlined SVGs from CSS:

  • data-inline-svg="<path>" when styling is stripped
  • data-inline-svg-styled="<path>" when it is kept

Compile-time validation

Every SVG name is registered as a :values entry on the :path attribute, so literal calls in HEEx are checked at compile time:

<.inline_svg path="chek" />

produces a compiler warning like:

attribute "path" in component MyAppWeb.InlineSVG.inline_svg/1
must be one of ["arrow-right", "check", …], got: "chek"

The check is a warning by default so a typo won't halt an incremental compile. To make it fail the build, compile with --warnings-as-errors. This is a common setup in CI and in precommit aliases:

# mix.exs
defp aliases do
  [precommit: ["compile --warnings-as-errors", "format", "test", "credo"]]
end

Dynamic paths can't be validated at compile time. <.inline_svg path={...}> would emit that same "must be one of […]" warning on every interpolated call, which is noisy and (under --warnings-as-errors) a build break. For those, use dynamic_inline_svg/1: same behaviour and options, no compile-time values: check:

<.inline_svg path="check" />                     # compile-checked
<.dynamic_inline_svg path={"icons/#{@name}"} />  # runtime-checked

Runtime lookup still raises ArgumentError for unknown names in both cases.

How it works

use Stencil.Component walks the configured directory at macro-expansion time and generates one function head per SVG:

defp stencil_raw_svg("check", true), do: "<svg …>…</svg>"

Each file is registered as an @external_resource, so the module re-compiles when any SVG changes. A __mix_recompile__?/0 callback also detects added or removed files.

License

MIT.