51 CSS Buttons04 / 51

Pure CSSMIT licensed

CSS Icon-Only Button with Accessible Tooltip

A three-button icon toolbar where each control carries a real accessible name, a 44px touch target inside a 40px visual square, and a tooltip that appears on hover and keyboard focus. The tooltip animates in with @starting-style and is positioned with CSS anchor positioning, falling back to a plain absolute offset.

Published

Live Demo
Try it

The code

<div class="cb-04">
  <div class="cb-04__stage">
    <div class="cb-04__bar" role="toolbar" aria-label="Article actions">
      <span class="cb-04__item">
        <button class="cb-04__btn" type="button" aria-label="Share article" aria-describedby="cb-04-t1" style="anchor-name:--cb-04-a1">
          <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="2.6"/><circle cx="6" cy="12" r="2.6"/><circle cx="18" cy="19" r="2.6"/><path d="M8.4 10.8 15.6 6.6M8.4 13.2l7.2 4.2"/></svg>
        </button>
        <span class="cb-04__tip" role="tooltip" id="cb-04-t1" style="position-anchor:--cb-04-a1">Share</span>
      </span>
      <span class="cb-04__item">
        <button class="cb-04__btn" type="button" aria-label="Bookmark article" aria-describedby="cb-04-t2" style="anchor-name:--cb-04-a2" aria-pressed="false">
          <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h10a1 1 0 0 1 1 1v15l-6-4-6 4V5a1 1 0 0 1 1-1z"/></svg>
        </button>
        <span class="cb-04__tip" role="tooltip" id="cb-04-t2" style="position-anchor:--cb-04-a2">Save for later</span>
      </span>
      <span class="cb-04__item">
        <button class="cb-04__btn" type="button" aria-label="Copy link" aria-describedby="cb-04-t3" style="anchor-name:--cb-04-a3">
          <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 14a4 4 0 0 0 5.66 0l2.5-2.5A4 4 0 0 0 12.5 5.84L11 7.3"/><path d="M14 10a4 4 0 0 0-5.66 0l-2.5 2.5A4 4 0 0 0 11.5 18.16L13 16.7"/></svg>
        </button>
        <span class="cb-04__tip" role="tooltip" id="cb-04-t3" style="position-anchor:--cb-04-a3">Copy link</span>
      </span>
      <span class="cb-04__item">
        <button class="cb-04__btn cb-04__btn--danger" type="button" aria-label="Report article" aria-describedby="cb-04-t4" style="anchor-name:--cb-04-a4">
          <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M12 19h.01"/><path d="M12 4 3.5 19h17L12 4z"/></svg>
        </button>
        <span class="cb-04__tip" role="tooltip" id="cb-04-t4" style="position-anchor:--cb-04-a4">Report</span>
      </span>
    </div>
  </div>
</div>
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap');

.cb-04 {
  --bg: #f4f4f2;
  --surface: #ffffff;
  --text: #18181b;
  --muted: #6b6b74;
  --line: #e2e2dd;
  --accent: #2563eb;
  --danger: #dc2626;
  --tip-bg: #18181b;
  --tip-ink: #fafafa;
  --size: 2.5rem;
  --sans: "Manrope",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

@supports (color: oklch(50% 0 0)) {
  .cb-04 {
    --bg: oklch(96.8% .004 100);
    --accent: oklch(53% .21 262);
    --danger: oklch(55% .21 27);
  }
}

@media (prefers-color-scheme: dark) {
  .cb-04:not([data-theme="light"]) {
    --bg: #0b0b0d;
    --surface: #16161a;
    --text: #f2f2f5;
    --muted: #9c9ca6;
    --line: #2a2a31;
    --tip-bg: #f4f4f6;
    --tip-ink: #101014;
    --accent: #7aa2ff;
    --danger: #f87171;
  }
}

.cb-04[data-theme="dark"] {
  --bg: #0b0b0d;
  --surface: #16161a;
  --text: #f2f2f5;
  --muted: #9c9ca6;
  --line: #2a2a31;
  --tip-bg: #f4f4f6;
  --tip-ink: #101014;
  --accent: #7aa2ff;
  --danger: #f87171;
}

.cb-04,
.cb-04 *,
.cb-04 *::before,
.cb-04 *::after {
  box-sizing: border-box;
}

.cb-04__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.5rem,6vw,4rem);
}

.cb-04__bar {
  display: flex;
  align-items: center;
  gap: .35rem;
  padding: .4rem;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: 0 12px 30px -18px color-mix(in oklab,var(--text) 55%,transparent);
}

.cb-04__item {
  position: relative;
  display: inline-flex;
}

.cb-04__btn {
  position: relative;
  cursor: pointer;
  display: grid;
  place-items: center;
  width: var(--size);
  height: var(--size);
  border: 0;
  border-radius: 50%;
  color: var(--muted);
  background: transparent;
  transition: background-color .18s ease, color .18s ease, scale .2s cubic-bezier(.16,1,.3,1);
}

.cb-04__btn::after {
  content: '';
  position: absolute;
  inset: -.35rem;
  border-radius: 50%;
}

.cb-04__btn svg {
  width: 1.15rem;
  height: 1.15rem;
}

.cb-04__btn:hover {
  color: var(--accent);
  background: color-mix(in oklab,var(--accent) 12%,transparent);
  scale: 1.06;
}

.cb-04__btn--danger:hover {
  color: var(--danger);
  background: color-mix(in oklab,var(--danger) 12%,transparent);
}

.cb-04__btn:active {
  scale: .94;
}

.cb-04__btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  color: var(--accent);
}

.cb-04__btn[aria-pressed="true"] {
  color: var(--accent);
  background: color-mix(in oklab,var(--accent) 16%,transparent);
}

.cb-04__tip {
  position: absolute;
  left: 50%;
  bottom: calc(100% + .6rem);
  translate: -50% 4px;
  display: none;
  opacity: 0;
  z-index: 3;
  pointer-events: none;
  white-space: nowrap;
  padding: .35rem .6rem;
  border-radius: .4rem;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .01em;
  color: var(--tip-ink);
  background: var(--tip-bg);
  transition: opacity .18s ease, translate .18s cubic-bezier(.16,1,.3,1);
}

.cb-04__tip::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  translate: -50% -1px;
  border: .3rem solid transparent;
  border-top-color: var(--tip-bg);
}

@supports (transition-behavior: allow-discrete) {
  .cb-04__tip {
    transition: opacity .18s ease, translate .18s cubic-bezier(.16,1,.3,1), display .18s allow-discrete;
  }
}

.cb-04__item:is(:hover,:has(:focus-visible)) .cb-04__tip {
  display: block;
  opacity: 1;
  translate: -50% 0;
}

@supports (animation-timeline: view()) {
  .cb-04__item:is(:hover,:has(:focus-visible)) .cb-04__tip {
    @starting-style {
      opacity: 0;
      translate: -50% 4px;
    }
  }
}
/* Each button carries its OWN anchor-name (inline, --cb-04-a1…a4) and each tooltip
   points at it with position-anchor. One shared anchor-name here would tether every
   tooltip to the same button — the classic anchor-positioning bug. */

@supports (anchor-name: --a) {
  .cb-04__tip {
    position: fixed;
    position-area: block-start;
    justify-self: anchor-center;
    margin-bottom: .6rem;
    left: auto;
    bottom: auto;
    translate: 0 4px;
  }

  .cb-04__item:is(:hover,:has(:focus-visible)) .cb-04__tip {
    translate: 0 0;
  }

  .cb-04__tip::after {
    left: 50%;
    translate: -50% -1px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cb-04__btn,
    .cb-04__tip {
    transition: none;
  }
}

@media (forced-colors: active) {
  .cb-04__bar,
    .cb-04__btn {
    border: 1px solid ButtonText;
  }

  .cb-04__tip {
    background: Canvas;
    color: CanvasText;
    border: 1px solid CanvasText;
  }
}
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 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: CSS Icon-Only Button with Accessible Tooltip
Source: https://codefronts.com/components/css-buttons/css-icon-only-button-with-accessible-tooltip/

A three-button icon toolbar where each control carries a real accessible name, a 44px touch target inside a 40px visual square, and a tooltip that appears on hover and keyboard focus. The tooltip animates in with @starting-style and is positioned with CSS anchor positioning, falling back to a plain absolute offset.
## HTML
```html
<div class="cb-04">
  <div class="cb-04__stage">
    <div class="cb-04__bar" role="toolbar" aria-label="Article actions">
      <span class="cb-04__item">
        <button class="cb-04__btn" type="button" aria-label="Share article" aria-describedby="cb-04-t1" style="anchor-name:--cb-04-a1">
          <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="2.6"/><circle cx="6" cy="12" r="2.6"/><circle cx="18" cy="19" r="2.6"/><path d="M8.4 10.8 15.6 6.6M8.4 13.2l7.2 4.2"/></svg>
        </button>
        <span class="cb-04__tip" role="tooltip" id="cb-04-t1" style="position-anchor:--cb-04-a1">Share</span>
      </span>
      <span class="cb-04__item">
        <button class="cb-04__btn" type="button" aria-label="Bookmark article" aria-describedby="cb-04-t2" style="anchor-name:--cb-04-a2" aria-pressed="false">
          <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h10a1 1 0 0 1 1 1v15l-6-4-6 4V5a1 1 0 0 1 1-1z"/></svg>
        </button>
        <span class="cb-04__tip" role="tooltip" id="cb-04-t2" style="position-anchor:--cb-04-a2">Save for later</span>
      </span>
      <span class="cb-04__item">
        <button class="cb-04__btn" type="button" aria-label="Copy link" aria-describedby="cb-04-t3" style="anchor-name:--cb-04-a3">
          <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 14a4 4 0 0 0 5.66 0l2.5-2.5A4 4 0 0 0 12.5 5.84L11 7.3"/><path d="M14 10a4 4 0 0 0-5.66 0l-2.5 2.5A4 4 0 0 0 11.5 18.16L13 16.7"/></svg>
        </button>
        <span class="cb-04__tip" role="tooltip" id="cb-04-t3" style="position-anchor:--cb-04-a3">Copy link</span>
      </span>
      <span class="cb-04__item">
        <button class="cb-04__btn cb-04__btn--danger" type="button" aria-label="Report article" aria-describedby="cb-04-t4" style="anchor-name:--cb-04-a4">
          <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11M12 19h.01"/><path d="M12 4 3.5 19h17L12 4z"/></svg>
        </button>
        <span class="cb-04__tip" role="tooltip" id="cb-04-t4" style="position-anchor:--cb-04-a4">Report</span>
      </span>
    </div>
  </div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap');

.cb-04 {
  --bg: #f4f4f2;
  --surface: #ffffff;
  --text: #18181b;
  --muted: #6b6b74;
  --line: #e2e2dd;
  --accent: #2563eb;
  --danger: #dc2626;
  --tip-bg: #18181b;
  --tip-ink: #fafafa;
  --size: 2.5rem;
  --sans: "Manrope",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

@supports (color: oklch(50% 0 0)) {
  .cb-04 {
    --bg: oklch(96.8% .004 100);
    --accent: oklch(53% .21 262);
    --danger: oklch(55% .21 27);
  }
}

@media (prefers-color-scheme: dark) {
  .cb-04:not([data-theme="light"]) {
    --bg: #0b0b0d;
    --surface: #16161a;
    --text: #f2f2f5;
    --muted: #9c9ca6;
    --line: #2a2a31;
    --tip-bg: #f4f4f6;
    --tip-ink: #101014;
    --accent: #7aa2ff;
    --danger: #f87171;
  }
}

.cb-04[data-theme="dark"] {
  --bg: #0b0b0d;
  --surface: #16161a;
  --text: #f2f2f5;
  --muted: #9c9ca6;
  --line: #2a2a31;
  --tip-bg: #f4f4f6;
  --tip-ink: #101014;
  --accent: #7aa2ff;
  --danger: #f87171;
}

.cb-04,
.cb-04 *,
.cb-04 *::before,
.cb-04 *::after {
  box-sizing: border-box;
}

.cb-04__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.5rem,6vw,4rem);
}

.cb-04__bar {
  display: flex;
  align-items: center;
  gap: .35rem;
  padding: .4rem;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: 0 12px 30px -18px color-mix(in oklab,var(--text) 55%,transparent);
}

.cb-04__item {
  position: relative;
  display: inline-flex;
}

.cb-04__btn {
  position: relative;
  cursor: pointer;
  display: grid;
  place-items: center;
  width: var(--size);
  height: var(--size);
  border: 0;
  border-radius: 50%;
  color: var(--muted);
  background: transparent;
  transition: background-color .18s ease, color .18s ease, scale .2s cubic-bezier(.16,1,.3,1);
}

.cb-04__btn::after {
  content: '';
  position: absolute;
  inset: -.35rem;
  border-radius: 50%;
}

.cb-04__btn svg {
  width: 1.15rem;
  height: 1.15rem;
}

.cb-04__btn:hover {
  color: var(--accent);
  background: color-mix(in oklab,var(--accent) 12%,transparent);
  scale: 1.06;
}

.cb-04__btn--danger:hover {
  color: var(--danger);
  background: color-mix(in oklab,var(--danger) 12%,transparent);
}

.cb-04__btn:active {
  scale: .94;
}

.cb-04__btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  color: var(--accent);
}

.cb-04__btn[aria-pressed="true"] {
  color: var(--accent);
  background: color-mix(in oklab,var(--accent) 16%,transparent);
}

.cb-04__tip {
  position: absolute;
  left: 50%;
  bottom: calc(100% + .6rem);
  translate: -50% 4px;
  display: none;
  opacity: 0;
  z-index: 3;
  pointer-events: none;
  white-space: nowrap;
  padding: .35rem .6rem;
  border-radius: .4rem;
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .01em;
  color: var(--tip-ink);
  background: var(--tip-bg);
  transition: opacity .18s ease, translate .18s cubic-bezier(.16,1,.3,1);
}

.cb-04__tip::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  translate: -50% -1px;
  border: .3rem solid transparent;
  border-top-color: var(--tip-bg);
}

@supports (transition-behavior: allow-discrete) {
  .cb-04__tip {
    transition: opacity .18s ease, translate .18s cubic-bezier(.16,1,.3,1), display .18s allow-discrete;
  }
}

.cb-04__item:is(:hover,:has(:focus-visible)) .cb-04__tip {
  display: block;
  opacity: 1;
  translate: -50% 0;
}

@supports (animation-timeline: view()) {
  .cb-04__item:is(:hover,:has(:focus-visible)) .cb-04__tip {
    @starting-style {
      opacity: 0;
      translate: -50% 4px;
    }
  }
}
/* Each button carries its OWN anchor-name (inline, --cb-04-a1…a4) and each tooltip
   points at it with position-anchor. One shared anchor-name here would tether every
   tooltip to the same button — the classic anchor-positioning bug. */

@supports (anchor-name: --a) {
  .cb-04__tip {
    position: fixed;
    position-area: block-start;
    justify-self: anchor-center;
    margin-bottom: .6rem;
    left: auto;
    bottom: auto;
    translate: 0 4px;
  }

  .cb-04__item:is(:hover,:has(:focus-visible)) .cb-04__tip {
    translate: 0 0;
  }

  .cb-04__tip::after {
    left: 50%;
    translate: -50% -1px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cb-04__btn,
    .cb-04__tip {
    transition: none;
  }
}

@media (forced-colors: active) {
  .cb-04__bar,
    .cb-04__btn {
    border: 1px solid ButtonText;
  }

  .cb-04__tip {
    background: Canvas;
    color: CanvasText;
    border: 1px solid CanvasText;
  }
}
```

How this works

The accessible name comes from aria-label on the button; the tooltip is a separate element referenced by aria-describedby, so assistive tech reads "Bookmark, button" and then the description — never a duplicated string:

<button aria-label="Bookmark" aria-describedby="cb-04-t2">…</button>
<span role="tooltip" id="cb-04-t2">Save for later</span>

Visibility is driven by :hover and :has(:focus-visible) on the wrapper, and the enter animation works because transition-behavior:allow-discrete plus @starting-style can now transition an element out of display:none:

.cb-04__tip{
  display:none; opacity:0; translate:0 4px;
  transition:opacity .18s, translate .18s, display .18s allow-discrete;
}
.cb-04__item:is(:hover,:has(:focus-visible)) .cb-04__tip{
  display:block; opacity:1; translate:0 0;
  @starting-style{ opacity:0; translate:0 4px; }
}

Where anchor positioning is available each tooltip is tethered to its own button — every button declares a unique anchor-name and its tooltip names it back:

<button style="anchor-name:--cb-04-a2">…</button>
<span role="tooltip" style="position-anchor:--cb-04-a2">Save for later</span>

.cb-04__tip{ position:fixed; position-area:block-start;
  justify-self:anchor-center; margin-bottom:.6rem; }

Declaring one shared anchor-name on the button class instead makes every tooltip resolve to the same element, so they all appear over one button — the single most common anchor-positioning mistake. Without anchor support the tooltip stays absolutely positioned inside its own item, which is per-item correct by construction.

Make it yours

  • Move tooltips below the row by switching position-area:block-start to block-end and flipping the fallback bottom to top.
  • Add buttons freely — the toolbar is a flex row with gap, and each item is self-contained.
  • For an active/pressed icon button (mute, pin, bookmark) add aria-pressed="true" and style [aria-pressed="true"]; do not use a class alone.
  • Raise --size to 2.75rem for touch-first products; the pseudo-element hit expander then becomes unnecessary.
  • Delay the tooltip with transition-delay:.35s on show and 0s on hide so quick pointer passes stay quiet.

Gotchas — read before shipping

  • title is not a tooltip: it never appears on keyboard focus, is unreadable on touch, and can't be styled. Use a real element plus aria-describedby.
  • Never put the same text in both aria-label and the tooltip — screen readers announce it twice. Label = what it does, tooltip = extra help.
  • A tooltip that only opens on :hover excludes keyboard users; :has(:focus-visible) on the wrapper is what fixes it in pure CSS.
  • Visually a 40px square is fine, but the WCAG 2.2 target minimum is 24px and Apple/Google guidance is 44px — the ::after hit expander gets you 44px without changing the layout.
  • @starting-style only applies to the first style change after the element becomes rendered; put it inside the visible rule (as above), not on the base rule.
  • anchor-name must be unique per anchor element. Put it inline (or generate it per instance) — a single name on a shared class silently tethers every tooltip to the same button, and justify-self:anchor-center is what keeps the tip centred over its own anchor.

Browser support

ChromeSafariFirefoxEdge
125+17.5+129+125+

@starting-style + transition-behavior:allow-discrete need Chrome 117 / Safari 17.5 / Firefox 129; without them the tooltip appears instantly (still fully accessible). CSS anchor positioning is Chrome 125+ only today and is gated behind @supports with an absolute-position fallback. :has() needs Chrome 105 / Safari 15.4 / Firefox 121.

Techniques used in this demo

Search CodeFronts

Loading…