06 / 24
All 24 in 24 Tailwind Liquid Glass Navbars
- 01Tailwind iOS 26 Floating Tab Bar
- 02Tailwind macOS Tahoe Menu Bar
- 03Tailwind Safari Toolbar with SVG Refraction
- 04Tailwind visionOS Glass Navbar
- 05Tailwind Liquid Glass Bottom Accessory Bar
- 06Tailwind Dynamic Island Morphing Nav
- 07Tailwind Liquid Glass Navbar (Canonical)
- 08Tailwind Responsive Glass Navbar with Burger
- 09Tailwind Next.js Sticky Glass Header
- 10Tailwind Spring Glide Menu (Framer Motion Port)
- 11Tailwind CSS Variables Glass Theme Generator
- 12Tailwind Glass Override for Bootstrap 5 Navbars
- 13Tailwind Scroll-Opacity Sticky Navbar
- 14Tailwind Inner-Glow Glass Nav Links
- 15Tailwind Cursor-Lens Glass Nav (GSAP-ready)
- 16Tailwind Animated Gradient Blobs Glass Navbar
- 17Tailwind Sliding Capsule Tab Bar (group-has)
- 18Tailwind Reflective Glass Sidebar
- 19Tailwind Fallback-First Glass Navbar
- 20Tailwind High-Contrast Accessible Glass Navbar
- 21Tailwind Light / Dark Liquid Glass Navbar
- 22Tailwind Performance-Tuned Glass Navbar
- 23Tailwind Minimalist Dual-Border Glass Header
- 24Tailwind Responsive Glass Sidebar to Bottom Tabs
Tailwind Dynamic Island Morphing Nav
A glass pill that morphs between a slim capsule and a full navigation panel — and the clearest demonstration of why data-* variants beat class toggling. The script sets data-state to idle or expanded and manages ARIA; every geometric consequence, including the panel's delayed fade, is declared in the markup next to the resting value.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="nbtw-06 relative flex flex-col items-center w-full min-h-svh pt-15 px-5 font-nb06-sans bg-[radial-gradient(circle_at_30%_30%,#0f3d2e,#05100b_70%),url('https://picsum.photos/seed/nb06/1000/700')] bg-center bg-cover [background-blend-mode:overlay]" aria-label="Dynamic Island navigation demo">
<p class="mb-5 text-sm text-white/75">Tap the island to morph it open ↑</p>
<div class="group relative overflow-hidden w-46 h-[46px] rounded-full isolate bg-[color-mix(in_oklab,black_40%,transparent)] border border-white/[0.22] shadow-[inset_0_1px_0_rgba(255,255,255,.4),0_24px_50px_-22px_rgba(0,0,0,.8)] backdrop-blur-[28px] backdrop-saturate-[1.6] transition-[width,height,border-radius] duration-[550ms] ease-nb06 data-[state=expanded]:w-75 data-[state=expanded]:h-38 data-[state=expanded]:rounded-[30px] motion-reduce:transition-none" id="nbtw-06-island" data-state="idle">
<button class="absolute top-0 left-0 z-[2] flex items-center justify-center gap-2.5 w-full h-[46px] border-0 bg-transparent text-white text-[15px] font-bold leading-none cursor-pointer focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" id="nbtw-06-trigger" type="button" aria-expanded="false" aria-controls="nbtw-06-panel">
<span class="flex-none size-[11px] rounded-full bg-nb06-accent shadow-[0_0_12px_var(--color-nb06-accent)] animate-nb06-pulse motion-reduce:animate-none" aria-hidden="true"></span>
<span class="inline-block leading-none">Lumen</span>
</button>
<div class="absolute inset-x-0 top-[46px] bottom-0 grid grid-cols-2 gap-2 px-4 pb-4 opacity-0 pointer-events-none transition-opacity duration-200 group-data-[state=expanded]:opacity-100 group-data-[state=expanded]:pointer-events-auto group-data-[state=expanded]:duration-300 group-data-[state=expanded]:delay-200 motion-reduce:transition-none" id="nbtw-06-panel" role="group" aria-label="Quick navigation">
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 aria-[current=page]:bg-nb06-accent aria-[current=page]:text-[#04120b]" href="#nbtw-06" aria-current="page">Home</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">Search</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">Alerts</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">You</a>
</div>
</div>
</section><section class="nbtw-06 relative flex flex-col items-center w-full min-h-svh pt-15 px-5 font-nb06-sans bg-[radial-gradient(circle_at_30%_30%,#0f3d2e,#05100b_70%),url('https://picsum.photos/seed/nb06/1000/700')] bg-center bg-cover [background-blend-mode:overlay]" aria-label="Dynamic Island navigation demo">
<p class="mb-5 text-sm text-white/75">Tap the island to morph it open ↑</p>
<div class="group relative overflow-hidden w-46 h-[46px] rounded-full isolate bg-[color-mix(in_oklab,black_40%,transparent)] border border-white/[0.22] shadow-[inset_0_1px_0_rgba(255,255,255,.4),0_24px_50px_-22px_rgba(0,0,0,.8)] backdrop-blur-[28px] backdrop-saturate-[1.6] transition-[width,height,border-radius] duration-[550ms] ease-nb06 data-[state=expanded]:w-75 data-[state=expanded]:h-38 data-[state=expanded]:rounded-[30px] motion-reduce:transition-none" id="nbtw-06-island" data-state="idle">
<button class="absolute top-0 left-0 z-[2] flex items-center justify-center gap-2.5 w-full h-[46px] border-0 bg-transparent text-white text-[15px] font-bold leading-none cursor-pointer focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" id="nbtw-06-trigger" type="button" aria-expanded="false" aria-controls="nbtw-06-panel">
<span class="flex-none size-[11px] rounded-full bg-nb06-accent shadow-[0_0_12px_var(--color-nb06-accent)] animate-nb06-pulse motion-reduce:animate-none" aria-hidden="true"></span>
<span class="inline-block leading-none">Lumen</span>
</button>
<div class="absolute inset-x-0 top-[46px] bottom-0 grid grid-cols-2 gap-2 px-4 pb-4 opacity-0 pointer-events-none transition-opacity duration-200 group-data-[state=expanded]:opacity-100 group-data-[state=expanded]:pointer-events-auto group-data-[state=expanded]:duration-300 group-data-[state=expanded]:delay-200 motion-reduce:transition-none" id="nbtw-06-panel" role="group" aria-label="Quick navigation">
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 aria-[current=page]:bg-nb06-accent aria-[current=page]:text-[#04120b]" href="#nbtw-06" aria-current="page">Home</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">Search</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">Alerts</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">You</a>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-nb06-accent: #3fd18b;
--color-nb06-accent: oklch(0.7 0.18 150);
--font-nb06-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb06: cubic-bezier(0.34, 1.4, 0.5, 1);
--animate-nb06-pulse: nb06-pulse 2.4s ease-in-out infinite;
}
@keyframes nb06-pulse {
50% {
transform: scale(1.35);
opacity: 0.6;
}
}@import "tailwindcss";
@theme {
--color-nb06-accent: #3fd18b;
--color-nb06-accent: oklch(0.7 0.18 150);
--font-nb06-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb06: cubic-bezier(0.34, 1.4, 0.5, 1);
--animate-nb06-pulse: nb06-pulse 2.4s ease-in-out infinite;
}
@keyframes nb06-pulse {
50% {
transform: scale(1.35);
opacity: 0.6;
}
}(() => {
const island = document.querySelector('#nbtw-06-island');
const trigger = document.querySelector('#nbtw-06-trigger');
const panel = document.querySelector('#nbtw-06-panel');
if (!island || !trigger || !panel) return;
const set = (open) => {
island.dataset.state = open ? 'expanded' : 'idle';
trigger.setAttribute('aria-expanded', String(open));
if (open) panel.querySelector('a').focus();
};
trigger.addEventListener('click', () => set(island.dataset.state !== 'expanded'));
island.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { set(false); trigger.focus(); }
});
})();(() => {
const island = document.querySelector('#nbtw-06-island');
const trigger = document.querySelector('#nbtw-06-trigger');
const panel = document.querySelector('#nbtw-06-panel');
if (!island || !trigger || !panel) return;
const set = (open) => {
island.dataset.state = open ? 'expanded' : 'idle';
trigger.setAttribute('aria-expanded', String(open));
if (open) panel.querySelector('a').focus();
};
trigger.addEventListener('click', () => set(island.dataset.state !== 'expanded'));
island.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { set(false); trigger.focus(); }
});
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-nb06-accent: #3fd18b;
--color-nb06-accent: oklch(0.7 0.18 150);
--font-nb06-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb06: cubic-bezier(0.34, 1.4, 0.5, 1);
--animate-nb06-pulse: nb06-pulse 2.4s ease-in-out infinite;
}
@keyframes nb06-pulse {
50% {
transform: scale(1.35);
opacity: 0.6;
}
}@import "tailwindcss";
@theme {
--color-nb06-accent: #3fd18b;
--color-nb06-accent: oklch(0.7 0.18 150);
--font-nb06-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb06: cubic-bezier(0.34, 1.4, 0.5, 1);
--animate-nb06-pulse: nb06-pulse 2.4s ease-in-out infinite;
}
@keyframes nb06-pulse {
50% {
transform: scale(1.35);
opacity: 0.6;
}
}What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The script sets one attribute and the ARIA that goes with it. Every pixel of the morph — width, height, radius, the panel's fade and its delay — is declared in the markup as a variant of that attribute.
- Both geometries sit in one class list, which is what makes the morph reviewable: you can see the resting and expanded numbers side by side instead of diffing two rule blocks.
group-data-[state=expanded]:delay-200is the whole trick behind the shape reading as liquid: the container moves first, the contents arrive after.- The expanded size is hard-coded because transitions need resolvable numbers at both ends.
autowould snap, and that is a CSS constraint the port cannot remove. pointer-events-noneat rest,group-data-[state=expanded]:pointer-events-autowhen open — otherwise the collapsed panel intercepts clicks aimed at the pill.- Adding a third state costs an attribute value and one
data-[state=...]set. No new selectors, which is the practical argument for state-as-data over state-as-class.
Here's a working CSS demo 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: Tailwind Dynamic Island Morphing Nav
Source: https://codefronts.com/navigation/tailwind-liquid-glass-navbars/tailwind-dynamic-island-morphing-nav/
A glass pill that morphs between a slim capsule and a full navigation panel — and the clearest demonstration of why data-* variants beat class toggling. The script sets data-state to idle or expanded and manages ARIA; every geometric consequence, including the panel's delayed fade, is declared in the markup next to the resting value.
## HTML
```html
<section class="nbtw-06 relative flex flex-col items-center w-full min-h-svh pt-15 px-5 font-nb06-sans bg-[radial-gradient(circle_at_30%_30%,#0f3d2e,#05100b_70%),url('https://picsum.photos/seed/nb06/1000/700')] bg-center bg-cover [background-blend-mode:overlay]" aria-label="Dynamic Island navigation demo">
<p class="mb-5 text-sm text-white/75">Tap the island to morph it open ↑</p>
<div class="group relative overflow-hidden w-46 h-[46px] rounded-full isolate bg-[color-mix(in_oklab,black_40%,transparent)] border border-white/[0.22] shadow-[inset_0_1px_0_rgba(255,255,255,.4),0_24px_50px_-22px_rgba(0,0,0,.8)] backdrop-blur-[28px] backdrop-saturate-[1.6] transition-[width,height,border-radius] duration-[550ms] ease-nb06 data-[state=expanded]:w-75 data-[state=expanded]:h-38 data-[state=expanded]:rounded-[30px] motion-reduce:transition-none" id="nbtw-06-island" data-state="idle">
<button class="absolute top-0 left-0 z-[2] flex items-center justify-center gap-2.5 w-full h-[46px] border-0 bg-transparent text-white text-[15px] font-bold leading-none cursor-pointer focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" id="nbtw-06-trigger" type="button" aria-expanded="false" aria-controls="nbtw-06-panel">
<span class="flex-none size-[11px] rounded-full bg-nb06-accent shadow-[0_0_12px_var(--color-nb06-accent)] animate-nb06-pulse motion-reduce:animate-none" aria-hidden="true"></span>
<span class="inline-block leading-none">Lumen</span>
</button>
<div class="absolute inset-x-0 top-[46px] bottom-0 grid grid-cols-2 gap-2 px-4 pb-4 opacity-0 pointer-events-none transition-opacity duration-200 group-data-[state=expanded]:opacity-100 group-data-[state=expanded]:pointer-events-auto group-data-[state=expanded]:duration-300 group-data-[state=expanded]:delay-200 motion-reduce:transition-none" id="nbtw-06-panel" role="group" aria-label="Quick navigation">
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 aria-[current=page]:bg-nb06-accent aria-[current=page]:text-[#04120b]" href="#nbtw-06" aria-current="page">Home</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">Search</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">Alerts</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">You</a>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-nb06-accent: #3fd18b;
--color-nb06-accent: oklch(0.7 0.18 150);
--font-nb06-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb06: cubic-bezier(0.34, 1.4, 0.5, 1);
--animate-nb06-pulse: nb06-pulse 2.4s ease-in-out infinite;
}
@keyframes nb06-pulse {
50% {
transform: scale(1.35);
opacity: 0.6;
}
}
```
## JavaScript
```js
(() => {
const island = document.querySelector('#nbtw-06-island');
const trigger = document.querySelector('#nbtw-06-trigger');
const panel = document.querySelector('#nbtw-06-panel');
if (!island || !trigger || !panel) return;
const set = (open) => {
island.dataset.state = open ? 'expanded' : 'idle';
trigger.setAttribute('aria-expanded', String(open));
if (open) panel.querySelector('a').focus();
};
trigger.addEventListener('click', () => set(island.dataset.state !== 'expanded'));
island.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { set(false); trigger.focus(); }
});
})();
```Here's a working CSS demo 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: Tailwind Dynamic Island Morphing Nav
Source: https://codefronts.com/navigation/tailwind-liquid-glass-navbars/tailwind-dynamic-island-morphing-nav/
A glass pill that morphs between a slim capsule and a full navigation panel — and the clearest demonstration of why data-* variants beat class toggling. The script sets data-state to idle or expanded and manages ARIA; every geometric consequence, including the panel's delayed fade, is declared in the markup next to the resting value.
## HTML
```html
<section class="nbtw-06 relative flex flex-col items-center w-full min-h-svh pt-15 px-5 font-nb06-sans bg-[radial-gradient(circle_at_30%_30%,#0f3d2e,#05100b_70%),url('https://picsum.photos/seed/nb06/1000/700')] bg-center bg-cover [background-blend-mode:overlay]" aria-label="Dynamic Island navigation demo">
<p class="mb-5 text-sm text-white/75">Tap the island to morph it open ↑</p>
<div class="group relative overflow-hidden w-46 h-[46px] rounded-full isolate bg-[color-mix(in_oklab,black_40%,transparent)] border border-white/[0.22] shadow-[inset_0_1px_0_rgba(255,255,255,.4),0_24px_50px_-22px_rgba(0,0,0,.8)] backdrop-blur-[28px] backdrop-saturate-[1.6] transition-[width,height,border-radius] duration-[550ms] ease-nb06 data-[state=expanded]:w-75 data-[state=expanded]:h-38 data-[state=expanded]:rounded-[30px] motion-reduce:transition-none" id="nbtw-06-island" data-state="idle">
<button class="absolute top-0 left-0 z-[2] flex items-center justify-center gap-2.5 w-full h-[46px] border-0 bg-transparent text-white text-[15px] font-bold leading-none cursor-pointer focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" id="nbtw-06-trigger" type="button" aria-expanded="false" aria-controls="nbtw-06-panel">
<span class="flex-none size-[11px] rounded-full bg-nb06-accent shadow-[0_0_12px_var(--color-nb06-accent)] animate-nb06-pulse motion-reduce:animate-none" aria-hidden="true"></span>
<span class="inline-block leading-none">Lumen</span>
</button>
<div class="absolute inset-x-0 top-[46px] bottom-0 grid grid-cols-2 gap-2 px-4 pb-4 opacity-0 pointer-events-none transition-opacity duration-200 group-data-[state=expanded]:opacity-100 group-data-[state=expanded]:pointer-events-auto group-data-[state=expanded]:duration-300 group-data-[state=expanded]:delay-200 motion-reduce:transition-none" id="nbtw-06-panel" role="group" aria-label="Quick navigation">
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 aria-[current=page]:bg-nb06-accent aria-[current=page]:text-[#04120b]" href="#nbtw-06" aria-current="page">Home</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">Search</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">Alerts</a>
<a class="flex items-center justify-center min-h-10 rounded-xl no-underline text-white text-sm font-semibold bg-white/10 border border-white/[0.16] transition-colors duration-200 hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-06">You</a>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-nb06-accent: #3fd18b;
--color-nb06-accent: oklch(0.7 0.18 150);
--font-nb06-sans: system-ui, 'Segoe UI', sans-serif;
--ease-nb06: cubic-bezier(0.34, 1.4, 0.5, 1);
--animate-nb06-pulse: nb06-pulse 2.4s ease-in-out infinite;
}
@keyframes nb06-pulse {
50% {
transform: scale(1.35);
opacity: 0.6;
}
}
```
## JavaScript
```js
(() => {
const island = document.querySelector('#nbtw-06-island');
const trigger = document.querySelector('#nbtw-06-trigger');
const panel = document.querySelector('#nbtw-06-panel');
if (!island || !trigger || !panel) return;
const set = (open) => {
island.dataset.state = open ? 'expanded' : 'idle';
trigger.setAttribute('aria-expanded', String(open));
if (open) panel.querySelector('a').focus();
};
trigger.addEventListener('click', () => set(island.dataset.state !== 'expanded'));
island.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { set(false); trigger.focus(); }
});
})();
```How this works
The state is an attribute, and the design is a variant of it. Both geometries are typed on the same element, so the morph is readable as a diff:
class="group w-46 h-[46px] rounded-full
transition-[width,height,border-radius] duration-[550ms] ease-nb06
data-[state=expanded]:w-75
data-[state=expanded]:h-38
data-[state=expanded]:rounded-[30px]"Fixed values, not auto. A transition needs two resolvable numbers at both ends. This is why both states carry explicit width and height utilities — w-auto or h-auto would simply snap.
The panel is deliberately late. group-data-[state=expanded]:delay-200 holds the contents back until the container has largely finished travelling, which is the difference between a shape that morphs and a box that grows with text sliding around inside it.
Keyboard users get the same morph. The trigger is a button carrying aria-expanded; opening moves focus to the first action, and Escape collapses and returns focus to the trigger. The collapsed pill is fully operable on its own, so nothing depends on the animation.
Make it yours
- Add states by extending the attribute (
data-state="call") and typing a matchingdata-[state=call]:set — no new selectors, no new stylesheet. - Tune the morph feel with
duration-[550ms]and--ease-nb06; a lower overshoot in the curve reads as glass, a higher one as rubber. - Anchor it at the top centre (
top-2 left-1/2 -translate-x-1/2 fixed) for a true notch-island position. - Change the expanded footprint by editing the two
data-[state=expanded]size utilities; the panel grid adapts on its own. - Give the panel its own entrance by adding
group-data-[state=expanded]:translate-y-0against a restingtranslate-y-1.
Gotchas — read before shipping
- Animate fixed dimensions, never
auto. This is the single reason the expanded size is hard-coded rather than content-derived. - Keep the collapsed pill a real labelled button. If the only way in is a hover-revealed control, the component is broken before the animation even runs.
- Return focus to the trigger on collapse, or a keyboard user is left focused on a
pointer-events-nonepanel. group-data-*needsgroupon the island, not on the section — a mis-scoped group is the usual cause of a panel that never appears.- Width and height are layout properties. This morph is cheap because the island is small; do not scale the same pattern to a full-screen sheet.
- The pulse is an infinite loop on resting chrome. It stays small and slow for that reason, and
motion-reduce:animate-nonestops it outright.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Tailwind v4 sets the floor. The morph is progressive enhancement: with no JS the island stays in its resting state as a labelled button, and the panel links remain reachable in the DOM order.