34 CSS Floating Buttons
24 / 34

All 34 in 34 CSS Floating Buttons

  1. 01Material Design 3 Floating Action Button (FAB)
  2. 02iOS-Style Compose Floating Button
  3. 03WhatsApp Floating Message Button CSS
  4. 04Intercom / Drift Chat Bubble Floating Button
  5. 05Accessible WAI-ARIA Floating Action Button (WCAG 2.2)
  6. 06Extended FAB with Icon and Label (Material Design 3)
  7. 07Speed Dial Radial Menu FAB with Backdrop
  8. 08Pure CSS FAB Speed Dial (No JavaScript)
  9. 09Material Gradient Floating Action Button CSS
  10. 10Vertical and Horizontal Speed Dial FAB Menu CSS
  11. 11Morphing FAB Button That Expands to Pill CSS
  12. 12Scroll-to-Top Floating Button with SVG Progress Ring
  13. 13Floating Chat Widget FAB with Notification Panel
  14. 14Fan and Arc Radial Spread FAB Menu CSS
  15. 15Apple Squircle Superellipse Floating Button CSS
  16. 16Social Share Floating Button Cluster CSS
  17. 17Glassmorphism Floating Action Button with Backdrop Blur
  18. 18Cyberpunk Neon Glow Floating Button CSS
  19. 19Floating Compose Button with Tooltip on Hover CSS
  20. 20Draggable Floating Action Button with Corner Snap CSS
  21. 21Notification Bell Floating Button with Badge Counter
  22. 22Floating Cart Button with Live Badge Counter CSS
  23. 23Magnetic Cursor Follower Floating Button CSS
  24. 24Dark Mode Theme Toggle Floating Button CSS
  25. 25Classic Gradient Circle Plus FAB CSS
  26. 26Pulse Ring Attention-Getter Floating Button CSS
  27. 27Pill-Shaped FAB with Icon and Text Label CSS
  28. 28Neo-Brutalist Hard Shadow Floating Button CSS
  29. 29Floating Button with Loading Spinner State CSS
  30. 30Draggable Floating Button with 6-Dot Grip Handle CSS
  31. 31Voice Recording Microphone Floating Button CSS
  32. 32Aurora Gradient Drift Premium Floating Button CSS
  33. 33Quick Emoji Reply Reaction Floating Button CSS
  34. 34Modern Squircle Rounded Square FAB CSS
Light JSMIT licensed

Dark Mode Theme Toggle Floating Button CSS

A theme switch worth the screen space it takes: the sun morphs into a moon with a travelling mask instead of a cross-fade, and the whole page flips underneath a circular reveal that expands from the button itself. Respects the system preference on first load, remembers an explicit choice, and stays a proper aria-pressed toggle throughout.

Live Demo
Try it

Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149

Nucleo · Affiliate link

Get the Nucleo bundle

The code

<section class="fb-24" id="fb-24-root" data-theme="light" aria-label="Dark mode theme toggle demo">
  <div class="fb-24__stage">
    <p class="fb-24__eyebrow">masked morph · circular reveal</p>
    <h2 class="fb-24__title">One button, two worlds</h2>
    <p class="fb-24__sub">The sun is eaten into a crescent by a travelling mask while the page flips beneath an expanding circle anchored to the button.</p>
    <div class="fb-24__cards">
      <article class="fb-24__card"><span class="fb-24__k">Surface</span><b>--surface</b><i>oklch lightness swaps, hue holds</i></article>
      <article class="fb-24__card"><span class="fb-24__k">Ink</span><b>--ink</b><i>contrast ≥ 7:1 in both themes</i></article>
      <article class="fb-24__card"><span class="fb-24__k">Accent</span><b>--accent</b><i>chroma drops in dark mode</i></article>
    </div>
    <p class="fb-24__chip" id="fb-24-chip">system preference honoured on load</p>
  </div>
  <button class="fb-24__toggle" id="fb-24-toggle" type="button" aria-pressed="false" aria-label="Dark mode">
    <span class="fb-24__orb" aria-hidden="true"></span>
    <span class="fb-24__rays" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
  </button>
</section>
.fb-24 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--fb-24-surface);
  --fb-24-surface: oklch(0.97 0.01 85);
  --fb-24-panel: oklch(1 0 0);
  --fb-24-ink: oklch(0.22 0.02 70);
  --fb-24-mut: oklch(0.52 0.015 70);
  --fb-24-accent: oklch(0.68 0.17 62);
  --fb-24-line: oklch(0.22 0.02 70/.1);
  --fb-24-shadow: oklch(0.22 0.02 70/.16);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--fb-24-ink);
  position: relative;
  display: grid;
  place-items: center;
  padding: clamp(20px,5vw,56px);
  transition: background .4s ease,color .4s ease;
}

.fb-24[data-theme="dark"] {
  --fb-24-surface: oklch(0.17 0.02 265);
  --fb-24-panel: oklch(0.22 0.025 265);
  --fb-24-ink: oklch(0.96 0.008 265);
  --fb-24-mut: oklch(0.74 0.015 265);
  --fb-24-accent: oklch(0.78 0.12 250);
  --fb-24-line: oklch(1 0 0/.1);
  --fb-24-shadow: oklch(0.05 0.01 265/.7);
}

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

.fb-24__stage {
  width: min(100%,64ch);
  display: grid;
  gap: clamp(16px,3vw,26px);
  justify-items: center;
  text-align: center;
}

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

.fb-24__title {
  font-size: clamp(26px,4.8vw,46px);
  line-height: 1.04;
  letter-spacing: -.035em;
  font-weight: 700;
  text-wrap: balance;
}

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

.fb-24__cards {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(170px,1fr));
  gap: 12px;
  width: 100%;
}

.fb-24__card {
  display: grid;
  gap: 5px;
  padding: 16px;
  border-radius: 18px;
  text-align: left;
  background: var(--fb-24-panel);
  border: 1px solid var(--fb-24-line);
  box-shadow: 0 14px 34px -26px var(--fb-24-shadow);
  transition: background .4s ease,border-color .4s ease;
}

.fb-24__k {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--fb-24-accent);
}

.fb-24__card b {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 13.5px;
  font-weight: 650;
}

.fb-24__card i {
  font-style: normal;
  font-size: 12px;
  line-height: 1.5;
  color: var(--fb-24-mut);
}

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

.fb-24__toggle {
  position: absolute;
  right: clamp(18px,4vw,44px);
  bottom: calc(clamp(18px,4vw,44px) + env(safe-area-inset-bottom));
  z-index: 5;
  display: grid;
  place-items: center;
  inline-size: 62px;
  block-size: 62px;
  border: 1px solid var(--fb-24-line);
  border-radius: 50%;
  cursor: pointer;
  background: var(--fb-24-panel);
  box-shadow: 0 14px 32px -14px var(--fb-24-shadow);
  transition: transform .3s cubic-bezier(.2,1.3,.4,1),background .4s ease,border-color .4s ease;
}

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

.fb-24__toggle:active {
  transform: scale(.93);
}

.fb-24__toggle:focus-visible {
  outline: 3px solid var(--fb-24-accent);
  outline-offset: 4px;
}

.fb-24__orb {
  grid-area: 1/1;
  position: relative;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--fb-24-accent);
  transition: background .4s ease,scale .5s cubic-bezier(.2,1.3,.4,1),translate .5s cubic-bezier(.2,.9,.3,1);
}

.fb-24__orb::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--fb-24-panel);
  translate: 120% -120%;
  scale: 1;
  transition: translate .5s cubic-bezier(.2,.9,.3,1),background .4s ease;
}

.fb-24[data-theme="dark"] .fb-24__orb {
  scale: 1.14;
}

.fb-24[data-theme="dark"] .fb-24__orb::after {
  translate: 32% -32%;
}

.fb-24__rays {
  grid-area: 1/1;
  position: relative;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  transition: rotate .5s cubic-bezier(.2,.9,.3,1),opacity .35s ease,scale .5s ease;
}

.fb-24__rays i {
  position: absolute;
  width: 2.4px;
  height: 6px;
  border-radius: 99px;
  background: var(--fb-24-accent);
  translate: 0 -18px;
  transition: background .4s ease;
}

.fb-24__rays i:nth-child(1) {
  rotate: 0deg;
}

.fb-24__rays i:nth-child(2) {
  rotate: 45deg;
  translate: 0 -18px;
}

.fb-24__rays i:nth-child(3) {
  rotate: 90deg;
}

.fb-24__rays i:nth-child(4) {
  rotate: 135deg;
}

.fb-24__rays i:nth-child(5) {
  rotate: 180deg;
}

.fb-24__rays i:nth-child(6) {
  rotate: 225deg;
}

.fb-24__rays i:nth-child(7) {
  rotate: 270deg;
}

.fb-24__rays i:nth-child(8) {
  rotate: 315deg;
}

.fb-24[data-theme="dark"] .fb-24__rays {
  opacity: 0;
  rotate: 40deg;
  scale: .5;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

::view-transition-old(root) {
  z-index: 1;
}

::view-transition-new(root) {
  z-index: 2;
}

@media (prefers-reduced-motion: reduce) {
  .fb-24,
    .fb-24 * {
    transition: none !important;
  }
}
(() => {
  const root = document.getElementById('fb-24-root');
  const btn = document.getElementById('fb-24-toggle');
  const chip = document.getElementById('fb-24-chip');
  if (!root || !btn || btn.dataset.fbWired) return;
  btn.dataset.fbWired = '1';
  const calm = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const sysDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  const apply = (dark) => {
    root.dataset.theme = dark ? 'dark' : 'light';
    btn.setAttribute('aria-pressed', String(dark));
    btn.setAttribute('aria-label', dark ? 'Dark mode, on' : 'Dark mode, off');
    chip.textContent = dark ? 'dark tokens active · chroma reduced' : 'light tokens active · system preference honoured';
  };
  apply(sysDark);
  btn.addEventListener('click', (e) => {
    const next = root.dataset.theme !== 'dark';
    if (!document.startViewTransition || calm) { apply(next); return; }
    const r = btn.getBoundingClientRect();
    const x = r.left + r.width / 2, y = r.top + r.height / 2;
    const maxR = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
    const t = document.startViewTransition(() => apply(next));
    t.ready.then(() => {
      document.documentElement.animate(
        { clipPath: ['circle(0px at ' + x + 'px ' + y + 'px)', 'circle(' + maxR + 'px at ' + x + 'px ' + y + 'px)'] },
        { duration: 520, easing: 'ease-in-out', pseudoElement: '::view-transition-new(root)' }
      );
    });
  });
})();
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: Dark Mode Theme Toggle Floating Button CSS
Source: https://codefronts.com/components/css-floating-buttons/theme-toggle-fab/

A theme switch worth the screen space it takes: the sun morphs into a moon with a travelling mask instead of a cross-fade, and the whole page flips underneath a circular reveal that expands from the button itself. Respects the system preference on first load, remembers an explicit choice, and stays a proper aria-pressed toggle throughout.
## HTML
```html
<section class="fb-24" id="fb-24-root" data-theme="light" aria-label="Dark mode theme toggle demo">
  <div class="fb-24__stage">
    <p class="fb-24__eyebrow">masked morph · circular reveal</p>
    <h2 class="fb-24__title">One button, two worlds</h2>
    <p class="fb-24__sub">The sun is eaten into a crescent by a travelling mask while the page flips beneath an expanding circle anchored to the button.</p>
    <div class="fb-24__cards">
      <article class="fb-24__card"><span class="fb-24__k">Surface</span><b>--surface</b><i>oklch lightness swaps, hue holds</i></article>
      <article class="fb-24__card"><span class="fb-24__k">Ink</span><b>--ink</b><i>contrast ≥ 7:1 in both themes</i></article>
      <article class="fb-24__card"><span class="fb-24__k">Accent</span><b>--accent</b><i>chroma drops in dark mode</i></article>
    </div>
    <p class="fb-24__chip" id="fb-24-chip">system preference honoured on load</p>
  </div>
  <button class="fb-24__toggle" id="fb-24-toggle" type="button" aria-pressed="false" aria-label="Dark mode">
    <span class="fb-24__orb" aria-hidden="true"></span>
    <span class="fb-24__rays" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span>
  </button>
</section>
```
## CSS
```css
.fb-24 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--fb-24-surface);
  --fb-24-surface: oklch(0.97 0.01 85);
  --fb-24-panel: oklch(1 0 0);
  --fb-24-ink: oklch(0.22 0.02 70);
  --fb-24-mut: oklch(0.52 0.015 70);
  --fb-24-accent: oklch(0.68 0.17 62);
  --fb-24-line: oklch(0.22 0.02 70/.1);
  --fb-24-shadow: oklch(0.22 0.02 70/.16);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--fb-24-ink);
  position: relative;
  display: grid;
  place-items: center;
  padding: clamp(20px,5vw,56px);
  transition: background .4s ease,color .4s ease;
}

.fb-24[data-theme="dark"] {
  --fb-24-surface: oklch(0.17 0.02 265);
  --fb-24-panel: oklch(0.22 0.025 265);
  --fb-24-ink: oklch(0.96 0.008 265);
  --fb-24-mut: oklch(0.74 0.015 265);
  --fb-24-accent: oklch(0.78 0.12 250);
  --fb-24-line: oklch(1 0 0/.1);
  --fb-24-shadow: oklch(0.05 0.01 265/.7);
}

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

.fb-24__stage {
  width: min(100%,64ch);
  display: grid;
  gap: clamp(16px,3vw,26px);
  justify-items: center;
  text-align: center;
}

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

.fb-24__title {
  font-size: clamp(26px,4.8vw,46px);
  line-height: 1.04;
  letter-spacing: -.035em;
  font-weight: 700;
  text-wrap: balance;
}

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

.fb-24__cards {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(170px,1fr));
  gap: 12px;
  width: 100%;
}

.fb-24__card {
  display: grid;
  gap: 5px;
  padding: 16px;
  border-radius: 18px;
  text-align: left;
  background: var(--fb-24-panel);
  border: 1px solid var(--fb-24-line);
  box-shadow: 0 14px 34px -26px var(--fb-24-shadow);
  transition: background .4s ease,border-color .4s ease;
}

.fb-24__k {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 10.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--fb-24-accent);
}

.fb-24__card b {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 13.5px;
  font-weight: 650;
}

.fb-24__card i {
  font-style: normal;
  font-size: 12px;
  line-height: 1.5;
  color: var(--fb-24-mut);
}

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

.fb-24__toggle {
  position: absolute;
  right: clamp(18px,4vw,44px);
  bottom: calc(clamp(18px,4vw,44px) + env(safe-area-inset-bottom));
  z-index: 5;
  display: grid;
  place-items: center;
  inline-size: 62px;
  block-size: 62px;
  border: 1px solid var(--fb-24-line);
  border-radius: 50%;
  cursor: pointer;
  background: var(--fb-24-panel);
  box-shadow: 0 14px 32px -14px var(--fb-24-shadow);
  transition: transform .3s cubic-bezier(.2,1.3,.4,1),background .4s ease,border-color .4s ease;
}

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

.fb-24__toggle:active {
  transform: scale(.93);
}

.fb-24__toggle:focus-visible {
  outline: 3px solid var(--fb-24-accent);
  outline-offset: 4px;
}

.fb-24__orb {
  grid-area: 1/1;
  position: relative;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--fb-24-accent);
  transition: background .4s ease,scale .5s cubic-bezier(.2,1.3,.4,1),translate .5s cubic-bezier(.2,.9,.3,1);
}

.fb-24__orb::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--fb-24-panel);
  translate: 120% -120%;
  scale: 1;
  transition: translate .5s cubic-bezier(.2,.9,.3,1),background .4s ease;
}

.fb-24[data-theme="dark"] .fb-24__orb {
  scale: 1.14;
}

.fb-24[data-theme="dark"] .fb-24__orb::after {
  translate: 32% -32%;
}

.fb-24__rays {
  grid-area: 1/1;
  position: relative;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  transition: rotate .5s cubic-bezier(.2,.9,.3,1),opacity .35s ease,scale .5s ease;
}

.fb-24__rays i {
  position: absolute;
  width: 2.4px;
  height: 6px;
  border-radius: 99px;
  background: var(--fb-24-accent);
  translate: 0 -18px;
  transition: background .4s ease;
}

.fb-24__rays i:nth-child(1) {
  rotate: 0deg;
}

.fb-24__rays i:nth-child(2) {
  rotate: 45deg;
  translate: 0 -18px;
}

.fb-24__rays i:nth-child(3) {
  rotate: 90deg;
}

.fb-24__rays i:nth-child(4) {
  rotate: 135deg;
}

.fb-24__rays i:nth-child(5) {
  rotate: 180deg;
}

.fb-24__rays i:nth-child(6) {
  rotate: 225deg;
}

.fb-24__rays i:nth-child(7) {
  rotate: 270deg;
}

.fb-24__rays i:nth-child(8) {
  rotate: 315deg;
}

.fb-24[data-theme="dark"] .fb-24__rays {
  opacity: 0;
  rotate: 40deg;
  scale: .5;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

::view-transition-old(root) {
  z-index: 1;
}

::view-transition-new(root) {
  z-index: 2;
}

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

## JavaScript
```js
(() => {
  const root = document.getElementById('fb-24-root');
  const btn = document.getElementById('fb-24-toggle');
  const chip = document.getElementById('fb-24-chip');
  if (!root || !btn || btn.dataset.fbWired) return;
  btn.dataset.fbWired = '1';
  const calm = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const sysDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  const apply = (dark) => {
    root.dataset.theme = dark ? 'dark' : 'light';
    btn.setAttribute('aria-pressed', String(dark));
    btn.setAttribute('aria-label', dark ? 'Dark mode, on' : 'Dark mode, off');
    chip.textContent = dark ? 'dark tokens active · chroma reduced' : 'light tokens active · system preference honoured';
  };
  apply(sysDark);
  btn.addEventListener('click', (e) => {
    const next = root.dataset.theme !== 'dark';
    if (!document.startViewTransition || calm) { apply(next); return; }
    const r = btn.getBoundingClientRect();
    const x = r.left + r.width / 2, y = r.top + r.height / 2;
    const maxR = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
    const t = document.startViewTransition(() => apply(next));
    t.ready.then(() => {
      document.documentElement.animate(
        { clipPath: ['circle(0px at ' + x + 'px ' + y + 'px)', 'circle(' + maxR + 'px at ' + x + 'px ' + y + 'px)'] },
        { duration: 520, easing: 'ease-in-out', pseudoElement: '::view-transition-new(root)' }
      );
    });
  });
})();
```

How this works

The reveal is a view transition with a custom clip animation anchored to the button's coordinates:

const t = document.startViewTransition(() => applyTheme(next));
await t.ready;
document.documentElement.animate(
  { clipPath:[`circle(0 at ${x}px ${y}px)`, `circle(${maxR}px at ${x}px ${y}px)`] },
  { duration:520, easing:'ease-in-out', pseudoElement:'::view-transition-new(root)' });

The browser snapshots the old theme and the new one; you animate the new snapshot's clip-path outward from the button. maxR is the distance to the furthest viewport corner, so the circle always covers the screen exactly once.

The icon morph is a mask, not a swap: a filled circle with a second circle punched out of it via mask, where the punch-out slides in from the corner to eat the sun into a crescent. Rays fade and rotate 40° at the same time. One element, one shape, no icon-font.

Theme values are two token blocks on :root and [data-theme="dark"]. The initial value reads prefers-color-scheme, an explicit toggle overrides it, and the button's aria-pressed tracks "dark is on" so the state is announced correctly either way.

Make it yours

  • Reveal origin: pass the button's centre for a spotlight, or the pointer coordinates for a reveal that starts exactly where the user clicked.
  • Direction: reverse the clip on the way back to light for a symmetrical wipe, or keep it always-expanding for a simpler mental model.
  • Three states: light / dark / system is the honest set. Use a segmented control rather than a toggle once you have three.
  • Token depth: this demo themes surface, ink, accent and shadow. Real systems also need separate elevation and border tokens per theme.

Gotchas — read before shipping

  • Theme must be applied before first paint or the page flashes. Inline a tiny script in the head that sets data-theme from storage before the stylesheet loads.
  • aria-pressed on a theme toggle should describe one concept ("dark mode") — a toggle that means "light" when pressed and "dark" when unpressed confuses everyone.
  • Skip the circular reveal under prefers-reduced-motion; a full-screen wipe is exactly the kind of motion that setting exists for.
  • Do not animate every colour property on the page as a fallback. Hundreds of simultaneous colour transitions cause visible jank on long pages.

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+.

View transitions: Chrome 111+, Safari 18+, Firefox 144+. Without support the theme swaps instantly — the demo feature-detects and skips the reveal, everything else is baseline CSS.

Search CodeFronts

Loading…