34 CSS Floating Buttons19 / 34

Pure CSSMIT licensed

Floating Compose Button with Tooltip on Hover CSS

An icon-only FAB is only usable if it can say what it does. This is the tooltip done to spec: it appears on hover and keyboard focus, waits 400ms before showing but disappears instantly, never covers the button, flips when it would leave the viewport, and — critically — the tooltip text is not the accessible name, it is a description of it.

Published Updated

Live Demo
Try it

The code

<section class="fb-19" aria-label="Floating compose button with tooltip demo">
  <div class="fb-19__app">
    <aside class="fb-19__rail">
      <button class="fb-19__railBtn" type="button" aria-label="Inbox" aria-describedby="fb-19-t1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 12l3-7h12l3 7v6H3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M3 12h5l1.4 2.4h5.2L16 12h5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg></button>
      <span class="fb-19__tip fb-19__tip--rail" id="fb-19-t1" role="tooltip">Inbox <kbd>G</kbd> <kbd>I</kbd></span>
      <button class="fb-19__railBtn" type="button" aria-label="Starred" aria-describedby="fb-19-t2"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 3.6l2.5 5.4 5.9.6-4.4 4 1.2 5.8L12 16.4 6.8 19.4 8 13.6l-4.4-4 5.9-.6z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg></button>
      <span class="fb-19__tip fb-19__tip--rail" id="fb-19-t2" role="tooltip">Starred <kbd>G</kbd> <kbd>S</kbd></span>
      <button class="fb-19__railBtn" type="button" aria-label="Archive" aria-describedby="fb-19-t3"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3" y="4" width="18" height="4.5" rx="1.4" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M5 8.5V19h14V8.5M10 12h4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
      <span class="fb-19__tip fb-19__tip--rail" id="fb-19-t3" role="tooltip">Archive <kbd>E</kbd></span>
    </aside>
    <div class="fb-19__main">
      <p class="fb-19__eyebrow">400ms in · 0ms out · flip-inline</p>
      <h2 class="fb-19__title">A tooltip is a description, not a name</h2>
      <p class="fb-19__sub">Hover the rail, then tab through it. Same tooltip, same timing, and the button still announces its own label first.</p>
      <p class="fb-19__chip">aria-describedby + aria-label · anchor()</p>
    </div>
    <div class="fb-19__dock">
      <button class="fb-19__fab" id="fb-19-fab" type="button" aria-label="Compose" aria-describedby="fb-19-tip">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 20h4L18.5 9.5l-4-4L4 16z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/><path d="M14.5 5.5l4 4" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
      </button>
      <span class="fb-19__tip" id="fb-19-tip" role="tooltip"><b>Compose</b><kbd>C</kbd></span>
    </div>
  </div>
</section>
.fb-19 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--fb-19-bg);
  --fb-19-bg: oklch(0.96 0.008 265);
  --fb-19-brand: oklch(0.52 0.16 258);
  --fb-19-ink: oklch(0.22 0.02 262);
  --fb-19-mut: oklch(0.53 0.015 262);
  --fb-19-card: oklch(1 0 0);
  --fb-19-tip: oklch(0.24 0.03 262);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--fb-19-ink);
  display: grid;
  place-items: center;
  padding: clamp(18px,4vw,48px);
}

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

.fb-19__app {
  position: relative;
  width: min(100%,760px);
  min-height: min(62svh,520px);
  display: grid;
  grid-template-columns: auto 1fr;
  gap: clamp(16px,3vw,32px);
  padding: clamp(18px,3vw,26px);
  border-radius: 26px;
  background: var(--fb-19-card);
  border: 1px solid oklch(0.22 0.02 262/.09);
  box-shadow: 0 26px 60px -34px oklch(0.22 0.02 262/.85);
}

.fb-19__rail {
  position: relative;
  display: grid;
  align-content: start;
  gap: 10px;
  padding: 10px;
  border-radius: 20px;
  background: oklch(0.97 0.008 262);
  border: 1px solid oklch(0.22 0.02 262/.07);
}

.fb-19__railBtn {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  border: 0;
  border-radius: 14px;
  background: transparent;
  color: var(--fb-19-mut);
  cursor: pointer;
  transition: background .16s ease,color .16s ease,transform .16s ease;
}

.fb-19__railBtn svg {
  width: 22px;
  height: 22px;
}

.fb-19__railBtn:hover {
  background: oklch(0.52 0.16 258/.1);
  color: var(--fb-19-brand);
}

.fb-19__railBtn:active {
  transform: scale(.94);
}

.fb-19__railBtn:focus-visible {
  outline: 3px solid var(--fb-19-brand);
  outline-offset: 2px;
}

.fb-19__main {
  display: grid;
  align-content: center;
  gap: 12px;
  justify-items: start;
  padding-right: clamp(0px,8vw,90px);
}

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

.fb-19__title {
  font-size: clamp(23px,3.8vw,38px);
  line-height: 1.06;
  letter-spacing: -.03em;
  font-weight: 700;
  text-wrap: balance;
}

.fb-19__sub {
  max-width: 44ch;
  font-size: clamp(13.5px,1.6vw,16px);
  line-height: 1.62;
  color: var(--fb-19-mut);
  text-wrap: pretty;
}

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

.fb-19__dock {
  position: absolute;
  right: clamp(16px,3vw,28px);
  bottom: clamp(16px,3vw,28px);
}

.fb-19__fab {
  anchor-name: --fb-19-anchor;
  display: grid;
  place-items: center;
  inline-size: 60px;
  block-size: 60px;
  border: 0;
  border-radius: 20px;
  cursor: pointer;
  color: oklch(0.99 0.01 258);
  background: linear-gradient(150deg,oklch(0.62 0.17 254),oklch(0.42 0.16 262));
  box-shadow: 0 14px 32px -12px oklch(0.42 0.16 262/.85),0 2px 5px oklch(0.3 0.06 262/.25);
  transition: transform .28s cubic-bezier(.2,1.3,.4,1);
}

.fb-19__fab svg {
  width: 25px;
  height: 25px;
}

.fb-19__fab:hover {
  transform: translateY(-2px);
}

.fb-19__fab:active {
  transform: scale(.94);
}

.fb-19__fab:focus-visible {
  outline: 3px solid oklch(0.42 0.16 262);
  outline-offset: 4px;
}

.fb-19__tip {
  position: absolute;
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
  padding: 8px 12px;
  border-radius: 10px;
  background: var(--fb-19-tip);
  color: oklch(0.98 0.01 262);
  font-size: 12.5px;
  font-weight: 600;
  pointer-events: none;
  opacity: 0;
  box-shadow: 0 8px 22px -10px oklch(0.22 0.02 262/.9);
  right: calc(100% + 12px);
  top: 50%;
  translate: -8px -50%;
  transition: opacity .16s ease .4s,translate .2s cubic-bezier(.2,.9,.3,1) .4s;
}

.fb-19__tip::after {
  content: "";
  position: absolute;
  left: 100%;
  top: 50%;
  translate: -3px -50%;
  width: 8px;
  height: 8px;
  background: var(--fb-19-tip);
  rotate: 45deg;
  border-radius: 1px;
}

.fb-19__tip kbd {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  padding: 2px 6px;
  border-radius: 5px;
  background: oklch(1 0 0/.14);
  border: 1px solid oklch(1 0 0/.16);
}

.fb-19__fab:is(:hover,:focus-visible) + .fb-19__tip,
.fb-19__railBtn:is(:hover,:focus-visible) + .fb-19__tip {
  opacity: 1;
  translate: 0 -50%;
  transition-delay: 0s,0s;
}

.fb-19__tip--rail {
  right: auto;
  left: calc(100% + 12px);
  translate: 8px -50%;
  margin-top: -34px;
}

.fb-19__tip--rail::after {
  left: auto;
  right: 100%;
  translate: 3px -50%;
}

.fb-19__railBtn:is(:hover,:focus-visible) + .fb-19__tip--rail {
  translate: 0 -50%;
}

@supports (anchor-name: --x) {
  .fb-19__tip {
    position-anchor: --fb-19-anchor;
    position-try-fallbacks: flip-inline;
    right: anchor(left);
    top: anchor(center);
    inset-inline-start: auto;
  }
}

@media (max-width: 640px) {
  .fb-19__app {
    grid-template-columns: 1fr;
  }

  .fb-19__rail {
    grid-auto-flow: column;
    justify-content: start;
  }

  .fb-19__tip--rail {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .fb-19 * {
    transition: none !important;
  }
}
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: Floating Compose Button with Tooltip on Hover CSS
Source: https://codefronts.com/components/css-floating-buttons/compose-tooltip-fab/

An icon-only FAB is only usable if it can say what it does. This is the tooltip done to spec: it appears on hover and keyboard focus, waits 400ms before showing but disappears instantly, never covers the button, flips when it would leave the viewport, and — critically — the tooltip text is not the accessible name, it is a description of it.
## HTML
```html
<section class="fb-19" aria-label="Floating compose button with tooltip demo">
  <div class="fb-19__app">
    <aside class="fb-19__rail">
      <button class="fb-19__railBtn" type="button" aria-label="Inbox" aria-describedby="fb-19-t1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 12l3-7h12l3 7v6H3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M3 12h5l1.4 2.4h5.2L16 12h5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg></button>
      <span class="fb-19__tip fb-19__tip--rail" id="fb-19-t1" role="tooltip">Inbox <kbd>G</kbd> <kbd>I</kbd></span>
      <button class="fb-19__railBtn" type="button" aria-label="Starred" aria-describedby="fb-19-t2"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 3.6l2.5 5.4 5.9.6-4.4 4 1.2 5.8L12 16.4 6.8 19.4 8 13.6l-4.4-4 5.9-.6z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg></button>
      <span class="fb-19__tip fb-19__tip--rail" id="fb-19-t2" role="tooltip">Starred <kbd>G</kbd> <kbd>S</kbd></span>
      <button class="fb-19__railBtn" type="button" aria-label="Archive" aria-describedby="fb-19-t3"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3" y="4" width="18" height="4.5" rx="1.4" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M5 8.5V19h14V8.5M10 12h4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
      <span class="fb-19__tip fb-19__tip--rail" id="fb-19-t3" role="tooltip">Archive <kbd>E</kbd></span>
    </aside>
    <div class="fb-19__main">
      <p class="fb-19__eyebrow">400ms in · 0ms out · flip-inline</p>
      <h2 class="fb-19__title">A tooltip is a description, not a name</h2>
      <p class="fb-19__sub">Hover the rail, then tab through it. Same tooltip, same timing, and the button still announces its own label first.</p>
      <p class="fb-19__chip">aria-describedby + aria-label · anchor()</p>
    </div>
    <div class="fb-19__dock">
      <button class="fb-19__fab" id="fb-19-fab" type="button" aria-label="Compose" aria-describedby="fb-19-tip">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 20h4L18.5 9.5l-4-4L4 16z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/><path d="M14.5 5.5l4 4" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
      </button>
      <span class="fb-19__tip" id="fb-19-tip" role="tooltip"><b>Compose</b><kbd>C</kbd></span>
    </div>
  </div>
</section>
```
## CSS
```css
.fb-19 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--fb-19-bg);
  --fb-19-bg: oklch(0.96 0.008 265);
  --fb-19-brand: oklch(0.52 0.16 258);
  --fb-19-ink: oklch(0.22 0.02 262);
  --fb-19-mut: oklch(0.53 0.015 262);
  --fb-19-card: oklch(1 0 0);
  --fb-19-tip: oklch(0.24 0.03 262);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--fb-19-ink);
  display: grid;
  place-items: center;
  padding: clamp(18px,4vw,48px);
}

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

.fb-19__app {
  position: relative;
  width: min(100%,760px);
  min-height: min(62svh,520px);
  display: grid;
  grid-template-columns: auto 1fr;
  gap: clamp(16px,3vw,32px);
  padding: clamp(18px,3vw,26px);
  border-radius: 26px;
  background: var(--fb-19-card);
  border: 1px solid oklch(0.22 0.02 262/.09);
  box-shadow: 0 26px 60px -34px oklch(0.22 0.02 262/.85);
}

.fb-19__rail {
  position: relative;
  display: grid;
  align-content: start;
  gap: 10px;
  padding: 10px;
  border-radius: 20px;
  background: oklch(0.97 0.008 262);
  border: 1px solid oklch(0.22 0.02 262/.07);
}

.fb-19__railBtn {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  border: 0;
  border-radius: 14px;
  background: transparent;
  color: var(--fb-19-mut);
  cursor: pointer;
  transition: background .16s ease,color .16s ease,transform .16s ease;
}

.fb-19__railBtn svg {
  width: 22px;
  height: 22px;
}

.fb-19__railBtn:hover {
  background: oklch(0.52 0.16 258/.1);
  color: var(--fb-19-brand);
}

.fb-19__railBtn:active {
  transform: scale(.94);
}

.fb-19__railBtn:focus-visible {
  outline: 3px solid var(--fb-19-brand);
  outline-offset: 2px;
}

.fb-19__main {
  display: grid;
  align-content: center;
  gap: 12px;
  justify-items: start;
  padding-right: clamp(0px,8vw,90px);
}

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

.fb-19__title {
  font-size: clamp(23px,3.8vw,38px);
  line-height: 1.06;
  letter-spacing: -.03em;
  font-weight: 700;
  text-wrap: balance;
}

.fb-19__sub {
  max-width: 44ch;
  font-size: clamp(13.5px,1.6vw,16px);
  line-height: 1.62;
  color: var(--fb-19-mut);
  text-wrap: pretty;
}

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

.fb-19__dock {
  position: absolute;
  right: clamp(16px,3vw,28px);
  bottom: clamp(16px,3vw,28px);
}

.fb-19__fab {
  anchor-name: --fb-19-anchor;
  display: grid;
  place-items: center;
  inline-size: 60px;
  block-size: 60px;
  border: 0;
  border-radius: 20px;
  cursor: pointer;
  color: oklch(0.99 0.01 258);
  background: linear-gradient(150deg,oklch(0.62 0.17 254),oklch(0.42 0.16 262));
  box-shadow: 0 14px 32px -12px oklch(0.42 0.16 262/.85),0 2px 5px oklch(0.3 0.06 262/.25);
  transition: transform .28s cubic-bezier(.2,1.3,.4,1);
}

.fb-19__fab svg {
  width: 25px;
  height: 25px;
}

.fb-19__fab:hover {
  transform: translateY(-2px);
}

.fb-19__fab:active {
  transform: scale(.94);
}

.fb-19__fab:focus-visible {
  outline: 3px solid oklch(0.42 0.16 262);
  outline-offset: 4px;
}

.fb-19__tip {
  position: absolute;
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
  padding: 8px 12px;
  border-radius: 10px;
  background: var(--fb-19-tip);
  color: oklch(0.98 0.01 262);
  font-size: 12.5px;
  font-weight: 600;
  pointer-events: none;
  opacity: 0;
  box-shadow: 0 8px 22px -10px oklch(0.22 0.02 262/.9);
  right: calc(100% + 12px);
  top: 50%;
  translate: -8px -50%;
  transition: opacity .16s ease .4s,translate .2s cubic-bezier(.2,.9,.3,1) .4s;
}

.fb-19__tip::after {
  content: "";
  position: absolute;
  left: 100%;
  top: 50%;
  translate: -3px -50%;
  width: 8px;
  height: 8px;
  background: var(--fb-19-tip);
  rotate: 45deg;
  border-radius: 1px;
}

.fb-19__tip kbd {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  padding: 2px 6px;
  border-radius: 5px;
  background: oklch(1 0 0/.14);
  border: 1px solid oklch(1 0 0/.16);
}

.fb-19__fab:is(:hover,:focus-visible) + .fb-19__tip,
.fb-19__railBtn:is(:hover,:focus-visible) + .fb-19__tip {
  opacity: 1;
  translate: 0 -50%;
  transition-delay: 0s,0s;
}

.fb-19__tip--rail {
  right: auto;
  left: calc(100% + 12px);
  translate: 8px -50%;
  margin-top: -34px;
}

.fb-19__tip--rail::after {
  left: auto;
  right: 100%;
  translate: 3px -50%;
}

.fb-19__railBtn:is(:hover,:focus-visible) + .fb-19__tip--rail {
  translate: 0 -50%;
}

@supports (anchor-name: --x) {
  .fb-19__tip {
    position-anchor: --fb-19-anchor;
    position-try-fallbacks: flip-inline;
    right: anchor(left);
    top: anchor(center);
    inset-inline-start: auto;
  }
}

@media (max-width: 640px) {
  .fb-19__app {
    grid-template-columns: 1fr;
  }

  .fb-19__rail {
    grid-auto-flow: column;
    justify-content: start;
  }

  .fb-19__tip--rail {
    display: none;
  }
}

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

How this works

Anchor positioning removes all the offset maths, and the delay is asymmetric:

.fab{ anchor-name:--compose }
.tip{ position:absolute; position-anchor:--compose;
      right:anchor(left); bottom:anchor(center);
      translate:-10px 50%; opacity:0;
      transition:opacity .16s .4s, translate .2s .4s }   /* delay in */
.fab:is(:hover,:focus-visible) + .tip{ opacity:1; translate:0 50%;
      transition-delay:0s }                              /* no delay out */

The 400ms in-delay stops tooltips from strobing as the pointer crosses a toolbar; zero out-delay means they never linger over content the user is trying to reach. Getting this asymmetry right is most of what separates a good tooltip from an annoying one.

On the accessibility side: the button already has aria-label="Compose". The tooltip carries the same word plus the shortcut, and is linked with aria-describedby — so a screen reader announces "Compose, button, Compose. Keyboard shortcut C" rather than nothing, and voice-control users can still say "click Compose".

position-try-fallbacks: flip-inline handles the edge case: near the right edge the tooltip flips to the other side automatically, with no measurement code.

Make it yours

  • Placement: change the two anchor() edges. bottom:anchor(top) puts it above; left:anchor(right) to the right.
  • Rich tooltips: add a second line with the shortcut in a . Keep total width under 32ch or it becomes a popover, not a tip.
  • Delay: 400ms in is the desktop convention. For a single isolated FAB, 150ms feels more responsive without strobing.
  • Touch: tooltips do not exist on touch. Pair the FAB with a visible label below 640px rather than relying on hover.

Gotchas — read before shipping

  • Never put the only label in the tooltip. Icon-only buttons need aria-label; the tooltip is aria-describedby on top of it.
  • title attributes are not tooltips — they are inconsistent, unstyleable, delayed by ~1s and skipped by many screen readers.
  • A tooltip that appears under the pointer can flicker as the pointer enters it. Offset by at least 8px and set pointer-events:none.
  • Tooltips must be dismissible with Escape when triggered by keyboard focus, or they can permanently obscure content.

Browser support

ChromeSafariFirefoxEdge
125+26+131+125+

CSS anchor positioning: Chrome 125+, Safari 26+, Firefox 131+. This demo wraps anchor rules in @supports and falls back to plain absolute positioning, so the tooltip works in every browser.

Search CodeFronts

Loading…