Skip to content

Lesson 5 — SVG components

Why SVG in Phlex?

Icons are everywhere in modern UIs, and inline SVG is often the best approach: perfectly sharp at any size, styleable with CSS, no extra HTTP request, and accessible with aria attributes. ERB makes inline SVG painful — you end up with raw XML pasted into your templates. Phlex makes SVG a first-class citizen.

Phlex::SVG vs Phlex::HTML

For SVG content, inherit from Phlex::SVG instead of Phlex::HTML. This gives you all the SVG element methods (path, circle, rect, line, polyline, polygon, g, defs, use, etc.):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require "phlex"

class StarIcon < Phlex::SVG
  def view_template
    svg(
      xmlns:   "http://www.w3.org/2000/svg",
      width:   "24",
      height:  "24",
      viewBox: "0 0 24 24",
      fill:    "none",
      stroke:  "currentColor"
    ) do
      path(
        stroke_linecap:  "round",
        stroke_linejoin: "round",
        stroke_width:    "2",
        d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.95-.69l1.519-4.674z"
      )
    end
  end
end

puts StarIcon.new.call

Notice stroke_linecap: — underscores in attribute names are converted to hyphens, matching SVG conventions.

Embedding SVG inside HTML components

A Phlex::SVG component can be rendered inside a Phlex::HTML component just like any other component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
require "phlex"

class CheckIcon < Phlex::SVG
  def view_template
    svg(xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 20 20", fill: "currentColor") do
      path(
        fill_rule: "evenodd",
        d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
        clip_rule: "evenodd"
      )
    end
  end
end

class SuccessAlert < Phlex::HTML
  def initialize(message)
    @message = message
  end

  def view_template
    div(role: "alert") do
      render CheckIcon.new
      span { @message }
    end
  end
end

puts SuccessAlert.new("Your changes have been saved.").call

A reusable icon system

In practice, you’ll want a family of icons with a consistent interface. Here’s a pattern that works well:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
require "phlex"

# Base class for all icons — handles common attributes
class BaseIcon < Phlex::SVG
  def initialize(size: 24, classes: nil, aria_label: nil)
    @size       = size
    @classes    = classes
    @aria_label = aria_label
  end

  def view_template
    svg(
      xmlns:       "http://www.w3.org/2000/svg",
      width:       @size,
      height:      @size,
      viewBox:     "0 0 24 24",
      fill:        "none",
      stroke:      "currentColor",
      stroke_width: "2",
      class:        @classes,
      aria: { label: @aria_label, hidden: @aria_label.nil? }
    ) do
      icon_paths
    end
  end

  private

  # Subclasses override this to provide their specific paths
  def icon_paths
    raise NotImplementedError, "#{self.class} must implement icon_paths"
  end
end

class HomeIcon < BaseIcon
  private

  def icon_paths
    path(d: "M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z")
    polyline(points: "9 22 9 12 15 12 15 22")
  end
end

class UserIcon < BaseIcon
  private

  def icon_paths
    path(d: "M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2")
    circle(cx: "12", cy: "7", r: "4")
  end
end

class SettingsIcon < BaseIcon
  private

  def icon_paths
    circle(cx: "12", cy: "12", r: "3")
    path(d: "M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z")
  end
end

# Now use them:
class NavBar < Phlex::HTML
  def view_template
    nav do
      render HomeIcon.new(size: 20, aria_label: "Home")
      render UserIcon.new(size: 20, aria_label: "Profile")
      render SettingsIcon.new(size: 20, aria_label: "Settings")
    end
  end
end

puts NavBar.new.call

This pattern — a base class with a icon_paths hook — gives every icon a consistent size API, accessibility attributes, and class support, while keeping each icon file to just the path and shape definitions.

Exercise

Add a ChevronIcon to the icon family from the lesson. The SVG path is a single polyline:

1
polyline(points: "9 18 15 12 9 6")

Add it to the NavBar component alongside the existing HomeIcon, UserIcon, and SettingsIcon. Then redirect the output to an HTML file and open it in a browser:

1
ruby 05_exercise.rb > nav_bar.html

You should see:


Solution to Exercise 05

Add this class after the SettingsIcon class, and add a render line in the view_template.

1
2
3
4
5
6
7
class ChevronIcon < BaseIcon
  private

  def icon_paths
    polyline(points: "9 18 15 12 9 6")
  end
end