Lesson 5 — Icon
KanbanFlow uses icons throughout — alert variants, toast notifications,
dismiss buttons, status badges. Rather than sprinkling emoji or raw SVG
inline across components, we build a single Icon component that owns
all HeroIcon paths and renders consistent, accessible SVG.
HeroIcons
HeroIcons is an open-source SVG icon set designed by the makers of Tailwind CSS. It comes in two variants — outline (stroke-based, the style we use) and solid (filled). The outline variant pairs naturally with Tailwind’s text colour utilities because the strokes inherit currentColor.
HeroIcons is MIT licensed, meaning you can use it freely in personal and commercial projects without attribution. The full set is browsable at heroicons.com — search by name, click an icon, and copy the SVG path data directly.
We embed the path data directly in the component rather than loading HeroIcons as a package. This keeps the asset pipeline simple (no Node, no npm), gives us complete control over which icons are available, and means the component has no runtime dependencies beyond Phlex itself. The tradeoff is manual updates if HeroIcons changes a path — in practice this rarely matters since icon shapes are stable across versions.
Why a component rather than a helper
The instinct for icons is often a view helper — icon(:x_mark) that
returns an HTML string. A Phlex component is better for two reasons.
First, it composes naturally. Icon(name: :x_mark, class_name: "h-4 w-4") inside a component template renders inline without raw or
html_safe — Phlex handles the output correctly. A helper returning a
string would need raw safe(...) at every call site.
Second, it’s testable. We can assert that Icon renders an svg with
aria-hidden="true", that an unknown name raises ArgumentError, and
that the correct path data is present — all with the same
ComponentTestHelper used for every other Phlex::UI component.
A Phlex gotcha: path inside svg
Before looking at the component, there’s a Phlex naming conflict worth knowing about.
Inside any Phlex component, path is an HTML element method — it
renders a <path> element in an HTML context. When you open an svg
block and try to call path(...) bare, Phlex sees its own HTML method,
not an SVG element call, and the result is wrong or raises an error.
The fix is to use the block argument that Phlex yields:
|
|
s is the component instance itself (Phlex’s yield(self) behaviour).
Calling s.path makes the intent unambiguous. This same issue affects
any SVG element whose name collides with an HTML tag — path is the
most common case in practice.
This is the same class of problem as the slot naming conflicts from
Module 3 (header, footer, body are all Phlex HTML methods). The
rule is consistent: if a method name is also an HTML tag, use an
explicit receiver to disambiguate.
The component
|
|
Design decisions
paths: is an array. Most HeroIcons outline variants use a single
<path> element, but some use two. Storing paths as an array handles
both cases identically — adding a two-path icon later is just adding a
second string to the array, with no structural change to the component.
class_name not class. Ruby reserves class as a keyword, so
Phlex components can’t use it as a prop name. class_name is the
established convention. The default "h-6 w-6" is the standard HeroIcon
display size — override at the call site when you need something smaller
or larger.
aria_hidden: "true". Icons in Phlex::UI are decorative by
default — they accompany text labels that already convey the meaning.
aria-hidden="true" removes them from the accessibility tree so screen
readers don’t read out “svg” or “image” for every icon in the UI. If
you ever use an icon as the sole content of a button (no label), add a
visible or sr-only span with descriptive text alongside it.
stroke: "currentColor". The SVG stroke inherits the CSS
color property of its parent element. This means icon colour is
controlled by Tailwind text utilities — text-danger, text-success,
text-text-muted — without any prop or variant on Icon itself.
Usage
|
|
Adding icons
To add a new HeroIcon, copy the path data from
heroicons.com, choose the outline variant
(stroke, not filled), and add an entry to ICONS:
|
|
The outline variant uses fill: "none" and stroke: "currentColor",
which matches the svg_attributes already set. The solid variant
(filled) would need different SVG attributes — add a variant prop if
you need both.
Lookbook preview
|
|
Tests
|
|
Using Icon in other components
From Module 8 onwards, Alert and Toast replace their emoji icons
with Icon. The pattern is the same in both:
|
|
Where ICON_NAMES maps variant symbols to icon name symbols:
|
|
The dismiss button in both components uses :x_mark:
|
|
Tags: #phlex #rails #components #icons #heroicons
#accessibility #svg #tutorial