Tailwind v4MIT licensed

Tailwind Reflective Glass Sidebar

A vertical glass sidebar that samples the colour of whatever panel is in view: scroll, and its tint, top glow and active accent shift as though the glass were picking up the light of the room behind it. The port keeps the reflected colour as a registered custom property, which is the one thing that makes the cross-fade smooth instead of a snap.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="nbtw-18 relative flex w-full min-h-svh overflow-hidden font-nb18-sans bg-[#0a0c14]" aria-label="Reflective glass sidebar demo">
  <nav class="relative z-[5] flex flex-col items-center gap-2.5 w-[74px] py-4.5 overflow-hidden text-white bg-[color-mix(in_oklab,var(--nb18-reflect)_16%,rgba(255,255,255,.06))] border-e border-e-[color-mix(in_oklab,var(--nb18-reflect)_40%,rgba(255,255,255,.14))] shadow-[inset_0_1px_0_rgba(255,255,255,.3)] backdrop-blur-[24px] backdrop-saturate-[1.8] transition-[background-color,border-color] duration-500 motion-reduce:transition-none" id="nbtw-18-side" aria-label="Primary">
    <span class="absolute inset-x-0 top-0 h-30 pointer-events-none opacity-60 bg-[radial-gradient(60px_120px_at_50%_0,color-mix(in_oklab,var(--nb18-reflect)_80%,transparent),transparent_70%)] transition-[background] duration-500 motion-reduce:transition-none" aria-hidden="true"></span>
    <span class="relative z-[1] mb-2 text-[22px] text-[color-mix(in_oklab,var(--nb18-reflect)_60%,white)] transition-colors duration-500" aria-hidden="true">◈</span>
    <a class="relative z-[1] grid place-items-center size-[46px] rounded-[14px] no-underline text-white/80 text-[19px] transition-[background-color,color] duration-300 hover:bg-white/[0.12] hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 aria-[current=page]:text-white aria-[current=page]:bg-[color-mix(in_oklab,var(--nb18-reflect)_55%,transparent)] aria-[current=page]:shadow-[0_0_18px_-2px_color-mix(in_oklab,var(--nb18-reflect)_70%,transparent)]" href="#nbtw-18" aria-current="page" aria-label="Home">⌂</a>
    <a class="relative z-[1] grid place-items-center size-[46px] rounded-[14px] no-underline text-white/80 text-[19px] transition-[background-color,color] duration-300 hover:bg-white/[0.12] hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-18" aria-label="Discover">◎</a>
    <a class="relative z-[1] grid place-items-center size-[46px] rounded-[14px] no-underline text-white/80 text-[19px] transition-[background-color,color] duration-300 hover:bg-white/[0.12] hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-18" aria-label="Media">▤</a>
    <a class="relative z-[1] grid place-items-center size-[46px] rounded-[14px] no-underline text-white/80 text-[19px] transition-[background-color,color] duration-300 hover:bg-white/[0.12] hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-18" aria-label="Settings">⚙</a>
  </nav>
  <div class="flex-1 h-svh overflow-y-auto snap-y snap-mandatory" id="nbtw-18-scroll">
    <div class="h-svh min-h-130 snap-start grid place-items-center bg-[radial-gradient(circle_at_50%_40%,color-mix(in_oklab,#5b8cff_55%,#0a0c14),#0a0c14_75%)]" data-color="#5b8cff"><h3 class="text-white text-[32px] font-extrabold tracking-[-0.02em] [text-shadow:0_2px_20px_rgba(0,0,0,.4)]">Ocean</h3></div>
    <div class="h-svh min-h-130 snap-start grid place-items-center bg-[radial-gradient(circle_at_50%_40%,color-mix(in_oklab,#ff5ea8_55%,#0a0c14),#0a0c14_75%)]" data-color="#ff5ea8"><h3 class="text-white text-[32px] font-extrabold tracking-[-0.02em] [text-shadow:0_2px_20px_rgba(0,0,0,.4)]">Bloom</h3></div>
    <div class="h-svh min-h-130 snap-start grid place-items-center bg-[radial-gradient(circle_at_50%_40%,color-mix(in_oklab,#38e8c6_55%,#0a0c14),#0a0c14_75%)]" data-color="#38e8c6"><h3 class="text-white text-[32px] font-extrabold tracking-[-0.02em] [text-shadow:0_2px_20px_rgba(0,0,0,.4)]">Mint</h3></div>
    <div class="h-svh min-h-130 snap-start grid place-items-center bg-[radial-gradient(circle_at_50%_40%,color-mix(in_oklab,#ffb03a_55%,#0a0c14),#0a0c14_75%)]" data-color="#ffb03a"><h3 class="text-white text-[32px] font-extrabold tracking-[-0.02em] [text-shadow:0_2px_20px_rgba(0,0,0,.4)]">Amber</h3></div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --font-nb18-sans: system-ui, 'Segoe UI', sans-serif;
}
/* The load-bearing rule of this demo. An unregistered custom property holding
   a colour is an untyped string: a new value swaps instantly and no transition
   can catch it. Registered as <color>, the browser interpolates. */

@property --nb18-reflect {
  syntax: '<color>';
  inherits: true;
  initial-value: #5b8cff;
}
(() => {
  const side = document.querySelector('#nbtw-18-side');
  const scroll = document.querySelector('#nbtw-18-scroll');
  if (!side || !scroll) return;
  const io = new IntersectionObserver((entries) => {
    entries.forEach((e) => {
      if (e.isIntersecting) side.style.setProperty('--nb18-reflect', e.target.dataset.color);
    });
  }, { root: scroll, threshold: 0.6 });
  scroll.querySelectorAll('[data-color]').forEach((p) => io.observe(p));
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --font-nb18-sans: system-ui, 'Segoe UI', sans-serif;
}
/* The load-bearing rule of this demo. An unregistered custom property holding
   a colour is an untyped string: a new value swaps instantly and no transition
   can catch it. Registered as <color>, the browser interpolates. */

@property --nb18-reflect {
  syntax: '<color>';
  inherits: true;
  initial-value: #5b8cff;
}

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • @property with syntax: '<color>' is the whole demo. Unregistered, the reflected colour would snap on every panel change and no transition could smooth it.
  • Five utilities across three elements all derive from var(--nb18-reflect) through color-mix(), so one property write re-tints the fill, the border, the glow, the logo and the active item together.
  • inherits: true matters because the derived utilities sit on descendants of the element the property is written to.
  • The observer replaces scroll maths entirely: four lines, no layout reads, and a threshold that reports the panel actually in view.
  • Mix percentages are kept low (16% fill, 55% active) so no panel colour can push the white icons below a readable contrast ratio.
  • snap-y snap-mandatory gives each colour a settled moment, which is what makes the reflection read as sampling a room rather than smearing between two.
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 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 Reflective Glass Sidebar
Source: https://codefronts.com/navigation/tailwind-liquid-glass-navbars/tailwind-reflective-glass-sidebar/

A vertical glass sidebar that samples the colour of whatever panel is in view: scroll, and its tint, top glow and active accent shift as though the glass were picking up the light of the room behind it. The port keeps the reflected colour as a registered custom property, which is the one thing that makes the cross-fade smooth instead of a snap.
## HTML
```html
<section class="nbtw-18 relative flex w-full min-h-svh overflow-hidden font-nb18-sans bg-[#0a0c14]" aria-label="Reflective glass sidebar demo">
  <nav class="relative z-[5] flex flex-col items-center gap-2.5 w-[74px] py-4.5 overflow-hidden text-white bg-[color-mix(in_oklab,var(--nb18-reflect)_16%,rgba(255,255,255,.06))] border-e border-e-[color-mix(in_oklab,var(--nb18-reflect)_40%,rgba(255,255,255,.14))] shadow-[inset_0_1px_0_rgba(255,255,255,.3)] backdrop-blur-[24px] backdrop-saturate-[1.8] transition-[background-color,border-color] duration-500 motion-reduce:transition-none" id="nbtw-18-side" aria-label="Primary">
    <span class="absolute inset-x-0 top-0 h-30 pointer-events-none opacity-60 bg-[radial-gradient(60px_120px_at_50%_0,color-mix(in_oklab,var(--nb18-reflect)_80%,transparent),transparent_70%)] transition-[background] duration-500 motion-reduce:transition-none" aria-hidden="true"></span>
    <span class="relative z-[1] mb-2 text-[22px] text-[color-mix(in_oklab,var(--nb18-reflect)_60%,white)] transition-colors duration-500" aria-hidden="true">◈</span>
    <a class="relative z-[1] grid place-items-center size-[46px] rounded-[14px] no-underline text-white/80 text-[19px] transition-[background-color,color] duration-300 hover:bg-white/[0.12] hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2 aria-[current=page]:text-white aria-[current=page]:bg-[color-mix(in_oklab,var(--nb18-reflect)_55%,transparent)] aria-[current=page]:shadow-[0_0_18px_-2px_color-mix(in_oklab,var(--nb18-reflect)_70%,transparent)]" href="#nbtw-18" aria-current="page" aria-label="Home">⌂</a>
    <a class="relative z-[1] grid place-items-center size-[46px] rounded-[14px] no-underline text-white/80 text-[19px] transition-[background-color,color] duration-300 hover:bg-white/[0.12] hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-18" aria-label="Discover">◎</a>
    <a class="relative z-[1] grid place-items-center size-[46px] rounded-[14px] no-underline text-white/80 text-[19px] transition-[background-color,color] duration-300 hover:bg-white/[0.12] hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-18" aria-label="Media">▤</a>
    <a class="relative z-[1] grid place-items-center size-[46px] rounded-[14px] no-underline text-white/80 text-[19px] transition-[background-color,color] duration-300 hover:bg-white/[0.12] hover:text-white focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2" href="#nbtw-18" aria-label="Settings">⚙</a>
  </nav>
  <div class="flex-1 h-svh overflow-y-auto snap-y snap-mandatory" id="nbtw-18-scroll">
    <div class="h-svh min-h-130 snap-start grid place-items-center bg-[radial-gradient(circle_at_50%_40%,color-mix(in_oklab,#5b8cff_55%,#0a0c14),#0a0c14_75%)]" data-color="#5b8cff"><h3 class="text-white text-[32px] font-extrabold tracking-[-0.02em] [text-shadow:0_2px_20px_rgba(0,0,0,.4)]">Ocean</h3></div>
    <div class="h-svh min-h-130 snap-start grid place-items-center bg-[radial-gradient(circle_at_50%_40%,color-mix(in_oklab,#ff5ea8_55%,#0a0c14),#0a0c14_75%)]" data-color="#ff5ea8"><h3 class="text-white text-[32px] font-extrabold tracking-[-0.02em] [text-shadow:0_2px_20px_rgba(0,0,0,.4)]">Bloom</h3></div>
    <div class="h-svh min-h-130 snap-start grid place-items-center bg-[radial-gradient(circle_at_50%_40%,color-mix(in_oklab,#38e8c6_55%,#0a0c14),#0a0c14_75%)]" data-color="#38e8c6"><h3 class="text-white text-[32px] font-extrabold tracking-[-0.02em] [text-shadow:0_2px_20px_rgba(0,0,0,.4)]">Mint</h3></div>
    <div class="h-svh min-h-130 snap-start grid place-items-center bg-[radial-gradient(circle_at_50%_40%,color-mix(in_oklab,#ffb03a_55%,#0a0c14),#0a0c14_75%)]" data-color="#ffb03a"><h3 class="text-white text-[32px] font-extrabold tracking-[-0.02em] [text-shadow:0_2px_20px_rgba(0,0,0,.4)]">Amber</h3></div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --font-nb18-sans: system-ui, 'Segoe UI', sans-serif;
}
/* The load-bearing rule of this demo. An unregistered custom property holding
   a colour is an untyped string: a new value swaps instantly and no transition
   can catch it. Registered as <color>, the browser interpolates. */

@property --nb18-reflect {
  syntax: '<color>';
  inherits: true;
  initial-value: #5b8cff;
}
```

## JavaScript
```js
(() => {
  const side = document.querySelector('#nbtw-18-side');
  const scroll = document.querySelector('#nbtw-18-scroll');
  if (!side || !scroll) return;
  const io = new IntersectionObserver((entries) => {
    entries.forEach((e) => {
      if (e.isIntersecting) side.style.setProperty('--nb18-reflect', e.target.dataset.color);
    });
  }, { root: scroll, threshold: 0.6 });
  scroll.querySelectorAll('[data-color]').forEach((p) => io.observe(p));
})();
```

How this works

Registration is the whole demo. An unregistered custom property holding a colour is an untyped string: assigning a new value swaps it instantly and no transition can catch it. Registered as a colour, the browser interpolates:

@property --nb18-reflect {
  syntax: '<color>';
  inherits: true;
  initial-value: #5b8cff;
}

Four utilities read one property. The sidebar's fill, its trailing border, the top glow layer and the active item's background and halo are all color-mix() derivations of var(--nb18-reflect), so a single write re-tints the entire component coherently.

Observation beats measurement. An IntersectionObserver on the panels reports which one is centred at a 0.6 threshold, with no scroll listener and no layout reads. The handler is four lines and does nothing but setProperty.

Contrast has to hold across every colour. The icons stay white with their own opacity ramp, and the reflected colour is mixed at 16% for the fill and 55% for the active item — low enough that no panel colour can push the icons below a readable ratio.

Make it yours

  • Feed real dominant colours in — extract from an image on load and write the same property; nothing else changes.
  • Strengthen the reflection by raising the mix percentages in the fill and glow; keep an eye on icon contrast as you do.
  • Add a moving highlight that also reads --nb18-reflect for a sense of light travelling across the pane.
  • Switch to horizontal by making the sidebar a top bar; the glow layer's gradient is the only thing that needs reorienting.
  • Lengthen the cross-fade with duration-700 for a slower, more liquid sample.

Gotchas — read before shipping

  • Register the property as <color> or the tint snaps. This is the entire difference between a reflective sidebar and a flickering one.
  • Use an IntersectionObserver, not scroll maths. Scroll-derived section detection is both less accurate and more expensive.
  • Keep text and icon contrast valid across every reflected colour, including the brightest panel — test the worst case, not the default.
  • The glow layer must be pointer-events-none; it covers the top of the sidebar including the logo area.
  • inherits: true on the registration matters, because the derived utilities live on descendants of the element the property is written to.
  • A scroll-snap container with panels this tall needs a real height; without it the observer thresholds never resolve cleanly.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Tailwind v4 sets the floor, and @property support (Chrome 85+, Safari 16.4+, Firefox 128+) sits inside it. Without JS the sidebar keeps its initial tint and stays fully usable.

Techniques used in this demo

Plain CSSno Tailwind surface

Search CodeFronts

Loading…