empowered.house ↗

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.

Use it
# in the module that renders the template
use EHVisuals

<div class="h-96 rounded-xl border border-border">
  <.sections_hero animated />
</div>
default
isometric
picture below · custom copy
hover · no glow · one button
light surface
isometric · fade out · still

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
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
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); }