sections/headers.ex
Header
A page header with the EmpoweredHouse lockup, navigation links and one lime action, above a ghosted page.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_headers animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
brand |
string | "EmpoweredHouse" |
The wordmark next to the mark. Up to 16 characters. |
links |
list | ["Product", "Agents", "Pricing", "Docs"] |
Up to five link names, about 28 characters in total. |
active |
integer | 0 |
Index of the link for the current page. A negative index marks none. |
action |
string | "Book a demo" |
The button. Up to 14 characters. |
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/headers.ex
defmodule EHVisuals.Sections.Headers do
@moduledoc """
A page header: the EmpoweredHouse lockup, navigation links and one lime
action, sitting on top of a ghosted page.
"""
use EHVisuals.Composition,
name: "sections/headers",
title: "Header",
description:
"A page header with the EmpoweredHouse lockup, navigation links and one lime action, above a ghosted page."
attr(:brand, :string,
default: "EmpoweredHouse",
doc: "The wordmark next to the mark. Up to 16 characters."
)
attr(:links, :list,
default: ~w(Product Agents Pricing Docs),
doc: "Up to five link names, about 28 characters in total."
)
attr(:active, :integer,
default: 0,
doc: "Index of the link for the current page. A negative index marks none."
)
attr(:action, :string, default: "Book a demo", doc: "The button. Up to 14 characters.")
attr(:label, :string, default: "A page header with a logo, four links and a button")
frame_attrs()
@spec sections_headers(map()) :: Phoenix.LiveView.Rendered.t()
def sections_headers(assigns) do
action_w = Float.round(38 + String.length(assigns.action) * 5.9, 1)
action_x = 462 - action_w
brand_end = 43 + String.length(assigns.brand) * 7.8
assigns =
assigns
|> assign(:action_w, action_w)
|> assign(:action_x, action_x)
|> assign(:items, place(Enum.take(assigns.links, 5), brand_end + 14, action_x - 16))
~H"""
<.frame name="sections/headers" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 480 224">
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="479" height="223" rx="12" />
<g class="ehv-a-fade ehv-i-1">
<.mark x="14" y="13" size="30" />
</g>
<text
class="ehv-t-strong ehv-sections-headers__word ehv-a-slide ehv-i-2"
x="43"
y="32.5"
font-size="13"
>
{@brand}
</text>
<g :for={{item, index} <- @items} class={"ehv-a-fade ehv-i-#{index + 3}"}>
<text class={[index != @active && "ehv-t-3"]} x={item.x} y="32" font-size="11">
{item.text}
</text>
<line
:if={index == @active}
class="ehv-line-ink"
x1={item.x}
x2={item.x + item.w}
y1="56.5"
y2="56.5"
/>
</g>
<g class="ehv-a-pop ehv-i-8">
<rect class="ehv-fill-lime" x={@action_x} y="15" width={@action_w} height="26" rx="7" />
<text
class="ehv-t-strong ehv-t-on-lime"
x={@action_x + (@action_w - 14) / 2}
y="32"
font-size="11"
text-anchor="middle"
>
{@action}
</text>
<path
class="ehv-sections-headers__arrow ehv-round"
d={"M#{@action_x + @action_w - 18} 28 h6 m-2.6 -2.6 l2.6 2.6 l-2.6 2.6"}
/>
</g>
<line class="ehv-line-soft ehv-a-fade ehv-i-1" x1="0.5" x2="479.5" y1="56.5" y2="56.5" />
<g class="ehv-a-fade ehv-i-9">
<rect class="ehv-fill-soft" x="24" y="84" width="168" height="11" rx="5.5" />
<rect class="ehv-fill-soft" x="24" y="103" width="118" height="11" rx="5.5" />
<rect class="ehv-fill-soft" x="24" y="132" width="196" height="5" rx="2.5" />
<rect class="ehv-fill-soft" x="24" y="144" width="156" height="5" rx="2.5" />
<rect class="ehv-fill-soft" x="24" y="170" width="70" height="22" rx="7" />
<rect class="ehv-line-soft" x="102.5" y="170.5" width="69" height="21" rx="7" />
</g>
<g class="ehv-a-fade ehv-i-10">
<rect class="ehv-well" x="276.5" y="78.5" width="179" height="121" rx="10" />
<path class="ehv-line-faint" d="M277 199 L331 143 L367 175 L401 137 L455 190" />
<circle class="ehv-line-soft" cx="418" cy="106" r="9" />
</g>
</g>
</svg>
</.frame>
"""
end
# Lays the links out between the wordmark and the button: centred, with a gap
# that tightens when the names are long.
defp place(links, from, to) do
widths = Enum.map(links, &(String.length(&1) * 5.8))
total = Enum.sum(widths)
count = length(links)
gap = if count > 1, do: ((to - from - total) / (count - 1)) |> min(20) |> max(8), else: 0
start = from + max((to - from - total - gap * (count - 1)) / 2, 0)
{items, _x} =
links
|> Enum.zip(widths)
|> Enum.map_reduce(start, fn {text, w}, x ->
{%{text: text, x: Float.round(x, 1), w: Float.round(w, 1)}, x + w + gap}
end)
Enum.with_index(items)
end
end
assets/css/sections/headers.css
/* sections/headers */
.ehv-sections-headers { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }
.ehv-sections-headers__word { letter-spacing: -0.02em; }
.ehv-sections-headers__arrow { fill: none; stroke: var(--ehv-on-lime); stroke-width: 1.25; }
.ehv--hover .ehv-sections-headers__arrow { transition: transform 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-sections-headers__arrow { transform: translateX(2px); }