Bell
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>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initBtnBell(el) on the mounted element instead.
<button type="button" class="btn-bell" aria-pressed="false">
<span class="btn-bell__icon" aria-hidden="true">
<span class="btn-bell__ring"></span>
<span class="btn-bell__ring"></span>
<svg viewBox="0 0 24 24" fill="none">
<g class="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" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"></path>
</g>
<g class="btn-bell__clapper">
<path d="M10 20a2 2 0 0 0 4 0" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"></path>
</g>
</svg>
</span>
<span class="btn-bell__text">
<span class="btn-bell__off" aria-hidden="false">Notify me</span>
<span class="btn-bell__on" aria-hidden="true">You’re on the list</span>
</span>
</button>/* 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; }
}function initBtnBell(root) {
var off = root.querySelector('.btn-bell__off');
var on = root.querySelector('.btn-bell__on');
function onClick() {
var pressed = root.getAttribute('aria-pressed') !== 'true';
root.setAttribute('aria-pressed', String(pressed));
// Only the label that is showing is read out.
off.setAttribute('aria-hidden', String(pressed));
on.setAttribute('aria-hidden', String(!pressed));
}
root.addEventListener('click', onClick);
return function () { root.removeEventListener('click', onClick); };
}
document.querySelectorAll('.btn-bell').forEach(initBtnBell);Goal: Create a "Bell" button.
Visual behavior: 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.
Technique:
- It is a toggle with aria-pressed; switching on runs a damped swing keyframe on the bell body from its top, and a separate keyframe on the clapper that lags behind, so it reads as weight.
- Two rings scale out and fade with staggered delays, and the label cross-fades to the confirmation.
Accessibility: 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.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Bell" button.
Visual behavior: 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.
Technique:
- It is a toggle with aria-pressed; switching on runs a damped swing keyframe on the bell body from its top, and a separate keyframe on the clapper that lags behind, so it reads as weight.
- Two rings scale out and fade with staggered delays, and the label cross-fades to the confirmation.
Accessibility: 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.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
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.