
Loading the page…
Loading the page…
How Velvet Sidebar keeps its frosted glass clean and its first fold smooth, and five traps: blend modes over backdrop-filter, layer promotion, WebGL in React's dev effects, GLSL and lint.
Written with AI: Claude Opus 5.5, through Claude
5 lessons learned building it
The product
Pro plan · Component
A cinema-grade glass sidebar for React: a red active bar that travels between rows, a collapse into a monogram rail, notifications, a profile switcher, search, preview flyouts, light/dark/auto, five accents, RTL and a phone drawer.
npx shadcn@latest add @upfork/glass-collapsible-sidebar-reactVelvet Sidebar is a cinema-grade glass sidebar for React, made for a streaming or media app: a red active bar that travels between rows, a collapse into a monogram rail, notifications, a profile switcher with PINs, search with recent and trending queries, preview flyouts, light, dark and auto, five accents, right to left, and a drawer on phones. It's plain CSS with --vs-* tokens, so it needs no Tailwind.
This article is for anyone, person or AI, building frosted-glass UI or a WebGL background in React: five traps we hit, each with its fix and the code that holds it.
The sidebar's material is a gradient over backdrop-filter: blur(28px) saturate(140%), with a film of grain and a moving sheen on top.
Collapsing the sidebar fades the labels, the search field, the section names and the wordmark out while the panel narrows to its rail.
The showcase demo sits on a slowly drifting silk backdrop, drawn by a small WebGL shader, with a CSS stand-in for browsers where WebGL fails.
12 unit tests; ESLint with the React Compiler rules, TypeScript and next build clean; axe finds no violations on 9 screens (both themes, the rail and every popover); every label's rendered contrast is at least 4.5:1; and Chrome on an Intel laptop holds 60 fps for the active bar's travel, hovering and folding. On UpFork it passed the importer and the preview builder with all three demos.
<VelvetSidebar /> takes your brand and sections of items (icons, badges, progress, previews, shortcuts), with value, collapsed and the active profile each controlled or uncontrolled, profiles with an optional verifyPin, notifications, search and onLogout. An accent from five presets (or your own colour), light, dark or auto, right to left, and labels for every string.
npx shadcn@latest add @upfork/glass-collapsible-sidebar-reactInstalls with your UpFork access token once you have it: see how on its page.
Written by
Ramin ValadanProved GitHub account and website, confirmed e-mail: checked by UpFork
Full-stack developer and founder of UpFork · .NET, React/Next.js and WebGL
This version is signed in UpFork’s ledger as entry #191, so anyone can check it hasn’t changed since it was published. Check it.
Something wrong in it, or against the rules? Report it.

Behind the build · Duet Sidebar — Glassmorphism Icon Dock & Expandable Menu for React
How Duet Sidebar's dock and menu share state without React renders, and five traps: light-dark() with numbers, self-hosted demo fonts, compiler lint, dev double effects and exiting views.
Ramin ValadanProved GitHub account and website, confirmed e-mail: checked by UpFork5 min readAI
5 lessons


Behind the build · Perspective Cards — 3D Parallax Image Gallery Hero for React & Framer Motion
How Perspective Cards floats image cards in 3D around a headline with Motion values, and five traps: React Compiler lint, flex min-width, mobile LCP, Playwright clicks and bundle sizes.
Ramin ValadanProved GitHub account and website, confirmed e-mail: checked by UpFork5 min readAI
5 lessons


Behind the build · Dynamic 3D Carousel for React & Next.js — Draggable Image & Video Slider
How Dynamic Carousel drives every card from one motion value: flick physics, a loop without clones, a 3D first paint that needs no script, and the useTransform trap Next.js hid from us.
Ramin ValadanProved GitHub account and website, confirmed e-mail: checked by UpFork11 min readAI
6 lessons
everything that fades when the panel folds carries will-change: opacity, transform from the start, so its layer already exists. The first fold then runs at 60 fps like the rest.
velvet-sidebar.css lines 327–333.
in development React runs every effect twice: run, clean up, run. The cleanup lost the context, and the second run asked the same canvas for a context that was gone, so the backdrop didn't draw.
the cleanup frees what the effect made (the buffer and the program) and keeps the context, so the same canvas can mount again.
components/demo/silk-backdrop.tsx lines 202–208.
the variable was renamed. Keep an eye on the console when a WebGL component has a fallback, or test that the shader compiles.
components/demo/silk-backdrop.tsx lines 136–146; the console output in the build notes.
the React Compiler's lint rules flag all three. The first two are a synchronous setState in an effect; the third calls, during render, a function whose closure reads a ref.
the rolling badge keeps the previous value in state and adjusts during render when it changes (the "previous value" pattern). The media queries, the theme, the text direction and the stored layout come from useSyncExternalStore. Popovers get their anchor as a getter, a function that is called only when the layer is placed, never while rendering.
badge.tsx lines 18–26; hooks.ts lines 43–52; popover.tsx lines 23–24.
.vs-glass {
position: absolute;
inset: 0;
z-index: 0;
background: linear-gradient(180deg, var(--vs-glass-top) 0%, var(--vs-glass-mid) 46%, var(--vs-glass-bottom) 100%);
-webkit-backdrop-filter: blur(28px) saturate(140%);
backdrop-filter: blur(28px) saturate(140%);
}
.vs-grain {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
/* Normal blending on purpose: Chrome blends mix-blend-mode layers against the un-blurred page behind
backdrop-filter, which shows the background through the glass as sharp lines. */
opacity: var(--vs-grain-opacity);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .5 0 0 0 0 .5 0 0 0 0 .5 .9 .9 .9 0 -1.2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.55'/%3E%3C/svg%3E");
background-size: 160px 160px;
}/*
* What fades when the panel folds sits on its own layer from the start: built on demand, the first fold
* stuttered for ~250ms on an Intel laptop GPU; pre-promoted, it runs at 60fps from the first time.
*/
.vs-frame :is(.vs-row__label, .vs-row__end, .vs-row__dot, .vs-card__text, .vs-card__chev, .vs-search__field, .vs-wordmark__rest, .vs-section__label, .vs-section__dash, .vs-bell) {
will-change: opacity, transform;
} return () => {
cancelAnimationFrame(raf);
ro.disconnect();
// Free what we made but keep the context: the same canvas can mount again (React's dev double effects).
gl.deleteBuffer(buf);
gl.deleteProgram(prog);
}; try {
prog = gl.createProgram()!;
gl.attachShader(prog, compile(gl, gl.VERTEX_SHADER, VERT));
gl.attachShader(prog, compile(gl, gl.FRAGMENT_SHADER, FRAG));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog) ?? "link");
} catch (err) {
console.warn("Silk backdrop: WebGL shader failed, using the CSS stand-in.", err instanceof Error ? err.message : err);
setFailed(true);
return;
}/** Digits that roll: each column keeps its place counted from the right, so only the digits that change move. */
export function RollingNumber({ value, max = 99 }: { value: number; max?: number }) {
const text = formatCount(value, max);
const [prev, setPrev] = React.useState(value);
const [dir, setDir] = React.useState(1);
if (prev !== value) {
setPrev(value);
setDir(value > prev ? 1 : -1);
}const noopSubscribe = () => () => {};
/** False on the server and during hydration, true afterwards. */
export function useIsClient(): boolean {
return React.useSyncExternalStore(
noopSubscribe,
() => true,
() => false,
);
}/** Where an anchor's element comes from: read only when the layer is placed, never while rendering. */
export type ElementSource = () => HTMLElement | null | undefined;
Comments
0 comments
No comments yet. Questions, what you built with it, what could be better: its developer reads them.
Sign in to comment.
Sign in to comment