empowered.house ↗

sections/headers.ex

Header

A page header with the EmpoweredHouse lockup, navigation links and one lime action, above a ghosted page.

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

<div class="h-96 rounded-xl border border-border">
  <.sections_headers animated />
</div>
default
isometric
five links · third one active
hover · no glow
light surface
isometric · fade out · still

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