30 CSS Notifications30 / 30

Pure CSSMIT licensed

CSS Floating Feedback Button With Badge

Two endgames for the floating button: a circle-to-pill FAB whose label expands through an animatable grid column (the transition-to-auto-width trick that works today, with interpolate-size noted as the 2027 one-liner), badge riding the morph — and a speed-dial FAB that blooms three labeled actions on a checkbox toggle, each springing out with its own delay, the unread badge clearing the moment the dial opens.

Published

Live Demo
Try it

The code

<div class="ntf-30-group">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-30a" aria-label="Floating feedback button expanding to a pill">
  <div class="ntf-30a__page">
    <h2 class="ntf-30a__ghost">Docs page</h2>
    <p class="ntf-30a__ghostsub">hover (or tab to) the button in the corner</p>
  </div>
  <button class="ntf-30a__fab" type="button" aria-label="Feedback, 2 unread replies">
    <span class="ntf-30a__glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.4 8.6 8.6 0 0 1-3.9-.9L3 20l1-4.1a8.4 8.4 0 1 1 17-4.4z"/></svg></span>
    <span class="ntf-30a__label" aria-hidden="true"><span>Give feedback</span></span>
    <b class="ntf-30a__badge" aria-hidden="true">2</b>
  </button>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-30b" aria-label="Speed dial floating button with actions">
  <div class="ntf-30b__page">
    <h2 class="ntf-30b__ghost">Support corner</h2>
    <p class="ntf-30b__ghostsub">open the dial — the badge clears, three labeled actions bloom</p>
  </div>
  <div class="ntf-30b__dial">
    <a class="ntf-30b__act" href="#" onclick="return false" style="--i:3"><span class="ntf-30b__pill">Report a bug</span><i class="ntf-30b__mini" aria-hidden="true">&#9888;</i></a>
    <a class="ntf-30b__act" href="#" onclick="return false" style="--i:2"><span class="ntf-30b__pill">Suggest a feature</span><i class="ntf-30b__mini" aria-hidden="true">&#10022;</i></a>
    <a class="ntf-30b__act" href="#" onclick="return false" style="--i:1"><span class="ntf-30b__pill">Message support</span><i class="ntf-30b__mini" aria-hidden="true">&#9993;</i></a>
    <label class="ntf-30b__fab" aria-label="Open support menu, 3 unread">
      <input class="ntf-30b__state" type="checkbox">
      <i class="ntf-30b__plus" aria-hidden="true"></i>
      <b class="ntf-30b__badge" aria-hidden="true">3</b>
    </label>
  </div>
</section>
</div>
.ntf-30-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

.ntf-30-group > section {
  flex: 1 1 480px;
  min-width: min(100%,360px);
}

.ntf-30a,
.ntf-30a *,
.ntf-30a *::before,
.ntf-30a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ntf-30a {
  position: relative;
  overflow: hidden;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: linear-gradient(180deg,#f6f4ef,#ebe7dc);
  font-family: 'Outfit','Segoe UI',system-ui,sans-serif;
}

.ntf-30a__page {
  text-align: center;
  padding: 24px;
}

.ntf-30a__ghost {
  font-size: clamp(26px,4vw,36px);
  font-weight: 600;
  letter-spacing: -.02em;
  color: rgba(35,31,24,.22);
}

.ntf-30a__ghostsub {
  margin-top: 8px;
  font-size: 13px;
  color: rgba(35,31,24,.35);
}

.ntf-30a__fab {
  position: absolute;
  bottom: calc(22px + env(safe-area-inset-bottom,0px));
  inset-inline-end: 22px;
  display: flex;
  align-items: center;
  height: 56px;
  padding: 0 16px;
  border: 0;
  border-radius: 999px;
  color: #f6f4ef;
  background: #232019;
  cursor: pointer;
  box-shadow: 0 3px 8px rgba(35,31,24,.3),0 22px 44px -16px rgba(35,31,24,.5);
  transition: scale .15s,background .2s;
}

.ntf-30a__fab:hover {
  background: #38332a;
}

.ntf-30a__fab:active {
  scale: .96;
}

.ntf-30a__fab:focus-visible {
  outline: 2.5px solid #232019;
  outline-offset: 3px;
}

.ntf-30a__glyph {
  flex: none;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
}

.ntf-30a__glyph svg {
  width: 22px;
  height: 22px;
}

.ntf-30a__label {
  display: grid;
  grid-template-columns: 0fr;
  transition: grid-template-columns .35s cubic-bezier(.22,1.2,.36,1);
}

.ntf-30a__label span {
  overflow: hidden;
  white-space: nowrap;
  font-size: 14px;
  font-weight: 700;
  padding-inline-start: 0;
  transition: padding .35s cubic-bezier(.22,1.2,.36,1);
}

.ntf-30a__fab:hover .ntf-30a__label,
.ntf-30a__fab:focus-visible .ntf-30a__label {
  grid-template-columns: 1fr;
}

.ntf-30a__fab:hover .ntf-30a__label span,
.ntf-30a__fab:focus-visible .ntf-30a__label span {
  padding-inline-start: 10px;
}

.ntf-30a__badge {
  position: absolute;
  top: -4px;
  inset-inline-end: -2px;
  min-inline-size: 20px;
  block-size: 20px;
  display: grid;
  place-items: center;
  padding-inline: 5px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: #fff;
  background: oklch(0.63 0.21 25);
  border: 2.5px solid #f6f4ef;
  animation: ntf-30a-pop .5s cubic-bezier(.22,1.6,.36,1) both .4s;
}

@keyframes ntf-30a-pop {
  from {
    scale: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ntf-30a__label,
    .ntf-30a__label span {
    transition-duration: .01s;
  }

  .ntf-30a__badge {
    animation: none;
  }
}

.ntf-30b,
.ntf-30b *,
.ntf-30b *::before,
.ntf-30b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ntf-30b {
  position: relative;
  overflow: hidden;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: radial-gradient(120% 100% at 50% 0%,#161b27,#0c0e15);
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.ntf-30b__page {
  text-align: center;
  padding: 24px;
}

.ntf-30b__ghost {
  font-size: clamp(26px,4vw,36px);
  font-weight: 600;
  letter-spacing: -.02em;
  color: rgba(226,234,250,.22);
}

.ntf-30b__ghostsub {
  margin-top: 8px;
  font-size: 13px;
  color: rgba(226,234,250,.32);
}

.ntf-30b__dial {
  position: absolute;
  bottom: calc(22px + env(safe-area-inset-bottom,0px));
  inset-inline-end: 22px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12px;
}

.ntf-30b__act {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  opacity: 0;
  translate: 0 14px;
  scale: .85;
  pointer-events: none;
  transition: opacity .25s ease,translate .3s cubic-bezier(.22,1.4,.36,1),scale .3s cubic-bezier(.22,1.4,.36,1);
}

.ntf-30b__dial:has(.ntf-30b__state:checked) .ntf-30b__act {
  opacity: 1;
  translate: 0 0;
  scale: 1;
  pointer-events: auto;
  transition-delay: calc(var(--i) * .05s);
}

.ntf-30b__pill {
  font-size: 12.5px;
  font-weight: 600;
  color: #0c0e15;
  background: #e8eefc;
  padding: 8px 13px;
  border-radius: 999px;
  box-shadow: 0 10px 24px -12px rgba(0,0,0,.8);
}

.ntf-30b__mini {
  flex: none;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font-style: normal;
  font-size: 16px;
  color: #e8eefc;
  background: rgba(255,255,255,.1);
  border: 1px solid rgba(165,190,230,.28);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

.ntf-30b__act:hover .ntf-30b__mini {
  background: rgba(255,255,255,.18);
}

.ntf-30b__fab {
  position: relative;
  width: 56px;
  height: 56px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: oklch(0.75 0.13 200);
  cursor: pointer;
  box-shadow: 0 3px 8px rgba(0,0,0,.4),0 22px 44px -16px oklch(0.75 0.13 200 / .5);
  transition: scale .15s,background .2s;
}

.ntf-30b__fab:hover {
  scale: 1.05;
}

.ntf-30b__fab:active {
  scale: .95;
}

.ntf-30b__state {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.ntf-30b__fab:has(.ntf-30b__state:focus-visible) {
  outline: 2.5px solid #e8eefc;
  outline-offset: 3px;
}

.ntf-30b__plus {
  position: relative;
  width: 20px;
  height: 20px;
  transition: rotate .35s cubic-bezier(.22,1.2,.36,1);
}

.ntf-30b__plus::before,
.ntf-30b__plus::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  background: #0c0e15;
  border-radius: 2px;
}

.ntf-30b__plus::before {
  width: 20px;
  height: 3px;
}

.ntf-30b__plus::after {
  width: 3px;
  height: 20px;
}

.ntf-30b__dial:has(.ntf-30b__state:checked) .ntf-30b__plus {
  rotate: 45deg;
}

.ntf-30b__badge {
  position: absolute;
  top: -3px;
  inset-inline-end: -3px;
  min-inline-size: 20px;
  block-size: 20px;
  display: grid;
  place-items: center;
  padding-inline: 5px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 800;
  color: #fff;
  background: oklch(0.63 0.21 25);
  border: 2.5px solid #161b27;
  transition: scale .25s cubic-bezier(.22,1.4,.36,1);
}

.ntf-30b__dial:has(.ntf-30b__state:checked) .ntf-30b__badge {
  scale: 0;
}

@media (prefers-reduced-motion: reduce) {
  .ntf-30b__act {
    transition-duration: .01s;
    transition-delay: 0s!important;
  }

  .ntf-30b__plus {
    transition-duration: .01s;
  }
}
/* No JavaScript — the label lives in a grid column that animates 0fr → 1fr on hover/focus-visible (the today-everywhere version of transitioning to width:auto; interpolate-size:allow-keywords is the coming one-liner). */

/* No JavaScript — a checkbox is the dial's state: :has(:checked) blooms the actions (staggered via --i delays that reset to 0 on close), rotates + into ×, and clears the badge because opening acknowledges the unread count. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Notification from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Floating Feedback Button With Badge
Source: https://codefronts.com/snippets/css-notifications/css-floating-feedback-button-with-badge/

Two endgames for the floating button: a circle-to-pill FAB whose label expands through an animatable grid column (the transition-to-auto-width trick that works today, with interpolate-size noted as the 2027 one-liner), badge riding the morph — and a speed-dial FAB that blooms three labeled actions on a checkbox toggle, each springing out with its own delay, the unread badge clearing the moment the dial opens.
## HTML
```html
<div class="ntf-30-group">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-30a" aria-label="Floating feedback button expanding to a pill">
  <div class="ntf-30a__page">
    <h2 class="ntf-30a__ghost">Docs page</h2>
    <p class="ntf-30a__ghostsub">hover (or tab to) the button in the corner</p>
  </div>
  <button class="ntf-30a__fab" type="button" aria-label="Feedback, 2 unread replies">
    <span class="ntf-30a__glyph" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.4 8.6 8.6 0 0 1-3.9-.9L3 20l1-4.1a8.4 8.4 0 1 1 17-4.4z"/></svg></span>
    <span class="ntf-30a__label" aria-hidden="true"><span>Give feedback</span></span>
    <b class="ntf-30a__badge" aria-hidden="true">2</b>
  </button>
</section>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&display=swap" rel="stylesheet">
<section class="ntf-30b" aria-label="Speed dial floating button with actions">
  <div class="ntf-30b__page">
    <h2 class="ntf-30b__ghost">Support corner</h2>
    <p class="ntf-30b__ghostsub">open the dial — the badge clears, three labeled actions bloom</p>
  </div>
  <div class="ntf-30b__dial">
    <a class="ntf-30b__act" href="#" onclick="return false" style="--i:3"><span class="ntf-30b__pill">Report a bug</span><i class="ntf-30b__mini" aria-hidden="true">&#9888;</i></a>
    <a class="ntf-30b__act" href="#" onclick="return false" style="--i:2"><span class="ntf-30b__pill">Suggest a feature</span><i class="ntf-30b__mini" aria-hidden="true">&#10022;</i></a>
    <a class="ntf-30b__act" href="#" onclick="return false" style="--i:1"><span class="ntf-30b__pill">Message support</span><i class="ntf-30b__mini" aria-hidden="true">&#9993;</i></a>
    <label class="ntf-30b__fab" aria-label="Open support menu, 3 unread">
      <input class="ntf-30b__state" type="checkbox">
      <i class="ntf-30b__plus" aria-hidden="true"></i>
      <b class="ntf-30b__badge" aria-hidden="true">3</b>
    </label>
  </div>
</section>
</div>
```
## CSS
```css
.ntf-30-group {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  width: 100%;
}

.ntf-30-group > section {
  flex: 1 1 480px;
  min-width: min(100%,360px);
}

.ntf-30a,
.ntf-30a *,
.ntf-30a *::before,
.ntf-30a *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ntf-30a {
  position: relative;
  overflow: hidden;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: linear-gradient(180deg,#f6f4ef,#ebe7dc);
  font-family: 'Outfit','Segoe UI',system-ui,sans-serif;
}

.ntf-30a__page {
  text-align: center;
  padding: 24px;
}

.ntf-30a__ghost {
  font-size: clamp(26px,4vw,36px);
  font-weight: 600;
  letter-spacing: -.02em;
  color: rgba(35,31,24,.22);
}

.ntf-30a__ghostsub {
  margin-top: 8px;
  font-size: 13px;
  color: rgba(35,31,24,.35);
}

.ntf-30a__fab {
  position: absolute;
  bottom: calc(22px + env(safe-area-inset-bottom,0px));
  inset-inline-end: 22px;
  display: flex;
  align-items: center;
  height: 56px;
  padding: 0 16px;
  border: 0;
  border-radius: 999px;
  color: #f6f4ef;
  background: #232019;
  cursor: pointer;
  box-shadow: 0 3px 8px rgba(35,31,24,.3),0 22px 44px -16px rgba(35,31,24,.5);
  transition: scale .15s,background .2s;
}

.ntf-30a__fab:hover {
  background: #38332a;
}

.ntf-30a__fab:active {
  scale: .96;
}

.ntf-30a__fab:focus-visible {
  outline: 2.5px solid #232019;
  outline-offset: 3px;
}

.ntf-30a__glyph {
  flex: none;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
}

.ntf-30a__glyph svg {
  width: 22px;
  height: 22px;
}

.ntf-30a__label {
  display: grid;
  grid-template-columns: 0fr;
  transition: grid-template-columns .35s cubic-bezier(.22,1.2,.36,1);
}

.ntf-30a__label span {
  overflow: hidden;
  white-space: nowrap;
  font-size: 14px;
  font-weight: 700;
  padding-inline-start: 0;
  transition: padding .35s cubic-bezier(.22,1.2,.36,1);
}

.ntf-30a__fab:hover .ntf-30a__label,
.ntf-30a__fab:focus-visible .ntf-30a__label {
  grid-template-columns: 1fr;
}

.ntf-30a__fab:hover .ntf-30a__label span,
.ntf-30a__fab:focus-visible .ntf-30a__label span {
  padding-inline-start: 10px;
}

.ntf-30a__badge {
  position: absolute;
  top: -4px;
  inset-inline-end: -2px;
  min-inline-size: 20px;
  block-size: 20px;
  display: grid;
  place-items: center;
  padding-inline: 5px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: #fff;
  background: oklch(0.63 0.21 25);
  border: 2.5px solid #f6f4ef;
  animation: ntf-30a-pop .5s cubic-bezier(.22,1.6,.36,1) both .4s;
}

@keyframes ntf-30a-pop {
  from {
    scale: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ntf-30a__label,
    .ntf-30a__label span {
    transition-duration: .01s;
  }

  .ntf-30a__badge {
    animation: none;
  }
}

.ntf-30b,
.ntf-30b *,
.ntf-30b *::before,
.ntf-30b *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ntf-30b {
  position: relative;
  overflow: hidden;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: radial-gradient(120% 100% at 50% 0%,#161b27,#0c0e15);
  font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}

.ntf-30b__page {
  text-align: center;
  padding: 24px;
}

.ntf-30b__ghost {
  font-size: clamp(26px,4vw,36px);
  font-weight: 600;
  letter-spacing: -.02em;
  color: rgba(226,234,250,.22);
}

.ntf-30b__ghostsub {
  margin-top: 8px;
  font-size: 13px;
  color: rgba(226,234,250,.32);
}

.ntf-30b__dial {
  position: absolute;
  bottom: calc(22px + env(safe-area-inset-bottom,0px));
  inset-inline-end: 22px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 12px;
}

.ntf-30b__act {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  opacity: 0;
  translate: 0 14px;
  scale: .85;
  pointer-events: none;
  transition: opacity .25s ease,translate .3s cubic-bezier(.22,1.4,.36,1),scale .3s cubic-bezier(.22,1.4,.36,1);
}

.ntf-30b__dial:has(.ntf-30b__state:checked) .ntf-30b__act {
  opacity: 1;
  translate: 0 0;
  scale: 1;
  pointer-events: auto;
  transition-delay: calc(var(--i) * .05s);
}

.ntf-30b__pill {
  font-size: 12.5px;
  font-weight: 600;
  color: #0c0e15;
  background: #e8eefc;
  padding: 8px 13px;
  border-radius: 999px;
  box-shadow: 0 10px 24px -12px rgba(0,0,0,.8);
}

.ntf-30b__mini {
  flex: none;
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font-style: normal;
  font-size: 16px;
  color: #e8eefc;
  background: rgba(255,255,255,.1);
  border: 1px solid rgba(165,190,230,.28);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

.ntf-30b__act:hover .ntf-30b__mini {
  background: rgba(255,255,255,.18);
}

.ntf-30b__fab {
  position: relative;
  width: 56px;
  height: 56px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: oklch(0.75 0.13 200);
  cursor: pointer;
  box-shadow: 0 3px 8px rgba(0,0,0,.4),0 22px 44px -16px oklch(0.75 0.13 200 / .5);
  transition: scale .15s,background .2s;
}

.ntf-30b__fab:hover {
  scale: 1.05;
}

.ntf-30b__fab:active {
  scale: .95;
}

.ntf-30b__state {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
}

.ntf-30b__fab:has(.ntf-30b__state:focus-visible) {
  outline: 2.5px solid #e8eefc;
  outline-offset: 3px;
}

.ntf-30b__plus {
  position: relative;
  width: 20px;
  height: 20px;
  transition: rotate .35s cubic-bezier(.22,1.2,.36,1);
}

.ntf-30b__plus::before,
.ntf-30b__plus::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  background: #0c0e15;
  border-radius: 2px;
}

.ntf-30b__plus::before {
  width: 20px;
  height: 3px;
}

.ntf-30b__plus::after {
  width: 3px;
  height: 20px;
}

.ntf-30b__dial:has(.ntf-30b__state:checked) .ntf-30b__plus {
  rotate: 45deg;
}

.ntf-30b__badge {
  position: absolute;
  top: -3px;
  inset-inline-end: -3px;
  min-inline-size: 20px;
  block-size: 20px;
  display: grid;
  place-items: center;
  padding-inline: 5px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 800;
  color: #fff;
  background: oklch(0.63 0.21 25);
  border: 2.5px solid #161b27;
  transition: scale .25s cubic-bezier(.22,1.4,.36,1);
}

.ntf-30b__dial:has(.ntf-30b__state:checked) .ntf-30b__badge {
  scale: 0;
}

@media (prefers-reduced-motion: reduce) {
  .ntf-30b__act {
    transition-duration: .01s;
    transition-delay: 0s!important;
  }

  .ntf-30b__plus {
    transition-duration: .01s;
  }
}
```

## JavaScript
```js
/* No JavaScript — the label lives in a grid column that animates 0fr → 1fr on hover/focus-visible (the today-everywhere version of transitioning to width:auto; interpolate-size:allow-keywords is the coming one-liner). */

/* No JavaScript — a checkbox is the dial's state: :has(:checked) blooms the actions (staggered via --i delays that reset to 0 on close), rotates + into ×, and clears the badge because opening acknowledges the unread count. */
```

How this works

CSS still can't transition width:auto everywhere — but a grid COLUMN can go from nothing to content-sized smoothly:

.fab .label{ display:grid; grid-template-columns:0fr;
  transition:grid-template-columns .35s cubic-bezier(.22,1.2,.36,1) }
.fab:hover .label,.fab:focus-visible .label{ grid-template-columns:1fr }
.fab .label span{ overflow:hidden; white-space:nowrap }

Same trick as animated height, rotated 90° — the span clips while its column grows. (The 2027 version is one line — interpolate-size:allow-keywords lets inline-size transition to auto — shipping in Chromium; the grid trick is the universal today-fallback.) The speed-dial is a checkbox state machine: :has(:checked) rotates the plus into a ×, and each action's transition gets an incremental delay opening (.05s/.1s/.15s) but ZERO delay closing — dials should bloom open and snap shut. The badge clears on open because opening IS acknowledging.

Make it yours

  • FAB geometry: 56px circle, 16–24px from the corner, above safe-area — thumbs expect it exactly there.
  • Expand on :focus-visible too (this demo does) — keyboard users deserve the label, not just pointer users.
  • Speed-dial labels go INSIDE pills, not tooltips — tooltips die on touch; pills are the labels.
  • More than 4 dial actions means you need a menu, not a dial — the bloom stops scanning at 4.

Gotchas — read before shipping

  • A FAB floats over content: give the page bottom padding equal to FAB height + offset or it will cover the last row of every list.
  • The 0fr trick needs the CHILD to own overflow:hidden — clipping on the grid itself clips the FAB's own shadow.
  • Speed-dial actions must close on outside click in production — pure CSS can't; promote the state to a popover or add 3 lines of JS.
  • Two FABs on one screen compete for the same thumb — if feedback AND chat both want the corner, they share one dial.

Browser support

ChromeSafariFirefoxEdge
129+not yetnot yet129+

Floor set by :has(), oklch(), backdrop-filter, interpolate-size as this demo is written. The core technique itself goes back further — Chrome 105+.

The grid-column label reveal works in all evergreen browsers (animatable grid tracks: Chrome 107 / Safari 16 / Firefox 66); the speed-dial's :has() needs Chrome 105 / Safari 15.4 / Firefox 121. interpolate-size (mentioned in comments) is Chromium 129+ progressive enhancement.

Techniques used in this demo

Search CodeFronts

Loading…