Phlex provides a powerful and safe attribute system with special handling for classes, styles, and data attributes.
Basic Attributes
Pass attributes as keyword arguments to element methods:
class Example < Phlex::HTML
def view_template
div(id: "main", class: "container") do
h1 { "Hello" }
end
end
end
# => <div id="main" class="container"><h1>Hello</h1></div>
Attribute Types
String Attributes
div(attribute: "test")
# => <div attribute="test"></div>
a(href: "/about", title: "Learn more") { "About" }
# => <a href="/about" title="Learn more">About</a>
Symbol Attributes
Symbols are converted to strings with underscores replaced by hyphens:
div(data_controller: :user_profile)
# => <div data-controller="user-profile"></div>
button(type: :submit) { "Submit" }
# => <button type="submit">Submit</button>
Boolean Attributes
input(type: "checkbox", checked: true, disabled: false)
# => <input type="checkbox" checked>
button(disabled: true) { "Submit" }
# => <button disabled>Submit</button>
Numeric Attributes
input(type: "number", min: 0, max: 100, value: 42)
# => <input type="number" min="0" max="100" value="42">
div(data: { count: 42, ratio: 1.5 })
# => <div data-count="42" data-ratio="1.5"></div>
Date and Time Attributes
time(datetime: Date.new(2023, 1, 15)) { "January 15" }
# => <time datetime="2023-01-15">January 15</time>
time(datetime: Time.new(2023, 1, 15, 12, 30, 45, "+00:00"))
# => <time datetime="2023-01-15T12:30:45+00:00"></time>
Nil Attributes
Nil values are omitted from the output:
div(class: nil, id: "main")
# => <div id="main"></div>
button(disabled: false) { "Enabled" }
# => <button>Enabled</button>
Array Attributes
Arrays are joined with spaces:
div(class: ["container", "mx-auto", "px-4"])
# => <div class="container mx-auto px-4"></div>
div(attribute: ["hello", "world", nil, "test"])
# => <div attribute="hello world test"></div>
Hash Attributes
Hashes create nested attributes with dashes:
div(data: { controller: "hello", action: "click->hello#greet" })
# => <div data-controller="hello" data-action="click->hello#greet"></div>
# Nested hashes
div(data: { user: { id: 123, name: "John" } })
# => <div data-user-id="123" data-user-name="John"></div>
Special _ Key
Use the _ key to set the attribute itself:
div(data: { _: "test", controller: "hello" })
# => <div data="test" data-controller="hello"></div>
The Mix Helper
The mix helper merges multiple attribute hashes, with smart handling for different types:
class Button < Phlex::HTML
include Phlex::Helpers
def initialize(**attributes)
@attributes = attributes
end
def view_template(&block)
button(**default_attributes, &block)
end
private
def default_attributes
mix(
{ class: "btn", type: "button" },
@attributes
)
end
end
Button.new(class: "btn-primary").call { "Click me" }
# => <button class="btn btn-primary" type="button">Click me</button>
String + String
mix({ class: "foo" }, { class: "bar" })
# => { class: "foo bar" }
Array + Array
mix({ class: ["foo"] }, { class: ["bar"] })
# => { class: ["foo", "bar"] }
String + Array
mix({ class: "foo" }, { class: ["bar", "baz"] })
# => { class: ["foo", "bar", "baz"] }
Array + String
mix({ class: ["foo"] }, { class: "bar" })
# => { class: ["foo", "bar"] }
Hash + Hash
mix(
{ data: { controller: "foo" } },
{ data: { controller: "bar" } }
)
# => { data: { controller: "foo bar" } }
Array + Hash
mix(
{ data: ["foo"] },
{ data: { controller: "bar" } }
)
# => { data: { _: ["foo"], controller: "bar" } }
Nil Handling
mix({ class: "foo" }, { class: nil })
# => { class: "foo" }
mix({ class: nil }, { class: "bar" })
# => { class: "bar" }
Override with ! Suffix
Use the ! suffix to override instead of merge:
mix({ class: "foo" }, { class!: "bar" })
# => { class: "bar" }
Style Attributes
Style as String
div(style: "color: blue; font-weight: bold")
# => <div style="color: blue; font-weight: bold"></div>
Style as Hash
div(style: { color: "blue", font_weight: "bold" })
# => <div style="color: blue; font-weight: bold;"></div>
# With numeric values
div(style: { line_height: 1.5, z_index: 10 })
# => <div style="line-height: 1.5; z-index: 10;"></div>
# With symbol values
div(style: { flex_direction: :column_reverse })
# => <div style="flex-direction: column-reverse;"></div>
Style as Array
div(style: ["color: blue;", "font-weight: bold"])
# => <div style="color: blue; font-weight: bold;"></div>
# Mix strings and hashes
div(style: [
"color: blue;",
{ font_weight: "bold", line_height: 1.5 }
])
# => <div style="color: blue; font-weight: bold; line-height: 1.5;"></div>
ID Attribute Rules
The :id attribute must be a lowercase symbol:
div(id: "main") # ✅ Correct
div(:id => "main") # ✅ Correct
div("id" => "main") # ❌ Raises Phlex::ArgumentError
div(:ID => "main") # ❌ Raises Phlex::ArgumentError
Phlex enforces lowercase :id symbols to prevent attribute confusion and ensure consistency.
Security Features
Unsafe Attribute Names
Phlex blocks dangerous attribute names:
# ❌ These all raise Phlex::ArgumentError
div(onclick: "alert('xss')")
div(srcdoc: "<script>...</script>")
div(sandbox: "allow-scripts")
div("http-equiv": "refresh")
JavaScript URL Protection
Phlex automatically filters javascript: URLs:
a(href: "javascript:alert('xss')") { "Click" }
# => <a>Click</a> (href is omitted)
a(href: "/safe/path") { "Click" }
# => <a href="/safe/path">Click</a>
Safe Objects
Use safe to mark trusted content:
div(data: { content: safe("<b>Bold</b>") })
# => <div data-content="<b>Bold</b>"></div>
Real-World Patterns
Conditional Classes
class Card < Phlex::HTML
def initialize(featured: false, size: :medium)
@featured = featured
@size = size
end
def view_template(&block)
div(class: card_classes, &block)
end
private
def card_classes
[
"card",
@featured ? "card-featured" : nil,
"card-#{@size}"
]
end
end
Merging User Attributes
class Button < Phlex::HTML
include Phlex::Helpers
def initialize(variant: :primary, **user_attributes)
@variant = variant
@user_attributes = user_attributes
end
def view_template(&block)
button(**button_attributes, &block)
end
private
def button_attributes
mix(
base_attributes,
variant_attributes,
@user_attributes
)
end
def base_attributes
{
type: "button",
class: "btn"
}
end
def variant_attributes
case @variant
when :primary
{ class: "btn-primary" }
when :secondary
{ class: "btn-secondary" }
when :danger
{ class: "btn-danger" }
end
end
end
Button.new(variant: :primary, class: "mt-4", data: { action: "click" }).call
# => <button type="button" class="btn btn-primary mt-4" data-action="click">...</button>
Stimulus Integration
class Dropdown < Phlex::HTML
include Phlex::Helpers
def initialize(**attributes)
@attributes = attributes
end
def view_template(&block)
div(**dropdown_attributes) do
button(**trigger_attributes) { "Toggle" }
div(**menu_attributes, &block)
end
end
private
def dropdown_attributes
mix(
{ data: { controller: "dropdown" } },
@attributes
)
end
def trigger_attributes
{
data: { action: "click->dropdown#toggle" }
}
end
def menu_attributes
{
data: { dropdown_target: "menu" },
class: "dropdown-menu"
}
end
end
Dynamic Data Attributes
class ProductCard < Phlex::HTML
def initialize(product)
@product = product
end
def view_template
article(**card_attributes) do
h2 { @product.name }
p { @product.description }
end
end
private
def card_attributes
{
class: "product-card",
data: {
controller: "product",
product_id_value: @product.id,
product_price_value: @product.price,
product_available_value: @product.available?,
product: {
name: @product.name,
sku: @product.sku
}
}
}
end
end
# => <article class="product-card"
# data-controller="product"
# data-product-id-value="123"
# data-product-price-value="29.99"
# data-product-available-value="true"
# data-product-name="Widget"
# data-product-sku="WDG-001">...</article>