Bell

No dependencies
Code format
React + Tailwind
import { useState } from 'react';

const css = `
  /* The bell swings and settles; the clapper follows a beat behind, so it reads as weight. */
  @keyframes btn-bell-swing { 0% { transform: rotate(0); } 15% { transform: rotate(18deg); } 32% { transform: rotate(-14deg); } 48% { transform: rotate(9deg); } 64% { transform: rotate(-5deg); } 80% { transform: rotate(2deg); } 100% { transform: rotate(0); } }
  @keyframes btn-bell-clapper { 0% { transform: translateX(0); } 20% { transform: translateX(-2.6px); } 38% { transform: translateX(2.4px); } 54% { transform: translateX(-1.6px); } 70% { transform: translateX(1px); } 100% { transform: translateX(0); } }
  @keyframes btn-bell-ring { from { transform: scale(0.6); opacity: 0.8; } to { transform: scale(1.7); opacity: 0; } }
  .btn-bell {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    height: 48px;
    padding: 0 20px 0 16px;
    border: none;
    border-radius: 999px;
    background: light-dark(#ebedf0, #232529);
    box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035), 0 8px 20px -12px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.6));
    color: light-dark(#373a40, #f1f2f3);
    font: inherit;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: background-color 0.3s;
  }
  .btn-bell:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
  .btn-bell[aria-pressed='true'] { background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); }
  .btn-bell__icon { position: relative; width: 22px; height: 22px; }
  .btn-bell__icon svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
  .btn-bell__body { transform-origin: 12px 3px; }
  .btn-bell__clapper { transform-origin: 12px 3px; }
  .btn-bell[aria-pressed='true'] .btn-bell__body { animation: btn-bell-swing 1s ease-out; }
  .btn-bell[aria-pressed='true'] .btn-bell__clapper { animation: btn-bell-clapper 1s ease-out 0.06s; }
  .btn-bell__ring { position: absolute; inset: -4px; border-radius: 50%; border: 1.5px solid currentColor; opacity: 0; }
  .btn-bell[aria-pressed='true'] .btn-bell__ring { animation: btn-bell-ring 0.8s ease-out 0.12s; }
  .btn-bell[aria-pressed='true'] .btn-bell__ring + .btn-bell__ring { animation-delay: 0.42s; }
  .btn-bell__text { display: grid; }
  .btn-bell__text > span { grid-area: 1 / 1; transition: opacity 0.25s, transform 0.35s cubic-bezier(0.3, 1.3, 0.5, 1); }
  .btn-bell__on { opacity: 0; transform: translateY(8px); }
  .btn-bell[aria-pressed='true'] .btn-bell__off { opacity: 0; transform: translateY(-8px); }
  .btn-bell[aria-pressed='true'] .btn-bell__on { opacity: 1; transform: none; }
  @media (prefers-reduced-motion: reduce) {
    .btn-bell * { animation: none !important; }
  }
`;

export function BellButton() {
  const [on, setOn] = useState(false);
  return (
    <>
      <style>
        {css}
      </style>
      <button type="button" className="btn-bell" aria-pressed={on} onClick={() => setOn((o) => !o)}>
        <span className="btn-bell__icon" aria-hidden="true">
          <span className="btn-bell__ring" />
          <span className="btn-bell__ring" />
          <svg viewBox="0 0 24 24" fill="none">
            <g className="btn-bell__body">
              <path d="M12 3v1.5M6.5 16.5V11a5.5 5.5 0 0 1 11 0v5.5l1.5 1.5H5Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round" />
            </g>
            <g className="btn-bell__clapper">
              <path d="M10 20a2 2 0 0 0 4 0" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
            </g>
          </svg>
        </span>
        <span className="btn-bell__text">
          <span className="btn-bell__off" aria-hidden={on}>Notify me</span>
          <span className="btn-bell__on" aria-hidden={!on}>You’re on the list</span>
        </span>
      </button>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/button-bell
Source on GitHub

About this component

A notify me button: pressed, the bell swings and settles with its clapper a beat behind, sound rings ripple out from it and the label confirms you are on the list.

Dependencies
None
Difficulty
Beginner
Triggers
click, state change, spring
Techniques
keyframes, svg, transform only
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A real <button> with a visible label; decoration is aria-hidden. It is a toggle button (aria-pressed), and only the label that is showing is exposed to screen readers. Under prefers-reduced-motion the bell does not swing.

More buttons

See all
Breathe
Brushstroke
Dispatch
Dissolve