sections/footers.ex
Footer
A page footer with the EmpoweredHouse lockup, three columns of links and a bottom line.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_footers animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
wordmark |
string | "EmpoweredHouse" |
|
tagline |
string | "Software and agents that run your operations." |
Wraps onto up to three lines of about 24 characters. |
columns |
list | [%{links: ["Workflows", "Agents", "Intelligence", "Pricing"], ...}, ...] |
Up to three `%{title, links}` with up to five links of about 12 characters each. |
legal |
string | "© 2026 EmpoweredHouse" |
|
policies |
list | ["Privacy", "Terms"] |
Short links on the bottom line. |
Frame props, shared by every composition
| Prop | Type | Default | What it does |
|---|---|---|---|
animated |
boolean | false |
Turns motion on. Without it the composition renders in its final, still state. |
trigger |
"mount" | "in-view" | "in-view-repeat" | "in-view" |
When the entrance plays: on page load, once when scrolled into view, or every time it comes back into view. |
hover |
boolean | false |
Enables the hover micro-interactions the composition supports. |
glow |
boolean | true |
The warm incandescent halo behind the composition. |
fade_out |
boolean | false |
Fades the bottom edge into the surface underneath. |
isometric |
boolean | false |
Tilts the composition into the isometric plane of the platform stack. |
theme |
"dark" | "light" | "dark" |
`dark` for the site canvas, `light` for the cream reversed surface. |
class |
string | nil |
Extra classes for the frame. |
rest |
global | — |
Any other HTML attribute, set on the frame. |
Source
Two files. Copy them into your app if you would rather own the code than depend on the library.
lib/eh_visuals/sections/footers.ex
defmodule EHVisuals.Sections.Footers do
@moduledoc """
A page footer: the EmpoweredHouse lockup, columns of links and a bottom line.
"""
use EHVisuals.Composition,
name: "sections/footers",
title: "Footer",
description:
"A page footer with the EmpoweredHouse lockup, three columns of links and a bottom line."
@columns [
%{title: "Product", links: ["Workflows", "Agents", "Intelligence", "Pricing"]},
%{title: "Company", links: ["About", "Customers", "Careers", "Contact"]},
%{title: "Resources", links: ["Docs", "Changelog", "Security", "Status"]}
]
attr(:wordmark, :string, default: "EmpoweredHouse")
attr(:tagline, :string,
default: "Software and agents that run your operations.",
doc: "Wraps onto up to three lines of about 24 characters."
)
attr(:columns, :list,
default: @columns,
doc: "Up to three `%{title, links}` with up to five links of about 12 characters each."
)
attr(:legal, :string, default: "© 2026 EmpoweredHouse")
attr(:policies, :list, default: ~w(Privacy Terms), doc: "Short links on the bottom line.")
attr(:label, :string,
default:
"A page footer with the EmpoweredHouse lockup, three columns of links and a legal line"
)
frame_attrs()
@spec sections_footers(map()) :: Phoenix.LiveView.Rendered.t()
def sections_footers(assigns) do
shown = Enum.take(assigns.columns, 3)
# Three columns sit 70 apart; fewer spread over the same width.
pitch = if length(shown) > 2, do: 70, else: 105
columns =
shown
|> Enum.with_index()
|> Enum.map(fn {column, index} ->
%{
title: column.title,
links: column.links |> Enum.take(5) |> Enum.with_index(),
index: index,
x: 176 + index * pitch
}
end)
tagline = assigns.tagline |> wrap(24) |> Enum.take(3)
links = columns |> Enum.map(&length(&1.links)) |> Enum.max(fn -> 1 end)
rule_y = max(58 + (links - 1) * 18, 62 + (length(tagline) - 1) * 14) + 20
assigns =
assigns
|> assign(:cols, columns)
|> assign(:tagline_lines, Enum.with_index(tagline))
|> assign(:rule_y, rule_y)
|> assign(:height, rule_y + 40)
~H"""
<.frame name="sections/footers" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 400 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="399" height={@height - 1} rx="12" />
<g class="ehv-a-fade ehv-i-1">
<.mark x="17.4" y="19" size="30" />
<text class="ehv-t-strong" x="47" y="39" font-size="14">{@wordmark}</text>
</g>
<text
:for={{line, index} <- @tagline_lines}
class="ehv-t-3 ehv-a-fade ehv-i-2"
x="24"
y={62 + index * 14}
font-size="10.5"
>
{line}
</text>
<g :for={column <- @cols} class={"ehv-a-rise ehv-i-#{column.index + 2}"}>
<text class="ehv-mono" x={column.x} y="37" font-size="8">
{String.upcase(column.title)}
</text>
<text
:for={{item, row} <- column.links}
class="ehv-t-2 ehv-sections-footers__link"
x={column.x}
y={58 + row * 18}
font-size="10.5"
>
{item}
</text>
</g>
<g class="ehv-a-fade ehv-i-6">
<line class="ehv-line-faint" x1="24" x2="376" y1={@rule_y + 0.5} y2={@rule_y + 0.5} />
<text class="ehv-mono" x="24" y={@rule_y + 22} font-size="8">
{String.upcase(@legal)}
</text>
<text class="ehv-mono" x="376" y={@rule_y + 22} font-size="8" text-anchor="end">
{@policies |> Enum.take(3) |> Enum.map_join(" · ", &String.upcase/1)}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
defp wrap(text, limit) do
text
|> String.split()
|> Enum.reduce([], fn
word, [] ->
[word]
word, [line | rest] ->
if String.length(line) + 1 + String.length(word) <= limit,
do: ["#{line} #{word}" | rest],
else: [word, line | rest]
end)
|> Enum.reverse()
end
end
assets/css/sections/footers.css
/* sections/footers */
.ehv-sections-footers { --ehv-max-w: 440px; }
.ehv--hover .ehv-sections-footers__link { transition: fill 200ms var(--ehv-ease); }
.ehv--hover .ehv-sections-footers__link:hover { fill: var(--ehv-ink); }