sections/cta.ex
Call to action
A call-to-action section with an eyebrow, a heading, one line of copy and two buttons.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.sections_cta animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "Get started" |
|
heading |
string | "Run your operations on one platform" |
Wraps onto a second line after about 40 characters. |
copy |
string | "Orders, approvals and month-end close in one pla" <> ... |
One line, up to about 56 characters. |
primary |
string | "Book a walkthrough" |
The lime button. |
secondary |
any | "See how it works" |
The outlined button. Pass `nil` to leave it out. |
align |
"center" | "left" | "center" |
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/cta.ex
defmodule EHVisuals.Sections.Cta do
@moduledoc """
A call-to-action section: an eyebrow, a heading, one line of copy and two buttons.
"""
use EHVisuals.Composition,
name: "sections/cta",
title: "Call to action",
description:
"A call-to-action section with an eyebrow, a heading, one line of copy and two buttons."
attr(:eyebrow, :string, default: "Get started")
attr(:heading, :string,
default: "Run your operations on one platform",
doc: "Wraps onto a second line after about 40 characters."
)
attr(:copy, :string,
default: "Orders, approvals and month-end close in one place.",
doc: "One line, up to about 56 characters."
)
attr(:primary, :string, default: "Book a walkthrough", doc: "The lime button.")
attr(:secondary, :any,
default: "See how it works",
doc: "The outlined button. Pass `nil` to leave it out."
)
attr(:align, :string, default: "center", values: ~w(center left))
attr(:label, :string,
default: "A call-to-action section with a heading, one line of copy and two buttons"
)
frame_attrs()
@spec sections_cta(map()) :: Phoenix.LiveView.Rendered.t()
def sections_cta(assigns) do
lines = assigns.heading |> wrap(40) |> Enum.take(2)
copy_y = 82 + (length(lines) - 1) * 20 + 22
buttons_y = copy_y + 20
height = buttons_y + 26 + 48
primary_w = Float.round(42 + String.length(assigns.primary) * 5.5, 1)
secondary_w =
if assigns.secondary,
do: Float.round(28 + String.length(assigns.secondary) * 5.6, 1),
else: 0
total = primary_w + if(assigns.secondary, do: 8 + secondary_w, else: 0)
centered = assigns.align == "center"
start = if centered, do: Float.round(200 - total / 2, 1), else: 24
assigns =
assigns
|> assign(:lines, Enum.with_index(lines))
|> assign(:copy_y, copy_y)
|> assign(:buttons_y, buttons_y)
|> assign(:height, height)
|> assign(:text_x, if(centered, do: 200, else: 24))
|> assign(:anchor, if(centered, do: "middle", else: "start"))
|> assign(:primary_x, start)
|> assign(:primary_w, primary_w)
|> assign(:secondary_x, start + primary_w + 8)
|> assign(:secondary_w, secondary_w)
|> assign(:arcs, arcs(centered, height))
~H"""
<.frame name="sections/cta" 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" />
<path
:for={{d, index} <- @arcs}
class={"ehv-line-soft ehv-a-draw ehv-i-#{index + 1}"}
d={d}
pathLength="1"
/>
<text
class="ehv-mono ehv-a-fade ehv-i-1"
x={@text_x}
y="56"
font-size="8.5"
text-anchor={@anchor}
>
{String.upcase(@eyebrow)}
</text>
<text
:for={{line, index} <- @lines}
class="ehv-t-strong ehv-a-fade ehv-i-2"
x={@text_x}
y={82 + index * 20}
font-size="16"
text-anchor={@anchor}
>
{line}
</text>
<text
class="ehv-t-2 ehv-a-fade ehv-i-3"
x={@text_x}
y={@copy_y}
font-size="11.5"
text-anchor={@anchor}
>
{@copy}
</text>
<g class="ehv-a-rise ehv-i-4">
<rect
class="ehv-fill-lime"
x={@primary_x}
y={@buttons_y}
width={@primary_w}
height="26"
rx="6"
/>
<text
class="ehv-t-strong ehv-t-on-lime"
x={@primary_x + 14}
y={@buttons_y + 17}
font-size="10.5"
>
{@primary}
</text>
<path
class="ehv-sections-cta__arrow ehv-round"
d={"M#{@primary_x + @primary_w - 22} #{@buttons_y + 13} h8 m-3 -3 l3 3 l-3 3"}
/>
</g>
<g :if={@secondary} class="ehv-a-rise ehv-i-5">
<rect
class="ehv-line"
x={@secondary_x + 0.5}
y={@buttons_y + 0.5}
width={@secondary_w - 1}
height="25"
rx="6"
/>
<text
class="ehv-t-strong"
x={@secondary_x + @secondary_w / 2}
y={@buttons_y + 17}
font-size="10.5"
text-anchor="middle"
>
{@secondary}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
# Quarter circles in two corners of the card, as {path, order}. The centred
# layout keeps them away from the copy; the left one moves both to the right.
defp arcs(centered, height) do
radii = [28, 52, 76]
corners =
if centered,
do: [{0.5, height - 0.5, 1, -1}, {399.5, 0.5, -1, 1}],
else: [{399.5, height - 0.5, -1, -1}, {399.5, 0.5, -1, 1}]
paths =
for {cx, cy, dx, dy} <- corners, r <- radii do
sweep = if dx * dy < 0, do: 0, else: 1
"M#{cx + dx * r} #{cy} A#{r} #{r} 0 0 #{sweep} #{cx} #{cy + dy * r}"
end
Enum.with_index(paths)
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/cta.css
/* sections/cta */
.ehv-sections-cta { --ehv-max-w: 440px; }
.ehv-sections-cta__arrow {
fill: none;
stroke: var(--ehv-on-lime);
stroke-width: 1.25;
transform-box: fill-box;
}
.ehv--hover .ehv-sections-cta__arrow { transition: transform 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-sections-cta__arrow { transform: translateX(3px); }