# Sidebar Floating Inset — React Sidebar Component

> A rounded sidebar that floats inside the page margin, with a search field that filters links live (rows reflow with layout animation), count badges and a storage meter.

- Category: [Sidebars](https://pixelpapa.com/components/sidebars.md)
- Tags: sidebar, floating, inset, search, badges
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/sidebars/sidebar-floating-inset

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/sidebar-floating-inset.json"
```

The file is written to `components/pixelpapa/sidebar-floating-inset.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { Archive, FileText, Inbox, Search, Send, Star, Trash2, Users, type LucideIcon } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useId, useState } from "react";
import { cn } from "@/lib/utils";

export type InsetLink = { label: string; icon: LucideIcon; group: string; badge?: number | string };

type FloatingSidebarProps = {
  brand: string;
  links: InsetLink[];
  active: string;
  onNavigate: (label: string) => void;
  /** Storage used, 0–1. */
  usage?: number;
  usageLabel?: string;
  className?: string;
};

export function FloatingSidebar({ brand, links, active, onNavigate, usage = 0.5, usageLabel, className }: FloatingSidebarProps) {
  const uid = useId();
  const [query, setQuery] = useState("");
  const q = query.trim().toLowerCase();
  const shown = links.filter((l) => l.label.toLowerCase().includes(q));
  const groups = [...new Set(shown.map((l) => l.group))];

  return (
    <aside className={cn("flex w-[272px] shrink-0 flex-col rounded-3xl border border-border bg-card p-3 shadow-xl shadow-black/[0.06]", className)}>
      <div className="flex items-center gap-2.5 px-2 pt-1.5 pb-3">
        <span className="flex size-8 items-center justify-center rounded-xl bg-linear-to-br from-primary to-chart-3 text-sm font-bold text-white">{brand[0]}</span>
        <span className="text-sm font-semibold text-foreground">{brand}</span>
      </div>

      <label className="flex h-9 items-center gap-2 rounded-xl border border-input bg-background px-3 text-sm transition-shadow focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/25">
        <Search className="size-4 shrink-0 text-muted-foreground" />
        <input
          type="search"
          aria-label="Filter navigation"
          placeholder="Search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onKeyDown={(e) => e.key === "Escape" && setQuery("")}
          className="min-w-0 flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground [&::-webkit-search-cancel-button]:hidden"
        />
        <kbd className="rounded-md border border-border bg-muted px-1.5 font-sans text-[11px] text-muted-foreground">{query ? "Esc" : "⌘K"}</kbd>
      </label>

      <nav aria-label="Main" className="mt-3 flex-1 overflow-y-auto">
        <AnimatePresence mode="popLayout" initial={false}>
          {groups.map((group) => (
            <motion.div key={group} layout="position" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ type: "spring", stiffness: 420, damping: 38 }}>
              <p className="px-3 pt-3 pb-1.5 text-[11px] font-medium tracking-wider text-muted-foreground uppercase">{group}</p>
              <ul>
                <AnimatePresence mode="popLayout" initial={false}>
                  {shown
                    .filter((l) => l.group === group)
                    .map(({ label, icon: Icon, badge }) => {
                      const on = label === active;
                      const at = q ? label.toLowerCase().indexOf(q) : -1;
                      return (
                        <motion.li
                          key={label}
                          layout="position"
                          initial={{ opacity: 0, x: -8 }}
                          animate={{ opacity: 1, x: 0 }}
                          exit={{ opacity: 0, transition: { duration: 0.1 } }}
                          transition={{ type: "spring", stiffness: 420, damping: 38 }}
                        >
                          <a
                            href="#"
                            aria-current={on ? "page" : undefined}
                            onClick={(e) => {
                              e.preventDefault();
                              onNavigate(label);
                            }}
                            className={cn(
                              "relative flex h-9 items-center gap-3 rounded-xl px-3 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/60",
                              on ? "font-medium text-primary-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground",
                            )}
                          >
                            {on && (
                              <motion.span
                                layoutId={`sidebar-floating-inset-${uid}`}
                                transition={{ type: "spring", stiffness: 440, damping: 36 }}
                                className="absolute inset-0 rounded-xl bg-primary shadow-md shadow-primary/30"
                              />
                            )}
                            <Icon className="relative size-4" />
                            <span className="relative flex-1">
                              {at < 0 ? (
                                label
                              ) : (
                                <>
                                  {label.slice(0, at)}
                                  <mark className={cn("rounded-sm bg-transparent underline decoration-2 underline-offset-2", on ? "text-primary-foreground" : "text-foreground decoration-primary")}>
                                    {label.slice(at, at + q.length)}
                                  </mark>
                                  {label.slice(at + q.length)}
                                </>
                              )}
                            </span>
                            {badge !== undefined && (
                              <span
                                className={cn(
                                  "relative rounded-full px-1.5 py-0.5 text-[11px] font-semibold tabular-nums",
                                  on ? "bg-primary-foreground/20 text-primary-foreground" : typeof badge === "string" ? "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400" : "bg-muted text-muted-foreground",
                                )}
                              >
                                {badge}
                              </span>
                            )}
                          </a>
                        </motion.li>
                      );
                    })}
                </AnimatePresence>
              </ul>
            </motion.div>
          ))}
        </AnimatePresence>
        {shown.length === 0 && <p className="px-3 py-6 text-center text-sm text-muted-foreground">Nothing matches “{query}”.</p>}
      </nav>

      <div className="mt-3 rounded-2xl bg-muted/60 p-3.5">
        <div className="flex items-baseline justify-between text-xs">
          <span className="font-medium text-foreground">Storage</span>
          <span className="text-muted-foreground tabular-nums">{usageLabel ?? `${Math.round(usage * 100)}%`}</span>
        </div>
        <div className="mt-2 h-1.5 overflow-hidden rounded-full bg-border">
          <motion.div initial={{ width: 0 }} animate={{ width: `${usage * 100}%` }} transition={{ duration: 0.9, ease: [0.22, 1, 0.36, 1] }} className="h-full rounded-full bg-primary" />
        </div>
      </div>
    </aside>
  );
}

const links: InsetLink[] = [
  { label: "Inbox", icon: Inbox, group: "Mail", badge: 24 },
  { label: "Starred", icon: Star, group: "Mail", badge: 3 },
  { label: "Sent", icon: Send, group: "Mail" },
  { label: "Drafts", icon: FileText, group: "Mail", badge: 2 },
  { label: "Shared inboxes", icon: Users, group: "Team", badge: "New" },
  { label: "Archive", icon: Archive, group: "Team" },
  { label: "Trash", icon: Trash2, group: "Team" },
];

const mail = [
  { from: "Tomas Reyl", subject: "Usage rollups are ready for review", time: "09:42" },
  { from: "Kestrel Bank", subject: "Your October statement is available", time: "08:15" },
  { from: "Mira Osei", subject: "Re: tokenizer upgrade plan", time: "Yesterday" },
  { from: "Jun Okabe", subject: "Offsite agenda — draft two", time: "Yesterday" },
  { from: "Halden Freight", subject: "Signed MSA attached", time: "Nov 10" },
];

export default function SidebarFloatingInsetDemo() {
  const [active, setActive] = useState("Inbox");

  return (
    <div className="flex min-h-screen w-full gap-4 bg-muted/50 p-4">
      <FloatingSidebar brand="Postbox" links={links} active={active} onNavigate={setActive} usage={0.62} usageLabel="9.3 of 15 GB" />
      <main className="min-w-0 flex-1 rounded-3xl border border-border bg-background p-8">
        <h1 className="text-2xl font-semibold tracking-tight text-foreground">{active}</h1>
        <p className="mt-1 text-sm text-muted-foreground">Thursday, November 12</p>
        <ul className="mt-6 divide-y divide-border rounded-2xl border border-border bg-card">
          {mail.map((m) => (
            <li key={m.subject} className="flex items-center gap-4 px-5 py-3.5 text-sm">
              <span className="w-36 shrink-0 truncate font-medium text-card-foreground">{m.from}</span>
              <span className="min-w-0 flex-1 truncate text-muted-foreground">{m.subject}</span>
              <span className="shrink-0 text-xs text-muted-foreground">{m.time}</span>
            </li>
          ))}
        </ul>
      </main>
    </div>
  );
}
```

## AI prompt

```text
Add the "Sidebar Floating Inset" component from PixelPapa UI to this project.

What it is: A rounded sidebar that floats inside the page margin, with a search field that filters links live (rows reflow with layout animation), count badges and a storage meter.
Category: Sidebars. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/sidebar-floating-inset.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/sidebar-floating-inset.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/sidebar-floating-inset" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More sidebars

- [Sidebar Collapsible Rail](https://pixelpapa.com/components/sidebars/sidebar-collapsible-rail.md): An app sidebar that springs between a full panel and a 64px icon rail; labels fade out, and in rail mode each icon shows a tooltip on hover or focus.
- [Sidebar Nested Sections](https://pixelpapa.com/components/sidebars/sidebar-nested-sections.md): A sidebar of collapsible groups with spring height animation; one active indicator bar slides between links, even across groups, along a tree guide line.
- [Sidebar Workspace Switcher](https://pixelpapa.com/components/sidebars/sidebar-workspace-switcher.md) (Pro): A sidebar with a workspace switcher on top whose menu springs open and rolls the new workspace into place, and a user card at the bottom with its own pop-up menu.
