34 CSS Floating Buttons11 / 34

Light JSMIT licensed

Morphing FAB Button That Expands to Pill CSS

A FAB that does not open a panel — it becomes one. Tap the circle and it stretches into a search pill with a live input and results; dismiss and it retracts into the same circle. Uses the View Transition API for the shared-element morph with a pure-CSS fallback, so the container feels continuous instead of one element vanishing while another appears.

Published Updated

Live Demo
Try it

The code

<section class="fb-11" aria-label="Morphing floating button demo">
  <div class="fb-11__page">
    <p class="fb-11__eyebrow">view-transition-name · shared element</p>
    <h2 class="fb-11__title">The button becomes the panel</h2>
    <p class="fb-11__sub">No fade-out, no pop-in. One container that stretches, keeps its identity, and hands focus to the field inside it.</p>
    <p class="fb-11__chip">startViewTransition() · Escape to retract</p>
  </div>
  <div class="fb-11__dock" id="fb-11-dock" data-open="false">
    <div class="fb-11__pill" id="fb-11-pill">
      <span class="fb-11__pIco" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><circle cx="11" cy="11" r="6.4" fill="none" stroke="currentColor" stroke-width="2"/><path d="M15.8 15.8L21 21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>
      <input class="fb-11__input" id="fb-11-input" type="text" placeholder="Search commands…" aria-label="Search commands" autocomplete="off">
      <button class="fb-11__close" id="fb-11-close" type="button" aria-label="Close search">×</button>
      <ul class="fb-11__results" id="fb-11-results">
        <li><button type="button"><kbd>⌘K</kbd>Open command palette</button></li>
        <li><button type="button"><kbd>N</kbd>New document</button></li>
        <li><button type="button"><kbd>T</kbd>Toggle theme</button></li>
      </ul>
    </div>
    <button class="fb-11__fab" id="fb-11-fab" type="button" aria-expanded="false" aria-controls="fb-11-pill" aria-label="Open search">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.4" fill="none" stroke="currentColor" stroke-width="2.2"/><path d="M15.8 15.8L21 21" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
    </button>
  </div>
</section>
.fb-11 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--fb-11-bg);
  --fb-11-bg: linear-gradient(200deg,oklch(0.22 0.04 275),oklch(0.14 0.02 275) 65%);
  --fb-11-brand: oklch(0.74 0.16 165);
  --fb-11-ink: oklch(0.97 0.01 275);
  --fb-11-mut: oklch(0.75 0.02 275);
  --fb-11-card: oklch(0.24 0.03 275);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--fb-11-ink);
  position: relative;
  display: grid;
  place-items: center;
  padding: clamp(20px,5vw,56px);
  overflow: hidden;
}

.fb-11 *,
.fb-11 *::before,
.fb-11 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.fb-11__page {
  width: min(100%,54ch);
  display: grid;
  gap: 12px;
  justify-items: center;
  text-align: center;
}

.fb-11__eyebrow {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fb-11-brand);
}

.fb-11__title {
  font-size: clamp(26px,4.6vw,44px);
  line-height: 1.05;
  letter-spacing: -.03em;
  font-weight: 700;
  text-wrap: balance;
}

.fb-11__sub {
  font-size: clamp(13.5px,1.7vw,16.5px);
  line-height: 1.62;
  color: var(--fb-11-mut);
  text-wrap: pretty;
}

.fb-11__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: var(--fb-11-mut);
  padding: 8px 14px;
  border: 1px solid oklch(1 0 0/.16);
  border-radius: 99px;
}

.fb-11__dock {
  position: absolute;
  right: clamp(18px,4vw,44px);
  bottom: calc(clamp(18px,4vw,44px) + env(safe-area-inset-bottom));
  z-index: 5;
  display: grid;
  justify-items: end;
}

.fb-11__fab {
  view-transition-name: fb-11-morph;
  display: grid;
  place-items: center;
  inline-size: 62px;
  block-size: 62px;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  color: oklch(0.16 0.04 165);
  background: linear-gradient(150deg,oklch(0.86 0.14 155),var(--fb-11-brand));
  box-shadow: 0 14px 34px -12px oklch(0.74 0.16 165/.6),0 2px 6px oklch(0.1 0.02 275/.6);
  transition: transform .28s cubic-bezier(.2,1.3,.4,1),opacity .2s ease;
}

.fb-11__fab svg {
  width: 26px;
  height: 26px;
}

.fb-11__fab:hover {
  transform: translateY(-2px) scale(1.04);
}

.fb-11__fab:active {
  transform: scale(.93);
}

.fb-11__fab:focus-visible {
  outline: 3px solid var(--fb-11-brand);
  outline-offset: 4px;
}

.fb-11__pill {
  display: none;
  view-transition-name: fb-11-morph;
  width: min(86vw,392px);
  padding: 10px 10px 10px 16px;
  border-radius: 26px;
  background: var(--fb-11-card);
  border: 1px solid oklch(1 0 0/.1);
  box-shadow: 0 30px 70px -26px oklch(0.05 0.02 275/.9),0 2px 6px oklch(0.05 0.02 275/.5);
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  transition: width .34s cubic-bezier(.2,.9,.3,1),border-radius .34s ease;
}

.fb-11__dock[data-open="true"] .fb-11__pill {
  display: grid;
}

.fb-11__dock[data-open="true"] .fb-11__fab {
  display: none;
}

.fb-11__pIco {
  display: grid;
  place-items: center;
  color: var(--fb-11-brand);
}

.fb-11__pIco svg {
  width: 20px;
  height: 20px;
}

.fb-11__input {
  grid-column: 2;
  min-height: 42px;
  border: 0;
  background: transparent;
  color: var(--fb-11-ink);
  font-size: 15.5px;
  font-family: inherit;
  outline: 0;
  min-width: 0;
}

.fb-11__input::placeholder {
  color: oklch(0.65 0.02 275);
}

.fb-11__input:focus-visible {
  outline: 2px solid var(--fb-11-brand);
  outline-offset: 4px;
  border-radius: 8px;
}

.fb-11__close {
  grid-column: 3;
  width: 38px;
  height: 38px;
  border: 0;
  border-radius: 50%;
  background: oklch(1 0 0/.07);
  color: var(--fb-11-mut);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: background .18s ease,color .18s ease;
}

.fb-11__close:hover {
  background: oklch(1 0 0/.14);
  color: var(--fb-11-ink);
}

.fb-11__close:focus-visible {
  outline: 2.5px solid var(--fb-11-brand);
  outline-offset: 2px;
}

.fb-11__results {
  grid-column: 1/-1;
  list-style: none;
  display: grid;
  gap: 2px;
  margin-top: 6px;
  border-top: 1px solid oklch(1 0 0/.08);
  padding-top: 8px;
}

.fb-11__results button {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  min-height: 44px;
  padding: 0 12px;
  border: 0;
  border-radius: 14px;
  background: transparent;
  color: var(--fb-11-ink);
  font-size: 14px;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: background .16s ease;
}

.fb-11__results button:hover {
  background: oklch(1 0 0/.07);
}

.fb-11__results button:focus-visible {
  outline: 2.5px solid var(--fb-11-brand);
  outline-offset: -2px;
}

.fb-11__results kbd {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  min-width: 32px;
  text-align: center;
  padding: 3px 6px;
  border-radius: 6px;
  background: oklch(1 0 0/.09);
  border: 1px solid oklch(1 0 0/.12);
  color: var(--fb-11-mut);
}

::view-transition-group(fb-11-morph) {
  animation-duration: .42s;
  animation-timing-function: cubic-bezier(.2,.9,.25,1);
}

@media (prefers-reduced-motion: reduce) {
  .fb-11 * {
    transition: none !important;
  }

  ::view-transition-group(fb-11-morph) {
    animation-duration: .01ms;
  }
}
(() => {
  const dock = document.getElementById('fb-11-dock');
  const fab = document.getElementById('fb-11-fab');
  const input = document.getElementById('fb-11-input');
  const close = document.getElementById('fb-11-close');
  if (!dock || !fab || fab.dataset.fbWired) return;
  fab.dataset.fbWired = '1';
  const apply = (open) => {
    dock.dataset.open = String(open);
    fab.setAttribute('aria-expanded', String(open));
  };
  const swap = (open) => {
    if (!document.startViewTransition) { apply(open); return Promise.resolve(); }
    return document.startViewTransition(() => apply(open)).finished;
  };
  fab.addEventListener('click', () => { swap(true).then(() => input.focus()); });
  close.addEventListener('click', () => { swap(false).then(() => fab.focus()); });
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') { swap(false).then(() => fab.focus()); }
  });
})();
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 Floating Button 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: Morphing FAB Button That Expands to Pill CSS
Source: https://codefronts.com/components/css-floating-buttons/morph-expand-fab/

A FAB that does not open a panel — it becomes one. Tap the circle and it stretches into a search pill with a live input and results; dismiss and it retracts into the same circle. Uses the View Transition API for the shared-element morph with a pure-CSS fallback, so the container feels continuous instead of one element vanishing while another appears.
## HTML
```html
<section class="fb-11" aria-label="Morphing floating button demo">
  <div class="fb-11__page">
    <p class="fb-11__eyebrow">view-transition-name · shared element</p>
    <h2 class="fb-11__title">The button becomes the panel</h2>
    <p class="fb-11__sub">No fade-out, no pop-in. One container that stretches, keeps its identity, and hands focus to the field inside it.</p>
    <p class="fb-11__chip">startViewTransition() · Escape to retract</p>
  </div>
  <div class="fb-11__dock" id="fb-11-dock" data-open="false">
    <div class="fb-11__pill" id="fb-11-pill">
      <span class="fb-11__pIco" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><circle cx="11" cy="11" r="6.4" fill="none" stroke="currentColor" stroke-width="2"/><path d="M15.8 15.8L21 21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>
      <input class="fb-11__input" id="fb-11-input" type="text" placeholder="Search commands…" aria-label="Search commands" autocomplete="off">
      <button class="fb-11__close" id="fb-11-close" type="button" aria-label="Close search">×</button>
      <ul class="fb-11__results" id="fb-11-results">
        <li><button type="button"><kbd>⌘K</kbd>Open command palette</button></li>
        <li><button type="button"><kbd>N</kbd>New document</button></li>
        <li><button type="button"><kbd>T</kbd>Toggle theme</button></li>
      </ul>
    </div>
    <button class="fb-11__fab" id="fb-11-fab" type="button" aria-expanded="false" aria-controls="fb-11-pill" aria-label="Open search">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.4" fill="none" stroke="currentColor" stroke-width="2.2"/><path d="M15.8 15.8L21 21" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>
    </button>
  </div>
</section>
```
## CSS
```css
.fb-11 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--fb-11-bg);
  --fb-11-bg: linear-gradient(200deg,oklch(0.22 0.04 275),oklch(0.14 0.02 275) 65%);
  --fb-11-brand: oklch(0.74 0.16 165);
  --fb-11-ink: oklch(0.97 0.01 275);
  --fb-11-mut: oklch(0.75 0.02 275);
  --fb-11-card: oklch(0.24 0.03 275);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--fb-11-ink);
  position: relative;
  display: grid;
  place-items: center;
  padding: clamp(20px,5vw,56px);
  overflow: hidden;
}

.fb-11 *,
.fb-11 *::before,
.fb-11 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.fb-11__page {
  width: min(100%,54ch);
  display: grid;
  gap: 12px;
  justify-items: center;
  text-align: center;
}

.fb-11__eyebrow {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fb-11-brand);
}

.fb-11__title {
  font-size: clamp(26px,4.6vw,44px);
  line-height: 1.05;
  letter-spacing: -.03em;
  font-weight: 700;
  text-wrap: balance;
}

.fb-11__sub {
  font-size: clamp(13.5px,1.7vw,16.5px);
  line-height: 1.62;
  color: var(--fb-11-mut);
  text-wrap: pretty;
}

.fb-11__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: var(--fb-11-mut);
  padding: 8px 14px;
  border: 1px solid oklch(1 0 0/.16);
  border-radius: 99px;
}

.fb-11__dock {
  position: absolute;
  right: clamp(18px,4vw,44px);
  bottom: calc(clamp(18px,4vw,44px) + env(safe-area-inset-bottom));
  z-index: 5;
  display: grid;
  justify-items: end;
}

.fb-11__fab {
  view-transition-name: fb-11-morph;
  display: grid;
  place-items: center;
  inline-size: 62px;
  block-size: 62px;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  color: oklch(0.16 0.04 165);
  background: linear-gradient(150deg,oklch(0.86 0.14 155),var(--fb-11-brand));
  box-shadow: 0 14px 34px -12px oklch(0.74 0.16 165/.6),0 2px 6px oklch(0.1 0.02 275/.6);
  transition: transform .28s cubic-bezier(.2,1.3,.4,1),opacity .2s ease;
}

.fb-11__fab svg {
  width: 26px;
  height: 26px;
}

.fb-11__fab:hover {
  transform: translateY(-2px) scale(1.04);
}

.fb-11__fab:active {
  transform: scale(.93);
}

.fb-11__fab:focus-visible {
  outline: 3px solid var(--fb-11-brand);
  outline-offset: 4px;
}

.fb-11__pill {
  display: none;
  view-transition-name: fb-11-morph;
  width: min(86vw,392px);
  padding: 10px 10px 10px 16px;
  border-radius: 26px;
  background: var(--fb-11-card);
  border: 1px solid oklch(1 0 0/.1);
  box-shadow: 0 30px 70px -26px oklch(0.05 0.02 275/.9),0 2px 6px oklch(0.05 0.02 275/.5);
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  transition: width .34s cubic-bezier(.2,.9,.3,1),border-radius .34s ease;
}

.fb-11__dock[data-open="true"] .fb-11__pill {
  display: grid;
}

.fb-11__dock[data-open="true"] .fb-11__fab {
  display: none;
}

.fb-11__pIco {
  display: grid;
  place-items: center;
  color: var(--fb-11-brand);
}

.fb-11__pIco svg {
  width: 20px;
  height: 20px;
}

.fb-11__input {
  grid-column: 2;
  min-height: 42px;
  border: 0;
  background: transparent;
  color: var(--fb-11-ink);
  font-size: 15.5px;
  font-family: inherit;
  outline: 0;
  min-width: 0;
}

.fb-11__input::placeholder {
  color: oklch(0.65 0.02 275);
}

.fb-11__input:focus-visible {
  outline: 2px solid var(--fb-11-brand);
  outline-offset: 4px;
  border-radius: 8px;
}

.fb-11__close {
  grid-column: 3;
  width: 38px;
  height: 38px;
  border: 0;
  border-radius: 50%;
  background: oklch(1 0 0/.07);
  color: var(--fb-11-mut);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: background .18s ease,color .18s ease;
}

.fb-11__close:hover {
  background: oklch(1 0 0/.14);
  color: var(--fb-11-ink);
}

.fb-11__close:focus-visible {
  outline: 2.5px solid var(--fb-11-brand);
  outline-offset: 2px;
}

.fb-11__results {
  grid-column: 1/-1;
  list-style: none;
  display: grid;
  gap: 2px;
  margin-top: 6px;
  border-top: 1px solid oklch(1 0 0/.08);
  padding-top: 8px;
}

.fb-11__results button {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  min-height: 44px;
  padding: 0 12px;
  border: 0;
  border-radius: 14px;
  background: transparent;
  color: var(--fb-11-ink);
  font-size: 14px;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: background .16s ease;
}

.fb-11__results button:hover {
  background: oklch(1 0 0/.07);
}

.fb-11__results button:focus-visible {
  outline: 2.5px solid var(--fb-11-brand);
  outline-offset: -2px;
}

.fb-11__results kbd {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  min-width: 32px;
  text-align: center;
  padding: 3px 6px;
  border-radius: 6px;
  background: oklch(1 0 0/.09);
  border: 1px solid oklch(1 0 0/.12);
  color: var(--fb-11-mut);
}

::view-transition-group(fb-11-morph) {
  animation-duration: .42s;
  animation-timing-function: cubic-bezier(.2,.9,.25,1);
}

@media (prefers-reduced-motion: reduce) {
  .fb-11 * {
    transition: none !important;
  }

  ::view-transition-group(fb-11-morph) {
    animation-duration: .01ms;
  }
}
```

## JavaScript
```js
(() => {
  const dock = document.getElementById('fb-11-dock');
  const fab = document.getElementById('fb-11-fab');
  const input = document.getElementById('fb-11-input');
  const close = document.getElementById('fb-11-close');
  if (!dock || !fab || fab.dataset.fbWired) return;
  fab.dataset.fbWired = '1';
  const apply = (open) => {
    dock.dataset.open = String(open);
    fab.setAttribute('aria-expanded', String(open));
  };
  const swap = (open) => {
    if (!document.startViewTransition) { apply(open); return Promise.resolve(); }
    return document.startViewTransition(() => apply(open)).finished;
  };
  fab.addEventListener('click', () => { swap(true).then(() => input.focus()); });
  close.addEventListener('click', () => { swap(false).then(() => fab.focus()); });
  input.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') { swap(false).then(() => fab.focus()); }
  });
})();
```

How this works

Both states share a view-transition-name, which is all the browser needs to tween between them:

.fab, .pill{ view-transition-name:fb-morph }
btn.onclick = () => {
  if (!document.startViewTransition) return swap();
  document.startViewTransition(() => swap());
};

Inside the callback you simply swap which element is in the DOM. The browser snapshots the old state, applies the new one, and animates position, size and border-radius between the two snapshots — a container morph with no FLIP maths and no animation library.

The fallback matters, because the morph should not be load-bearing. Without startViewTransition the same swap runs inside a CSS transition on inline-size and border-radius, which gets you 80% of the effect. Feature-detect, never assume.

Focus management is the part that makes it usable: on expand, focus moves into the input; on collapse, it returns to the FAB. Escape collapses. The input is a bare input with a sibling button — no form element, so a stray Enter key can never navigate the page away.

Make it yours

  • Morph target: the same technique turns a FAB into a full compose sheet, a colour picker, or a mini player. Only the expanded markup changes.
  • Duration: ::view-transition-group(fb-morph){ animation-duration:.42s } tunes the morph independently of everything else on the page.
  • Direction: anchor transform-origin and the pill's inset-inline-end to the FAB's corner so it always grows away from the screen edge.
  • Result list: the panel under the pill can animate its own rows with a staggered @starting-style for a layered reveal.

Gotchas — read before shipping

  • view-transition-name must be unique per page at any given moment. Two visible elements sharing one name aborts the transition with a console error.
  • startViewTransition takes a callback that performs the DOM change synchronously — awaiting a fetch inside it holds the page frozen on the old snapshot.
  • Autofocusing an input on expand opens the mobile keyboard, which can push a bottom-anchored pill off-screen. Focus after the transition finishes, and keep the pill above the keyboard inset.
  • Never place the input inside a form in an embedded playground — a submit reloads the frame. A button plus keydown handler is enough.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

Same-document view transitions: Chrome 111+, Safari 18+, Firefox 144+. Everywhere else the CSS transition fallback runs — the feature detection is two lines and this demo ships it.

Search CodeFronts

Loading…