empowered.house ↗

notifications/bell.ex

Bell

A notification bell with an unread badge and the newest notification popping out beside it.

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

<div class="h-96 rounded-xl border border-border">
  <.notifications_bell animated />
</div>
default
isometric
many unread · custom copy
hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
count integer 3 Unread notifications. Zero hides the badge, more than nine shows 9+.
source string "Approvals" Where the notification comes from.
time string "Now"
title string "PO-2291 needs your approval" Up to 32 characters.
text string "Northwind · €18,400 · over the limit" Up to 38 characters.
action string "Review"
dismiss string "Later"
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/bell.ex
bell.ex
defmodule EHVisuals.Notifications.Bell do
  @moduledoc """
  A notification bell with its unread badge, and the newest notification
  popping out beside it.
  """

  use EHVisuals.Composition,
    name: "notifications/bell",
    title: "Bell",
    description:
      "A notification bell with an unread badge and the newest notification popping out beside it."

  attr(:count, :integer,
    default: 3,
    doc: "Unread notifications. Zero hides the badge, more than nine shows 9+."
  )

  attr(:source, :string, default: "Approvals", doc: "Where the notification comes from.")
  attr(:time, :string, default: "Now")
  attr(:title, :string, default: "PO-2291 needs your approval", doc: "Up to 32 characters.")

  attr(:text, :string,
    default: "Northwind · €18,400 · over the limit",
    doc: "Up to 38 characters."
  )

  attr(:action, :string, default: "Review")
  attr(:dismiss, :string, default: "Later")

  attr(:label, :string,
    default: "A bell with three unread notifications and the newest one shown beside it"
  )

  frame_attrs()

  @spec notifications_bell(map()) :: Phoenix.LiveView.Rendered.t()
  def notifications_bell(assigns) do
    count = max(assigns.count, 0)
    action_w = Float.round(24 + String.length(assigns.action) * 6.1, 1)

    assigns =
      assigns
      |> assign(:unread, count)
      |> assign(:badge, if(count > 9, do: "9+", else: Integer.to_string(count)))
      |> assign(:caption, if(count == 0, do: "All read", else: "#{count} unread"))
      |> assign(:action_w, action_w)
      |> assign(:dismiss_x, 142 + action_w + 8)
      |> assign(:dismiss_w, Float.round(24 + String.length(assigns.dismiss) * 6.1, 1))
      |> assign(:time_w, Float.round(String.length(assigns.time) * 5.8 + 2, 1))

    ~H"""
    <.frame name="notifications/bell" label={@label} {frame_opts(assigns)}>
      <svg viewBox="0 0 400 172">
        <g class="ehv-h-lift">
          <g class="ehv-a-pop">
            <rect class="ehv-card-3" x="12.5" y="38.5" width="91" height="91" rx="22" />
            <g class="ehv-notifications-bell__ring">
              <path
                class="ehv-notifications-bell__icon ehv-round"
                d="M58 63 v-4 M58 63 c-9.5 0 -15 6.8 -15 16 v8.5 l-5.5 8.5 h41 l-5.5 -8.5 v-8.5 c0 -9.2 -5.5 -16 -15 -16 z"
              />
              <path
                class="ehv-notifications-bell__icon ehv-round"
                d="M52.5 101.5 a5.5 5.5 0 0 0 11 0"
              />
            </g>
          </g>
          <g :if={@unread > 0}>
            <circle class="ehv-fill-lime ehv-a-ping ehv-i-4" cx="94" cy="47" r="11" />
            <g class="ehv-a-pop ehv-i-3">
              <circle class="ehv-notifications-bell__badge" cx="94" cy="47" r="12" />
              <text
                class="ehv-mono ehv-t-on-lime ehv-t-strong"
                x="94"
                y="50.5"
                font-size="10"
                text-anchor="middle"
              >
                {@badge}
              </text>
            </g>
          </g>
          <text class="ehv-mono ehv-a-fade ehv-i-2" x="58" y="150" font-size="8" text-anchor="middle">
            {String.upcase(@caption)}
          </text>
          <g class="ehv-a-fade ehv-i-3">
            <line class="ehv-line ehv-dash" x1="104" x2="128" y1="84.5" y2="84.5" />
            <rect class="ehv-card" x="146.5" y="46.5" width="223" height="116" rx="12" />
            <rect class="ehv-card" x="137.5" y="38.5" width="241" height="116" rx="12" />
          </g>
          <g class="ehv-a-slide ehv-i-4">
            <rect
              class="ehv-card ehv-notifications-bell__card"
              x="128.5"
              y="22.5"
              width="259"
              height="124"
              rx="12"
            />
            <rect class="ehv-card-3" x="142.5" y="36.5" width="20" height="20" rx="6" />
            <rect class="ehv-fill-ink" x="149" y="43" width="7" height="7" rx="2" />
            <text class="ehv-mono ehv-t-2" x="171" y="49.5" font-size="8.5">
              {String.upcase(@source)}
            </text>
            <circle class="ehv-fill-lime ehv-a-pulse" cx={368 - @time_w} cy="46.5" r="2.5" />
            <text class="ehv-mono ehv-t-lime" x="374" y="49.5" font-size="8.5" text-anchor="end">
              {String.upcase(@time)}
            </text>
            <text class="ehv-t-strong" x="142" y="80" font-size="13.5">{@title}</text>
            <text class="ehv-t-2" x="142" y="98" font-size="11.5">{@text}</text>
            <rect class="ehv-fill-lime" x="142" y="110" width={@action_w} height="24" rx="6" />
            <text
              class="ehv-t-strong ehv-t-on-lime"
              x={142 + @action_w / 2}
              y="126"
              font-size="10.5"
              text-anchor="middle"
            >
              {@action}
            </text>
            <rect
              class="ehv-card-2"
              x={@dismiss_x + 0.5}
              y="110.5"
              width={@dismiss_w - 1}
              height="23"
              rx="6"
            />
            <text
              class="ehv-t-2"
              x={@dismiss_x + @dismiss_w / 2}
              y="126"
              font-size="10.5"
              text-anchor="middle"
            >
              {@dismiss}
            </text>
          </g>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/notifications/bell.css
bell.css
/* notifications/bell */
.ehv-notifications-bell { --ehv-max-w: 440px; --ehv-step: 110ms; }

.ehv-notifications-bell__icon { fill: none; stroke: var(--ehv-ink); stroke-width: 1.5; }

/* The badge sits on the corner of the tile, ringed in the canvas colour. */
.ehv-notifications-bell__badge { fill: var(--ehv-lime-fill); stroke: var(--ehv-canvas); stroke-width: 3; }

/* The bell swings from its top, then rests. */
@keyframes ehv-notifications-bell-ring {
  0%, 24%, 100% { transform: rotate(0deg); }
  4% { transform: rotate(-11deg); }
  9% { transform: rotate(9deg); }
  14% { transform: rotate(-6deg); }
  19% { transform: rotate(3deg); }
}

.ehv-notifications-bell__ring { transform-box: fill-box; transform-origin: 50% 0%; }
.ehv--animated .ehv-notifications-bell__ring {
  animation: ehv-notifications-bell-ring 3.6s ease-in-out infinite;
  animation-delay: var(--ehv-dur);
}

.ehv--hover .ehv-notifications-bell__card { transition: stroke 200ms var(--ehv-ease); }
.ehv--hover:hover .ehv-notifications-bell__card { stroke: var(--ehv-lime); }

@media (prefers-reduced-motion: reduce) {
  .ehv--animated .ehv-notifications-bell__ring { animation: none; }
}