GodUIGodUI
129Follow on X

Anatomy of the GodUI Sidebar

How the GodUI Sidebar collapses without animating a single width. The panel slides, a surface glides to the icon rail, labels fade ahead of its edge, sub-menus sweep open and shut, and the page content rides the panel's edge, all with translate and opacity.

01/05The panel slides, the width snaps
tsx
/* shadcn/ui */"transition-[width] duration-200 ease-linear"      // the gap"transition-[left,right,width] duration-200 …"     // the panel/* GodUI: a class sets where the panel rests… */"group-data-[collapsible=offcanvas]:group-data-[side=left]:-translate-x-full",// …and the panel glides there with the same FLIP as the page beside it.useFlipGroup(wrapperRef, state, { selector: panelSelector(side, variant, collapsible) });
01

The panel slides, the width snaps

shadcn's sidebar collapses by animating widths. An in-flow gap shrinks from 16rem to 0 while the fixed panel animates its left off-screen. Every frame changes a size, so the browser lays out the whole page again, then repaints it.

GodUI gives the gap its new width in one step, so the layout changes once. The panel itself never resizes. Offcanvas, a class moves it off-screen with translate, and a Web Animations FLIP glides it there on a spring, the same clock the page beside it uses (see below). A CSS transition would run on its own clock, and turning it around mid-way would leave the page behind.

02/05A surface behind a snapped box
tsx
<div data-slot="sidebar-container" /* width snaps to 3rem */><div  data-slot="sidebar-surface"  className="w-(--sidebar-width) bg-sidebar …    group-data-[collapsible=icon]:-translate-x-[calc(      var(--sidebar-width)-var(--sidebar-width-icon))]"/><div data-slot="sidebar-inner" /* transparent */>{children}</div></div>
02

A surface behind a snapped box

Icon mode can't slide the whole panel away, because the icons have to stay on screen. So the panel's box (dashed) snaps to the icon rail at once. The background and border moved off that box onto a new layer behind it, sidebar-surface, which is always full width. Collapsing, the surface slides left by the width difference, so its right edge travels from 16rem to 3rem.

The floating card can't slide as one piece, because its rounded left side would leave the screen. It's cut into three: a left cap that stays put, a middle that scales on x from its left edge, and a right cap that slides. The middle's end and the cap move by the same amount every frame, so the corners, border and shadow are never stretched.

03/05Labels fade ahead of the edge
tsx
// While it collapses, the content keeps its full width (the rows still// take their icon-layout heights): no box snaps and cuts a label."motion-safe:in-data-[moving=collapsing]:w-[calc(var(--sidebar-width)-…)]","motion-safe:[[data-moving=collapsing]_&_[data-sidebar=menu-button]]:w-full!",// Labels: out on the fast token, in once the edge reaches them,// both on the panel's spring, from whatever is drawn.el.animate([{ opacity: from, translate: "0px 0px" }, { opacity: 0, translate: "-4px 0px" }],{ duration: fast, easing: spring, fill: "forwards" },);// Sub-menus: the box's clip edge rides the row below,// its items hold still with their own row.const boxFrom = row - room * shut; // px; shut: 1 closed, 0 opensub.animate([{ translate: "0 " + boxFrom + "px" }, { translate: "0 0" }], clock);item.animate([{ translate: "0 " + room * shut + "px" }, { translate: "0 0" }], clock);
03

Labels fade ahead of the edge

shadcn's icon layout hides a lot: menu text, group labels, badges, actions and sub-menus. Snapped at the click, all of it vanishes in one frame and the rest is cut mid-word by a 3rem box. So while the panel collapses, its content keeps its full width. Only the rows' heights take the icon layout (and glide there with FLIP). Badges, actions and sub-menus stay drawn so they can leave smoothly. The edge wipes past the labels while they fade out and tuck 4px toward their icons, on the fast token and the panel's own spring, so they're mostly gone before the edge reaches them. The horizontal snap lands in the commit that ends the move, when nothing it hides is drawn.

Expanding, each label starts to fade in once the surface's edge reaches it, read off the same spring, with a small top-first cascade. A sub-menu uses the Accordion's clip window. Its box slides with its bottom edge glued to the row below while its items counter-move and hold still with their own row, so the edge sweeps over them instead of the items popping in at the end. Every fade and sweep starts from what's drawn, so toggling mid-way, or holding Ctrl/⌘+B, never jumps.

04/05The content FLIPs
tsx
// SidebarProvider: everything after the sidebar glides from where it was,// on the wrapper's spring (ease-spring-smooth) and --godui-duration-base.useFlipGroup(wrapperRef, state, {selector: ':scope > [data-slot="sidebar"] ~ *',});
04

The content FLIPs

The page next to the sidebar is laid out right after the gap, so when the gap snaps (dashed) the content jumps to its new spot. useFlipGroup measures it before and after, puts it back where it was with a translate, and plays that back to rest on the surface's spring and clock. The two start in the same frame and share a curve, so the content's edge stays glued to the panel's on every frame, in both directions. Reverse mid-way and every piece restarts from where it's drawn: the FLIP reads the running offset before it cancels it, and accounts for the panel's own resting translate having changed underneath.

While it glides, the content is drawn partly past the right edge of the page. For the length of the move the wrapper clips that (overflow-x-clip), so no scrollbar flashes; at rest it clips nothing. The content is also lifted above the panel while it moves: the panel's content is laid out at full width before the surface's edge gets there, and the content riding the edge covers it until the edge passes.

05/05Result
Building Your App/Data Fetching
05

The result

Collapse it to icons and back, press Ctrl/⌘+B, and toggle again mid-way to reverse. Collapsed, hover an icon for its tooltip.

What's animated

InteractionKeyframe / mechanismPropertiesEasingDuration
Offcanvas collapse / expandthe container's class translate, glided there by FLIPtranslate 0 ↔ ∓100%ease-spring-smooth260ms
Icon collapse / expandsidebar-surface's class translate, glided there by FLIPtranslate by the width differenceease-spring-smooth260ms
Floating card, icon modeleft cap still, middle scales from its left, right cap slides with itscale, translateease-spring-smooth260ms
Content beside the sidebarFLIP (useFlipGroup on the wrapper)translateease-spring-smooth260ms
Rows in icon mode (group labels tuck up, header and footer buttons shrink, badges and actions ride their row)FLIP inside the paneltranslateease-spring-smooth260ms
Labels collapsing to icons (menu text and chevrons, group labels, badges, actions)WAAPI fade, from what's drawn; menu text tucks 4px toward its iconopacity, translateease-spring-smooth150ms
Labels expanding from iconsWAAPI fade, starting when the panel's edge reaches each one (a small top-first cascade), ending with the moveopacity, translateease-spring-smooth≤ 260ms
Sub-menus, icon collapse / expandthe Accordion's clip window: the box's edge rides the row below, its items hold still with their own row and fade as the edge passes themtranslate, opacityease-spring-smooth260ms
Railtransitiontranslateease-spring-smooth260ms
Gap and container width, button size and paddingsnap (collapsing to icons, the panel's content keeps its width until the move ends, when everything it hides has faded)nonenonenone
Mobile (below 768px)GodUI Sheettranslate, opacityease-spring-smooth380ms

The panel, its surface, the content, the rows, the labels and the sub-menus all move on one clock: Web Animations on the same spring, started in the same frame. Toggle again mid-way (or hold Ctrl/⌘+B) and each piece turns around from where it's drawn, still glued.

Why GPU-only

The panel, its surface, the content beside it and the sub-menus animate translate, the floating card's middle animates scale, and the labels animate opacity (and a 4px translate). Widths, padding and button sizes change once at the click and once at the end of a collapse. Every frame in between is composited.

Reduced motion

Nothing slides, glides or fades: the panel, its surface and the content take their new places in one step, and labels and sub-menus appear and disappear at once. useFlipGroup, the floating card's stretch and the label and sub-menu moves skip their animations. The full-width hold is motion-safe: only, and the rail turns its transition off with motion-reduce:transition-none.

Replacing shadcn

npx shadcn add @godui/sidebar overwrites components/ui/sidebar.tsx. Exports, props and data-slot attributes match shadcn/ui new-york-v4, so existing imports keep working. It also installs godui-motion (easings, keyframes, the FLIP hook) and shadcn's use-mobile hook and separator. useFlipGroup gains an optional measure (track one element, move another) and now carries a running glide over a change of the element's own translate. It also waits on each running animation only once while it re-baselines, so toggling many times mid-move can't pile up work.

A few things differ, all on desktop:

  • The container has one extra child, data-slot="sidebar-surface", which draws the panel's background and border (and the floating card's rounded border and shadow). sidebar-inner is transparent, so put background overrides on the surface, not the inner. The border is still the container's (the surface inherits its width, style and color, and sits on the container's border box), so border classes passed to Sidebar paint on the box edge as they do in shadcn.
  • While the sidebar moves, the wrapper clips horizontal overflow (overflow-x-clip): for a moment the gliding content is drawn past the right edge, and that shouldn't flash a scrollbar. At rest nothing is clipped, so wide content scrolls the page as it does in shadcn. On a page that's wider than the window, a classic (non-overlay) horizontal scrollbar therefore disappears for the length of the move and comes back when it ends.
  • While the sidebar moves, the content after it is lifted above the panel. That lasts as long as the move's animations (one --godui-duration-base). If one of them is paused, it lets go one --godui-duration-slow after that animation should have ended. The panel's content is laid out at full width while the surface's edge is still on its way (expanding at once; collapsing to icons until the move ends), and the content, glued to that edge, covers it until the edge passes. Give that content a background (SidebarInset has bg-background).
  • Collapsing to icons, the panel's content keeps its full width (and its badges, actions and sub-menus stay drawn) until the move ends, so labels can fade out instead of being cut by the snapped rail. The rows take their icon-layout heights at once. Code that measures the panel's content in that ~260ms sees the full width; the icon layout's widths apply when the move ends (at once under reduced motion).
  • While collapsing to icons (about 260ms), badges, actions, group actions and sub-menus are forced to display: flex so they can fade and sweep. A call-site className="hidden" on one of them flashes visible during the collapse; hide it with a conditional render instead.
  • A menu button's label is everything after its first child (its icon): that's what fades. Wrap the label in an element (<span>Home</span>, as shadcn's examples do). A bare text label (<Home /> Home) can't be faded on the compositor, so that button skips the full-width hold and its label is clipped by the rail on the click, as in shadcn. A button whose first child is text keeps it, clipped by the rail, as in shadcn.
  • While the content glides (about 260ms), position: fixed descendants of SidebarInset move with it (the FLIP's translate makes it their containing block), and the lift keeps their z-index inside it.
  • A right-side sidebar's content glides with its edge too. The page content before it changes width at once: its left edge doesn't move, so there is nothing to glide.