sections/hero.ex
Hero section
A page hero with an eyebrow, a heading with one lime line, copy, two buttons and a small abstract picture.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_hero animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "Agents for operations" |
Up to 32 characters. |
heading |
list | ["Orders in.", "Invoices out."] |
One to three lines of up to 18 characters each. |
accent |
integer | 1 |
Index of the heading line set in lime. A negative index sets none. |
primary |
string | "Book a demo" |
The lime button. |
secondary |
any | "See how it works" |
The outlined button. `nil` hides it. |
picture |
"beside" | "below" | "beside" |
Where the picture sits: `beside` the text or `below` it, with the text centred. |
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/hero.ex
defmodule EHVisuals.Sections.Hero do
@moduledoc """
A page hero section: an eyebrow, a heading with one line in lime, a line of
copy, two buttons and a small abstract picture beside or under the text.
"""
use EHVisuals.Composition,
name: "sections/hero",
title: "Hero section",
description:
"A page hero with an eyebrow, a heading with one lime line, copy, two buttons and a small abstract picture."
attr(:eyebrow, :string, default: "Agents for operations", doc: "Up to 32 characters.")
attr(:heading, :list,
default: ["Orders in.", "Invoices out."],
doc: "One to three lines of up to 18 characters each."
)
attr(:accent, :integer,
default: 1,
doc: "Index of the heading line set in lime. A negative index sets none."
)
attr(:primary, :string, default: "Book a demo", doc: "The lime button.")
attr(:secondary, :any,
default: "See how it works",
doc: "The outlined button. `nil` hides it."
)
attr(:picture, :string,
default: "beside",
values: ~w(beside below),
doc: "Where the picture sits: `beside` the text or `below` it, with the text centred."
)
attr(:label, :string,
default: "A page hero with a two-line heading, two buttons and a small picture"
)
frame_attrs()
@spec sections_hero(map()) :: Phoenix.LiveView.Rendered.t()
def sections_hero(assigns) do
lines = assigns.heading |> Enum.take(3) |> Enum.with_index()
below = assigns.picture == "below"
base = 86 + (max(length(lines), 1) - 1) * 31
eyebrow_w = Float.round(22 + String.length(assigns.eyebrow) * 5.5, 1)
primary_w = Float.round(38 + String.length(assigns.primary) * 5.9, 1)
secondary_w =
if assigns.secondary,
do: Float.round(26 + String.length(assigns.secondary) * 5.6, 1),
else: 0
buttons_w = primary_w + if(assigns.secondary, do: 8 + secondary_w, else: 0)
# Text is left-aligned beside the picture and centred above it.
left = fn width -> if below, do: Float.round(240 - width / 2, 1), else: 28 end
height = if below, do: base + 84 + 16 + 100 + 26, else: max(base + 84 + 28, 226)
scene = if below, do: scene_below(base + 100), else: scene_beside(height)
assigns =
assigns
|> assign(:lines, lines)
|> assign(:base, base)
|> assign(:anchor, if(below, do: "middle", else: "start"))
|> assign(:text_x, if(below, do: 240, else: 28))
|> assign(:eyebrow_x, left.(eyebrow_w))
|> assign(:eyebrow_w, eyebrow_w)
|> assign(:copy, [{left.(216), 216, base + 21}, {left.(168), 168, base + 33}])
|> assign(:primary_x, left.(buttons_w))
|> assign(:primary_w, primary_w)
|> assign(:secondary_x, left.(buttons_w) + primary_w + 8)
|> assign(:secondary_w, secondary_w)
|> assign(:scene, scene)
|> assign(:height, height)
~H"""
<.frame name="sections/hero" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 480 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="479" height={@height - 1} rx="12" />
<g class="ehv-a-fade ehv-i-1">
<rect
class="ehv-line-soft"
x={@eyebrow_x + 0.5}
y="28.5"
width={@eyebrow_w}
height="19"
rx="6"
/>
<text
class="ehv-mono ehv-t-2"
x={@eyebrow_x + 0.5 + @eyebrow_w / 2}
y="41"
font-size="8"
text-anchor="middle"
>
{String.upcase(@eyebrow)}
</text>
</g>
<text
:for={{line, index} <- @lines}
class={[
"ehv-t-display ehv-a-rise",
"ehv-i-#{index + 2}",
index == @accent && "ehv-t-lime"
]}
x={@text_x}
y={86 + index * 31}
font-size="27"
text-anchor={@anchor}
>
{line}
</text>
<g class="ehv-a-fade ehv-i-5">
<rect
:for={{x, width, y} <- @copy}
class="ehv-fill-soft"
x={x}
y={y}
width={width}
height="5"
rx="2.5"
/>
</g>
<g class="ehv-a-rise ehv-i-6">
<rect
class="ehv-fill-lime"
x={@primary_x}
y={@base + 58}
width={@primary_w}
height="26"
rx="7"
/>
<text
class="ehv-t-strong ehv-t-on-lime"
x={@primary_x + (@primary_w - 14) / 2}
y={@base + 75}
font-size="11"
text-anchor="middle"
>
{@primary}
</text>
<path
class="ehv-sections-hero__arrow ehv-round"
d={"M#{@primary_x + @primary_w - 18} #{@base + 71} h6 m-2.6 -2.6 l2.6 2.6 l-2.6 2.6"}
/>
<g :if={@secondary}>
<rect
class="ehv-line"
x={@secondary_x + 0.5}
y={@base + 58.5}
width={@secondary_w - 1}
height="25"
rx="7"
/>
<text
x={@secondary_x + @secondary_w / 2}
y={@base + 75}
font-size="11"
text-anchor="middle"
>
{@secondary}
</text>
</g>
</g>
<g class="ehv-a-fade ehv-i-6">
<rect
class="ehv-well"
x={@scene.x + 0.5}
y={@scene.y + 0.5}
width={@scene.w - 1}
height={@scene.h - 1}
rx="10"
/>
<path :for={d <- @scene.guides} class="ehv-line-faint" d={d} />
</g>
<path
:for={{d, index} <- @scene.wires}
class={"ehv-line ehv-round ehv-a-draw ehv-i-#{index + 9}"}
d={d}
pathLength="1"
/>
<g :for={{tile, index} <- @scene.tiles} class={"ehv-a-rise ehv-i-#{index + 7}"}>
<rect
class={if tile.on, do: "ehv-card-lime", else: "ehv-card-2"}
x={tile.x + 0.5}
y={tile.y + 0.5}
width={tile.w - 1}
height={tile.h - 1}
rx="7"
/>
<rect
class={if tile.on, do: "ehv-fill-lime ehv-a-pulse", else: "ehv-fill-4"}
x={tile.x + 9}
y={tile.y + 9}
width="7"
height="7"
rx="2"
/>
<rect
class="ehv-fill-strong"
x={tile.x + 9}
y={tile.y + tile.h - 13}
width={tile.w - 30}
height="4"
rx="2"
/>
<rect
class="ehv-fill-soft"
x={tile.x + 22}
y={tile.y + 10.5}
width={tile.w - 36}
height="4"
rx="2"
/>
</g>
</g>
</svg>
</.frame>
"""
end
# Three steps going down a staircase, in a tall well on the right.
defp scene_beside(height) do
{x, y, w, h} = {292, 28, 160, height - 56}
step = (h - 34 - 32) / 2
tiles =
for index <- 0..2 do
%{
x: x + 14 + index * 36,
y: Float.round(y + 16 + index * step, 1),
w: 60,
h: 34,
on: index == 2
}
end
wires =
for [from, to] <- Enum.chunk_every(tiles, 2, 1, :discard) do
"M#{from.x + 16} #{from.y + from.h} V#{to.y + to.h / 2} H#{to.x}"
end
guides = [
"M#{x + 1} #{y + h - 30} H#{x + w - 1}",
"M#{x + w - 34} #{y + 1} V#{y + h - 1}"
]
%{
x: x,
y: y,
w: w,
h: h,
tiles: Enum.with_index(tiles),
wires: Enum.with_index(wires),
guides: guides
}
end
# The same three steps in a row, in a wide well under the text.
defp scene_below(y) do
{x, w, h} = {28, 424, 100}
tiles =
for index <- 0..2 do
%{x: x + 42 + index * 124, y: y + 30, w: 92, h: 40, on: index == 2}
end
wires =
for [from, to] <- Enum.chunk_every(tiles, 2, 1, :discard) do
"M#{from.x + from.w} #{from.y + from.h / 2} H#{to.x}"
end
guides = ["M#{x + 1} #{y + 15} H#{x + w - 1}", "M#{x + 1} #{y + h - 15} H#{x + w - 1}"]
%{
x: x,
y: y,
w: w,
h: h,
tiles: Enum.with_index(tiles),
wires: Enum.with_index(wires),
guides: guides
}
end
end
assets/css/sections/hero.css
/* sections/hero */
.ehv-sections-hero { --ehv-max-w: 520px; --ehv-iso-scale: 0.82; }
.ehv-sections-hero__arrow { fill: none; stroke: var(--ehv-on-lime); stroke-width: 1.25; }
.ehv--hover .ehv-sections-hero__arrow { transition: transform 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-sections-hero__arrow { transform: translateX(2px); }