notifications/toast.ex
Toast
A toast that has slid in at the corner of a faint page outline, with a title, one line and an action.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.notifications_toast animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
title |
string | "Invoice posted" |
Up to 24 characters. |
text |
string | "INV-2291 went to the ERP." |
One line, up to 30 characters. |
action |
any | "Undo" |
The button, or `nil` for none. |
corner |
"top-left" | "top-right" | "bottom-left" | "bottom-right" | "bottom-right" |
The corner of the page the toast sits in. It slides in from that side. |
tone |
"ok" | "risk" | "ok" |
`ok` is a lime tick, `risk` a coral warning. |
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/notifications/toast.ex
defmodule EHVisuals.Notifications.Toast do
@moduledoc """
A toast notification that has slid in at the corner of a page: a title, one
line and an action, over a faint outline of the page behind it.
"""
use EHVisuals.Composition,
name: "notifications/toast",
title: "Toast",
description:
"A toast that has slid in at the corner of a faint page outline, with a title, one line and an action."
@toast_w 236
@margin 14
attr(:title, :string, default: "Invoice posted", doc: "Up to 24 characters.")
attr(:text, :string,
default: "INV-2291 went to the ERP.",
doc: "One line, up to 30 characters."
)
attr(:action, :any, default: "Undo", doc: "The button, or `nil` for none.")
attr(:corner, :string,
default: "bottom-right",
values: ~w(top-left top-right bottom-left bottom-right),
doc: "The corner of the page the toast sits in. It slides in from that side."
)
attr(:tone, :string,
default: "ok",
values: ~w(ok risk),
doc: "`ok` is a lime tick, `risk` a coral warning."
)
attr(:label, :string,
default: "A toast in the bottom right corner of a page saying an invoice was posted"
)
frame_attrs()
@spec notifications_toast(map()) :: Phoenix.LiveView.Rendered.t()
def notifications_toast(assigns) do
[vertical, side] = String.split(assigns.corner, "-")
height = if assigns.action, do: 88, else: 64
assigns =
assigns
|> assign(:side, side)
|> assign(:x, if(side == "left", do: @margin, else: 400 - @margin - @toast_w))
|> assign(:y, if(vertical == "top", do: 34 + @margin, else: 260 - @margin - height))
|> assign(:toast_h, height)
|> assign(:action_w, 22 + String.length(assigns.action || "") * 6)
~H"""
<.frame name="notifications/toast" label={@label} {frame_opts(assigns)}>
<svg viewBox="0 0 400 260">
<g class="ehv-a-fade">
<rect
class="ehv-notifications-toast__page"
x="0.5"
y="0.5"
width="399"
height="259"
rx="12"
/>
<line class="ehv-line-faint" x1="1" x2="399" y1="34.5" y2="34.5" />
<circle :for={x <- [18, 31, 44]} class="ehv-line-soft" cx={x} cy="17.5" r="3.5" />
<rect class="ehv-fill-soft" x="140" y="11.5" width="120" height="12" rx="6" />
<rect class="ehv-fill-soft" x="24" y="54" width="112" height="10" rx="5" />
<rect class="ehv-fill-soft" x="24" y="72" width="196" height="6" rx="3" />
<rect
:for={x <- [24, 145, 266]}
class="ehv-line-faint"
x={x + 0.5}
y="96.5"
width="110"
height="60"
rx="8"
/>
<rect
:for={x <- [24, 145, 266]}
class="ehv-fill-soft"
x={x + 12}
y="110"
width="44"
height="6"
rx="3"
/>
<rect
:for={x <- [24, 145, 266]}
class="ehv-fill-soft"
x={x + 12}
y="126"
width="70"
height="12"
rx="4"
/>
<g :for={y <- [180, 202, 224]}>
<line class="ehv-line-faint" x1="24" x2="376" y1={y + 0.5} y2={y + 0.5} />
<rect class="ehv-fill-soft" x="24" y={y + 8} width="64" height="6" rx="3" />
<rect class="ehv-fill-soft" x="120" y={y + 8} width="110" height="6" rx="3" />
</g>
</g>
<g class={"ehv-notifications-toast__in ehv-notifications-toast__in--#{@side} ehv-i-4"}>
<g class="ehv-h-lift">
<rect
class={[
"ehv-notifications-toast__card",
@tone == "risk" && "ehv-notifications-toast__card--risk"
]}
x={@x + 0.5}
y={@y + 0.5}
width="235"
height={@toast_h - 1}
rx="11"
/>
<g :if={@tone == "ok"}>
<circle class="ehv-fill-lime-soft" cx={@x + 25} cy={@y + 27} r="11" />
<path
class="ehv-line-lime ehv-round ehv-a-draw ehv-i-8"
d={"M#{@x + 20} #{@y + 27.2} l3.4 3.4 l6.6 -7.4"}
pathLength="1"
/>
</g>
<g :if={@tone == "risk"}>
<circle class="ehv-fill-coral-soft" cx={@x + 25} cy={@y + 27} r="11" />
<path
class="ehv-line-coral ehv-round"
d={"M#{@x + 25} #{@y + 21.5} v6.5 M#{@x + 25} #{@y + 32} v0.4"}
/>
</g>
<text class="ehv-t-strong" x={@x + 46} y={@y + 25} font-size="12.5">{@title}</text>
<text class="ehv-t-2" x={@x + 46} y={@y + 42} font-size="11.5">{@text}</text>
<path
class="ehv-line ehv-round"
d={"M#{@x + 214} #{@y + 16} l7 7 M#{@x + 221} #{@y + 16} l-7 7"}
/>
<g :if={@action}>
<rect
class="ehv-notifications-toast__action"
x={@x + 46.5}
y={@y + 52.5}
width={@action_w - 1}
height="21"
rx="6"
/>
<text
class={["ehv-t-strong", if(@tone == "risk", do: "ehv-t-coral", else: "ehv-t-lime")]}
x={@x + 46 + @action_w / 2}
y={@y + 67}
font-size="10.5"
text-anchor="middle"
>
{@action}
</text>
</g>
<rect
class="ehv-fill-soft"
x={@x + 12}
y={@y + @toast_h - 6}
width="212"
height="2"
rx="1"
/>
<rect
class={[
"ehv-notifications-toast__timer",
if(@tone == "risk", do: "ehv-fill-coral", else: "ehv-fill-lime")
]}
x={@x + 12}
y={@y + @toast_h - 6}
width="127.2"
height="2"
rx="1"
/>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/notifications/toast.css
/* notifications/toast */
.ehv-notifications-toast { --ehv-max-w: 440px; }
/* The page behind is only an outline: it must not compete with the toast. */
.ehv-notifications-toast__page { fill: var(--ehv-surface-1); fill-opacity: 0.5; stroke: var(--ehv-border-soft); stroke-width: 1; }
.ehv-notifications-toast__card { fill: var(--ehv-surface-2); stroke: var(--ehv-border-strong); stroke-width: 1; }
.ehv-notifications-toast__card--risk { stroke: var(--ehv-coral); }
.ehv-notifications-toast__action { fill: var(--ehv-surface-3); stroke: var(--ehv-border-soft); stroke-width: 1; }
/* The toast slides in from the side of the page it sits on. */
@keyframes ehv-notifications-toast-in-right { from { opacity: 0; transform: translateX(32px); } }
@keyframes ehv-notifications-toast-in-left { from { opacity: 0; transform: translateX(-32px); } }
.ehv--animated .ehv-notifications-toast__in {
animation-duration: var(--ehv-dur);
animation-timing-function: var(--ehv-ease);
animation-fill-mode: backwards;
animation-delay: calc(var(--ehv-delay, 0ms) + var(--ehv-i, 0) * var(--ehv-step));
}
.ehv--animated .ehv-notifications-toast__in--right { animation-name: ehv-notifications-toast-in-right; }
.ehv--animated .ehv-notifications-toast__in--left { animation-name: ehv-notifications-toast-in-left; }
/* The time left before the toast leaves: the bar runs down from full to where it rests. */
@keyframes ehv-notifications-toast-timer { from { transform: scaleX(1.6667); } }
.ehv-notifications-toast__timer { transform-box: fill-box; transform-origin: left; }
.ehv--animated .ehv-notifications-toast__timer {
animation: ehv-notifications-toast-timer 2.8s linear backwards;
animation-delay: calc(var(--ehv-delay, 0ms) + 4 * var(--ehv-step) + var(--ehv-dur));
}
.ehv--hover .ehv-notifications-toast__action { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-notifications-toast__action { stroke: var(--ehv-ink-2); }