avatars/stack.ex
Avatar stack
An overlapping stack of avatars with a badge for the rest and a short caption, for who is involved.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.avatars_stack animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
eyebrow |
string | "Approvers" |
|
people |
list | ["Marta Nowak", "Jan Kowalski", "Ewa Lis", "Piotr Zielinski", "Olga Sowa"] |
Up to five names, shown as initials. |
extra |
integer | 12 |
How many more people the badge stands for. 0 hides the badge. |
caption |
string | "Marta, Jan and 15 others" |
The line under the stack. Up to 40 characters. |
meta |
any | "Sign off the month-end close" |
The mono line under the caption. `nil` hides it. |
highlight |
any | 0 |
Index of the avatar filled with lime, from 0. `nil` leaves all of them neutral. |
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/avatars/stack.ex
defmodule EHVisuals.Avatars.Stack do
@moduledoc """
An overlapping stack of avatars with a badge for the rest and a caption.
"""
use EHVisuals.Composition,
name: "avatars/stack",
title: "Avatar stack",
description:
"An overlapping stack of avatars with a badge for the rest and a short caption, for who is involved."
attr(:eyebrow, :string, default: "Approvers")
attr(:people, :list,
default: ["Marta Nowak", "Jan Kowalski", "Ewa Lis", "Piotr Zielinski", "Olga Sowa"],
doc: "Up to five names, shown as initials."
)
attr(:extra, :integer,
default: 12,
doc: "How many more people the badge stands for. 0 hides the badge."
)
attr(:caption, :string,
default: "Marta, Jan and 15 others",
doc: "The line under the stack. Up to 40 characters."
)
attr(:meta, :any,
default: "Sign off the month-end close",
doc: "The mono line under the caption. `nil` hides it."
)
attr(:highlight, :any,
default: 0,
doc: "Index of the avatar filled with lime, from 0. `nil` leaves all of them neutral."
)
attr(:label, :string, default: "Five overlapping avatars and a badge for twelve more people")
frame_attrs()
@spec avatars_stack(map()) :: Phoenix.LiveView.Rendered.t()
def avatars_stack(assigns) do
avatars =
for {name, index} <- assigns.people |> Enum.take(5) |> Enum.with_index() do
%{
initials: initials(name),
index: index,
cx: 44 + index * 38,
on: index == assigns.highlight
}
end
assigns =
assigns
|> assign(:avatars, avatars)
|> assign(:badge_x, 44 + length(avatars) * 38)
|> assign(:badge_slot, length(avatars))
|> assign(:height, if(assigns.meta, do: 154, else: 140))
~H"""
<.frame name="avatars/stack" label={@label} {frame_opts(assigns)}>
<svg viewBox={"0 0 320 #{@height}"}>
<g class="ehv-h-lift">
<rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="319" height={@height - 1} rx="12" />
<text class="ehv-mono ehv-a-fade ehv-i-1" x="20" y="31" font-size="9">
{String.upcase(@eyebrow)}
</text>
<g :for={avatar <- @avatars} class={"ehv-a-slide ehv-i-#{avatar.index + 2}"}>
<g class={"ehv-avatars-stack__slot ehv-avatars-stack__slot--#{avatar.index}"}>
<circle class="ehv-avatars-stack__gap" cx={avatar.cx} cy="76" r="27" />
<circle
class={if avatar.on, do: "ehv-fill-lime", else: "ehv-card-3"}
cx={avatar.cx}
cy="76"
r="24"
/>
<text
class={["ehv-mono", if(avatar.on, do: "ehv-t-on-lime", else: "ehv-t-2")]}
x={avatar.cx}
y="79.5"
font-size="10"
text-anchor="middle"
>
{avatar.initials}
</text>
</g>
</g>
<g :if={@extra > 0} class={"ehv-a-slide ehv-i-#{@badge_slot + 2}"}>
<g class={"ehv-avatars-stack__slot ehv-avatars-stack__slot--#{@badge_slot}"}>
<circle class="ehv-avatars-stack__gap" cx={@badge_x} cy="76" r="27" />
<circle class="ehv-well" cx={@badge_x} cy="76" r="24" />
<text
class="ehv-mono ehv-t-ink"
x={@badge_x}
y="79.5"
font-size="10"
text-anchor="middle"
>
+{@extra}
</text>
</g>
</g>
<g class="ehv-a-fade ehv-i-8">
<text x="20" y="124" font-size="12.5">{@caption}</text>
<text :if={@meta} class="ehv-mono" x="20" y="139" font-size="8.5">
{String.upcase(@meta)}
</text>
</g>
</g>
</svg>
</.frame>
"""
end
defp initials(name) do
name |> String.split() |> Enum.take(2) |> Enum.map_join(&String.first/1) |> String.upcase()
end
end
assets/css/avatars/stack.css
/* avatars/stack */
.ehv-avatars-stack { --ehv-max-w: 350px; }
/* The ring in the card's own colour that separates one avatar from the next. */
.ehv-avatars-stack__gap { fill: var(--ehv-surface-1); }
/* Hover fans the stack out: each avatar moves a little further than the last. */
.ehv--hover .ehv-avatars-stack__slot { transition: transform 260ms var(--ehv-ease); }
.ehv--hover:hover .ehv-avatars-stack__slot--0 { transform: translateX(0); }
.ehv--hover:hover .ehv-avatars-stack__slot--1 { transform: translateX(3px); }
.ehv--hover:hover .ehv-avatars-stack__slot--2 { transform: translateX(6px); }
.ehv--hover:hover .ehv-avatars-stack__slot--3 { transform: translateX(9px); }
.ehv--hover:hover .ehv-avatars-stack__slot--4 { transform: translateX(12px); }
.ehv--hover:hover .ehv-avatars-stack__slot--5 { transform: translateX(15px); }