Inset Sidebar
A framed workspace canvas beside softly recessed navigation and account controls.
Make it yours.
Install the editable source and its dependencies into your React + Tailwind project.
Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/inset-sidebar.jsonUse it
Scroll horizontally for long lines
"use client";
import { InsetSidebar } from "@/components/jez-ui/blocks/inset-sidebar";
export default function Example() {
return (
<>
<InsetSidebar />
</>
);
}
Compose your own
These styled parts are included in the same install. Use children to build your layout; add className only when you want an override.
Scroll horizontally for long lines
"use client";
import {
InsetSidebarRoot,
InsetSidebarPanel,
InsetSidebarHeader,
InsetSidebarBody,
InsetSidebarFooter,
InsetSidebarContent,
InsetSidebarTrigger,
} from "@/components/jez-ui/blocks/inset-sidebar";
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
} from "@/components/jez-ui/ui/sidebar";
export default function Example() {
return (
<InsetSidebarRoot>
<InsetSidebarPanel>
<InsetSidebarHeader>Our studio</InsetSidebarHeader>
<InsetSidebarBody>
<SidebarGroup>
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{["Work", "Archive"].map((label) => (
<SidebarMenuItem key={label}>
<SidebarMenuButton asChild>
<a href={"#" + label.toLowerCase()}>
<span>{label}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</InsetSidebarBody>
<InsetSidebarFooter>Studio settings</InsetSidebarFooter>
</InsetSidebarPanel>
<InsetSidebarContent>
<header className="flex items-center gap-3 border-b border-border p-4">
<InsetSidebarTrigger />
Your workspace
</header>
<main className="p-6">Your own page content.</main>
</InsetSidebarContent>
</InsetSidebarRoot>
);
}
Scroll horizontally for long lines
import { InsetSidebar, InsetSidebarRoot, InsetSidebarPanel, InsetSidebarHeader, InsetSidebarBody, InsetSidebarFooter, InsetSidebarContent, InsetSidebarTrigger } from "@/components/jez-ui/blocks/inset-sidebar";Props & types
Scroll horizontally for long lines
Omit<React.ComponentProps<typeof SidebarLayout>, "variant">Source
View 6 source files
registry/blocks/inset-sidebar.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { SidebarLayout } from "./sidebar-layout";
export function InsetSidebar(
props: Omit<React.ComponentProps<typeof SidebarLayout>, "variant">,
) {
return <SidebarLayout variant="inset" {...props} />;
}
import {
SidebarProvider,
Sidebar,
SidebarHeader,
SidebarContent,
SidebarFooter,
SidebarInset,
SidebarTrigger,
} from "../ui/sidebar";
import { cn } from "../ui/utils";
export function InsetSidebarRoot({
className,
...props
}: React.ComponentProps<typeof SidebarProvider>) {
return (
<SidebarProvider
defaultOpen={true}
className={cn(
"min-h-[620px] overflow-hidden rounded-xl border border-border",
className,
)}
{...props}
/>
);
}
export function InsetSidebarPanel(props: React.ComponentProps<typeof Sidebar>) {
return <Sidebar variant="inset" collapsible="icon" {...props} />;
}
export function InsetSidebarHeader(
props: React.ComponentProps<typeof SidebarHeader>,
) {
return <SidebarHeader {...props} />;
}
export function InsetSidebarBody(
props: React.ComponentProps<typeof SidebarContent>,
) {
return <SidebarContent {...props} />;
}
export function InsetSidebarFooter(
props: React.ComponentProps<typeof SidebarFooter>,
) {
return <SidebarFooter {...props} />;
}
export function InsetSidebarContent(
props: React.ComponentProps<typeof SidebarInset>,
) {
return <SidebarInset {...props} />;
}
export function InsetSidebarTrigger(
props: React.ComponentProps<typeof SidebarTrigger>,
) {
return <SidebarTrigger {...props} />;
}
registry/blocks/sidebar-layout.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import {
Home,
Inbox,
Folder,
Users,
Settings,
PanelLeftClose,
Menu,
X,
Search,
Plus,
ArrowUpRight,
} from "lucide-react";
import {
OrganizationSwitcher,
type Organization,
} from "./organization-switcher";
import { UserSwitcher, type SwitcherAccount } from "./user-switcher";
import { SidebarProvider } from "../ui/sidebar";
import { cn } from "../ui/utils";
const links = [
{ name: "Overview", icon: Home },
{ name: "Inbox", icon: Inbox },
{ name: "Projects", icon: Folder },
{ name: "Team", icon: Users },
{ name: "Settings", icon: Settings },
];
export function SidebarLayout({
sidebar,
organizations,
accounts,
onOrganizationChange,
onAccountChange,
onCreateOrganization,
onAddAccount,
onSignOut,
variant = "workspace",
className,
children,
onNavigate,
...rootProps
}: React.ComponentProps<"div"> & {
sidebar?: React.ReactNode;
organizations?: Organization[];
accounts?: SwitcherAccount[];
onOrganizationChange?: (id: string) => void;
onAccountChange?: (id: string) => void;
onCreateOrganization?: () => void;
onAddAccount?: () => void;
onSignOut?: () => void;
variant?: "workspace" | "rail" | "inset";
className?: string;
children?: React.ReactNode;
onNavigate?: (name: string) => void;
}) {
const [current, setCurrent] = React.useState("Overview"),
[mobile, setMobile] = React.useState(false),
[collapsed, setCollapsed] = React.useState(false),
[query, setQuery] = React.useState("");
const rail = variant === "rail" || collapsed;
return (
<SidebarProvider
open={!collapsed}
onOpenChange={(open) => setCollapsed(!open)}
openMobile={mobile}
onOpenMobileChange={setMobile}
className="contents"
>
<div
{...rootProps}
className={cn(
"relative flex min-h-[620px] overflow-hidden rounded-xl border border-border bg-muted/50",
variant === "inset" && "md:p-2",
className,
)}
>
<aside
data-state={rail && !mobile ? "collapsed" : "expanded"}
className={cn(
"group/sidebar absolute inset-y-0 left-0 z-30 flex flex-col border-r border-border bg-background p-3 md:relative md:flex",
mobile ? "flex w-64 shadow-xl" : "hidden",
rail ? "md:w-[76px]" : "md:w-60",
variant === "inset" && "md:border-0 md:bg-transparent",
)}
>
{sidebar ?? (
<>
<OrganizationSwitcher
items={organizations}
onValueChange={onOrganizationChange}
onCreate={onCreateOrganization}
compact={rail && !mobile}
/>
<button
aria-label="Close sidebar"
onClick={() => setMobile(false)}
className="absolute right-1 top-1 rounded bg-background p-1 md:hidden"
>
<X size={14} />
</button>
{!rail && (
<label className="mt-5 flex items-center gap-2 rounded-md border border-border bg-background px-2.5 py-2">
<Search
size={14}
className="shrink-0 text-muted-foreground"
/>
<input
aria-label="Filter navigation"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Find a page…"
className="min-w-0 bg-transparent text-xs outline-none"
/>
</label>
)}
<nav aria-label="Workspace pages" className="mt-6 grid gap-1">
{links
.filter(
(l) =>
rail ||
l.name.toLowerCase().includes(query.toLowerCase()),
)
.map(({ name, icon: Icon }) => (
<button
key={name}
aria-label={name}
title={rail ? name : undefined}
aria-current={current === name ? "page" : undefined}
onClick={() => {
setCurrent(name);
setMobile(false);
onNavigate?.(name);
}}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm transition-colors",
rail && "justify-center px-2",
current === name
? "bg-primary/10 font-medium text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
<Icon size={17} />
{!rail && (
<>
<span className="flex-1">{name}</span>
{name === "Inbox" && (
<span className="text-xs">4</span>
)}
</>
)}
</button>
))}
{!rail &&
!links.some((l) =>
l.name.toLowerCase().includes(query.toLowerCase()),
) && (
<p className="p-3 text-xs text-muted-foreground">
No pages found.
</p>
)}
</nav>
{!rail && (
<div className="mt-8 px-3">
<p className="mb-3 text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
Pinned projects
</p>
{["Website refresh", "Brand guidelines"].map((name, i) => (
<button
key={name}
onClick={() => {
setCurrent(name);
setMobile(false);
onNavigate?.(name);
}}
className="flex w-full items-center gap-2 py-2 text-left text-xs text-muted-foreground hover:text-foreground"
>
<span
className={cn(
"size-1.5 rounded-sm",
i ? "bg-[#b98d70]" : "bg-[#8c9d7a]",
)}
/>
{name}
</button>
))}
</div>
)}
<div className="mt-auto border-t border-border pt-3">
<UserSwitcher
items={accounts}
onValueChange={onAccountChange}
onAddAccount={onAddAccount}
onSignOut={onSignOut}
compact={rail && !mobile}
/>
</div>
</>
)}
</aside>
{mobile && (
<button
aria-label="Dismiss sidebar"
onClick={() => setMobile(false)}
className="absolute inset-0 z-20 bg-black/25 md:hidden"
/>
)}
<div
className={cn(
"min-w-0 flex-1 bg-background",
variant === "inset" && "rounded-lg border border-border shadow-sm",
)}
>
<header className="flex h-16 items-center gap-3 border-b border-border px-5">
<button
aria-label="Open sidebar"
onClick={() => setMobile(true)}
className="p-1 md:hidden"
>
<Menu size={18} />
</button>
<button
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
onClick={() => setCollapsed(!collapsed)}
className="hidden rounded p-1 text-muted-foreground hover:bg-muted md:block"
>
<PanelLeftClose size={17} />
</button>
<span className="text-xs text-muted-foreground">Workspace</span>
<span className="text-border">/</span>
<span className="truncate text-sm">{current}</span>
</header>
<main className="p-6 md:p-8">
{children ?? (
<>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs text-muted-foreground">
Your workspace, at a glance
</p>
<h1 className="mt-2 font-display text-2xl tracking-tight">
{current === "Overview"
? "Make room for good work."
: current}
</h1>
</div>
<button
onClick={() => {
setCurrent("New project");
onNavigate?.("New project");
}}
className="flex items-center gap-2 rounded-lg border border-border px-3 py-2 text-xs hover:bg-muted"
>
<Plus size={14} />
New project
</button>
</div>
{current === "New project" ? (
<form
className="mt-8 grid max-w-sm gap-4"
onSubmit={(e) => {
e.preventDefault();
const name = new FormData(e.currentTarget)
.get("name")
?.toString()
.trim();
if (name) setCurrent(name);
}}
>
<label className="grid gap-2 text-sm">
Project name
<input
name="name"
required
className="rounded-lg border border-border bg-background p-3"
placeholder="Untitled project"
/>
</label>
<button className="rounded-lg bg-primary p-3 text-sm text-primary-foreground">
Create project
</button>
</form>
) : (
<>
<p className="mt-3 max-w-lg text-sm leading-relaxed text-muted-foreground">
A clear view of the projects and conversations moving your
team forward.
</p>
<div className="mt-9 divide-y divide-border border-y border-border">
{[
"Website refresh",
"Brand guidelines",
"Component library",
].map((name, i) => (
<button
key={name}
onClick={() => {
setCurrent(name);
onNavigate?.(name);
}}
className="flex w-full items-center gap-4 py-5 text-left hover:bg-muted/40"
>
<span className="grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-xs">
0{i + 1}
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium">
{name}
</span>
<span className="mt-1 block text-xs text-muted-foreground">
{
[
"12 tasks · In progress",
"8 tasks · In review",
"24 tasks · Planning",
][i]
}
</span>
</span>
<ArrowUpRight
size={16}
className="text-muted-foreground"
/>
</button>
))}
</div>
</>
)}
</>
)}
</main>
</div>
</div>
</SidebarProvider>
);
}
registry/blocks/organization-switcher.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "../ui/utils";
import { DropdownMenu as D } from "radix-ui";
import { Check, ChevronsUpDown, Plus } from "lucide-react";
export type Organization = { id: string; name: string; plan: string };
const defaults: Organization[] = [
{ id: "common", name: "Common Studio", plan: "Team workspace" },
{ id: "personal", name: "Personal", plan: "Free workspace" },
{ id: "fieldwork", name: "Fieldwork", plan: "Team workspace" },
];
export type OrganizationSwitcherOptions = {
items?: Organization[];
onValueChange?: (id: string) => void;
onCreate?: () => void;
compact?: boolean;
};
export type OrganizationSwitcherProps = Omit<
React.ComponentProps<"div">,
keyof OrganizationSwitcherOptions
> &
OrganizationSwitcherOptions;
function useOrganizationSwitcherModel({
items = defaults,
onValueChange,
onCreate,
compact = false,
children,
...rootProps
}: OrganizationSwitcherProps) {
const [value, setValue] = React.useState(items[0]?.id);
const [notice, setNotice] = React.useState("");
const active = items.find((i) => i.id === value) ?? items[0];
if (!active) return null;
return {
items,
onValueChange,
onCreate,
compact,
children,
rootProps,
value,
setValue,
notice,
setNotice,
active,
};
}
const OrganizationSwitcherCompositionContext = React.createContext<ReturnType<
typeof useOrganizationSwitcherModel
> | null>(null);
function useOrganizationSwitcherComposition() {
const context = React.useContext(OrganizationSwitcherCompositionContext);
if (!context)
throw new Error(
"OrganizationSwitcher parts must be inside OrganizationSwitcher.",
);
return context;
}
export function OrganizationSwitcher(props: OrganizationSwitcherProps) {
const model = useOrganizationSwitcherModel(props);
if (!model) return null;
const { rootProps, children } = model;
return (
<OrganizationSwitcherCompositionContext.Provider value={model}>
<div {...rootProps}>
{children !== undefined ? (
children
) : (
<>
<OrganizationSwitcherMenu />
<OrganizationSwitcherStatus />
</>
)}
</div>
</OrganizationSwitcherCompositionContext.Provider>
);
}
export function OrganizationSwitcherMenu({
children,
...props
}: Partial<React.ComponentProps<typeof D.Root>> & {
children?: React.ReactNode;
}) {
return (
<D.Root {...props}>
{children === undefined ? (
<>
<OrganizationSwitcherTrigger />
<OrganizationSwitcherPopup />
</>
) : (
children
)}
</D.Root>
);
}
export function OrganizationSwitcherStatus({
children,
}: React.PropsWithChildren) {
const { notice } = useOrganizationSwitcherComposition();
return children === undefined
? notice && (
<p role="status" className="p-2 text-xs">
{notice}
</p>
)
: children;
}
export function OrganizationSwitcherTrigger({
children,
...props
}: Partial<React.ComponentProps<typeof D.Trigger>> & {
children?: React.ReactNode;
}) {
const { compact, active } = useOrganizationSwitcherComposition();
return (
<D.Trigger
aria-label={`Workspace: ${active.name}`}
{...props}
className={cn(
"flex w-full items-center gap-3 rounded-lg p-2 text-left hover:bg-muted",
props.className,
)}
>
{children === undefined ? (
<>
<span className="grid size-9 shrink-0 place-items-center rounded-lg bg-primary font-display text-lg text-primary-foreground">
{active.name[0]}
</span>
{!compact && (
<>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-semibold">
{active.name}
</span>
<span className="block text-xs text-muted-foreground">
{active.plan}
</span>
</span>
<ChevronsUpDown size={14} />
</>
)}
</>
) : (
children
)}
</D.Trigger>
);
}
export function OrganizationSwitcherOptions({
children,
...props
}: Partial<React.ComponentProps<typeof D.RadioGroup>> & {
children?: React.ReactNode;
}) {
const { items, onValueChange, setValue, active } =
useOrganizationSwitcherComposition();
return (
<D.RadioGroup
value={active.id}
{...props}
onValueChange={(value) => {
((id) => {
setValue(id);
onValueChange?.(id);
})(value);
props.onValueChange?.(value);
}}
>
{children === undefined
? items.map((item) => (
<D.RadioItem
key={item.id}
value={item.id}
className="flex items-center justify-between gap-3 rounded-md p-3 text-sm outline-none data-[highlighted]:bg-muted"
>
<span>
{item.name}
<span className="mt-1 block text-xs text-muted-foreground">
{item.plan}
</span>
</span>
<D.ItemIndicator>
<Check size={14} />
</D.ItemIndicator>
</D.RadioItem>
))
: children}
</D.RadioGroup>
);
}
export function OrganizationSwitcherPopup({
children,
...props
}: Partial<React.ComponentProps<typeof D.Portal>> & {
children?: React.ReactNode;
}) {
const { onCreate, setNotice } = useOrganizationSwitcherComposition();
return (
<D.Portal {...props}>
{children === undefined ? (
<D.Content
sideOffset={8}
align="start"
collisionPadding={12}
className="jez-popover z-50 w-64 rounded-xl border border-border bg-background p-1.5 text-foreground shadow-xl"
>
<D.Label className="px-3 py-2 text-xs text-muted-foreground">
Your workspaces
</D.Label>
<OrganizationSwitcherOptions />
<D.Separator className="my-1 h-px bg-border" />
<D.Item
onSelect={() =>
onCreate
? onCreate()
: setNotice("Demo: connect your workspace creation flow.")
}
className="flex items-center gap-2 rounded-md p-3 text-sm outline-none data-[highlighted]:bg-muted"
>
<Plus size={15} />
Create workspace
</D.Item>
</D.Content>
) : (
children
)}
</D.Portal>
);
}
registry/ui/utils.ts
Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
return twMerge(clsx(values));
}
registry/blocks/user-switcher.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { DropdownMenu as D } from "radix-ui";
import { Check, ChevronsUpDown, Plus, Settings, LogOut } from "lucide-react";
import { cn } from "../ui/utils";
export type SwitcherAccount = {
id: string;
name: string;
detail: string;
initials: string;
color?: string;
};
const accounts: SwitcherAccount[] = [
{
id: "alex",
name: "Alex Rivers",
detail: "alex@common.example",
initials: "AR",
color: "#d5dec9",
},
{
id: "jamie",
name: "Jamie Chen",
detail: "jamie@common.example",
initials: "JC",
color: "#e2cfc0",
},
];
export type UserSwitcherOptions = {
items?: SwitcherAccount[];
defaultValue?: string;
onValueChange?: (id: string) => void;
onAddAccount?: () => void;
onSignOut?: () => void;
settingsHref?: string;
className?: string;
compact?: boolean;
};
export type UserSwitcherProps = Omit<
React.ComponentProps<"div">,
keyof UserSwitcherOptions
> &
UserSwitcherOptions;
function useUserSwitcherModel({
items = accounts,
defaultValue = items[0]?.id,
onValueChange,
onAddAccount,
onSignOut,
settingsHref = "/templates/projects/preview/settings",
className,
compact = false,
children,
...rootProps
}: UserSwitcherProps) {
const [value, setValue] = React.useState(defaultValue);
const [notice, setNotice] = React.useState("");
const active = items.find((i) => i.id === value) ?? items[0];
if (!active) return null;
return {
items,
defaultValue,
onValueChange,
onAddAccount,
onSignOut,
settingsHref,
className,
compact,
children,
rootProps,
value,
setValue,
notice,
setNotice,
active,
};
}
const UserSwitcherCompositionContext = React.createContext<ReturnType<
typeof useUserSwitcherModel
> | null>(null);
function useUserSwitcherComposition() {
const context = React.useContext(UserSwitcherCompositionContext);
if (!context)
throw new Error("UserSwitcher parts must be inside UserSwitcher.");
return context;
}
export function UserSwitcher(props: UserSwitcherProps) {
const model = useUserSwitcherModel(props);
if (!model) return null;
const { className, rootProps, children } = model;
return (
<UserSwitcherCompositionContext.Provider value={model}>
<div {...rootProps} className={cn("relative w-full max-w-xs", className)}>
{children !== undefined ? (
children
) : (
<>
<UserSwitcherMenu />
<UserSwitcherStatus />
</>
)}
</div>
</UserSwitcherCompositionContext.Provider>
);
}
export function UserSwitcherMenu({
children,
...props
}: Partial<React.ComponentProps<typeof D.Root>> & {
children?: React.ReactNode;
}) {
return (
<D.Root {...props}>
{children === undefined ? (
<>
<UserSwitcherTrigger />
<UserSwitcherPopup />
</>
) : (
children
)}
</D.Root>
);
}
export function UserSwitcherStatus({ children }: React.PropsWithChildren) {
const { notice } = useUserSwitcherComposition();
return children === undefined
? notice && (
<p role="status" className="px-2 pt-2 text-xs text-muted-foreground">
{notice}
</p>
)
: children;
}
export function UserSwitcherTrigger({
children,
...props
}: Partial<React.ComponentProps<typeof D.Trigger>> & {
children?: React.ReactNode;
}) {
const { compact, active } = useUserSwitcherComposition();
return (
<D.Trigger
aria-label={`Account: ${active.name}`}
{...props}
className={cn(
"flex w-full items-center gap-3 rounded-lg p-2 text-left hover:bg-muted focus-visible:outline-2 focus-visible:outline-primary",
props.className,
)}
>
{children === undefined ? (
<>
<span
style={{ background: active.color ?? "#d5dec9" }}
className="grid size-9 shrink-0 place-items-center rounded-full text-xs font-semibold text-[#263326]"
>
{active.initials}
</span>
{!compact && (
<>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">
{active.name}
</span>
<span className="block truncate text-xs text-muted-foreground">
{active.detail}
</span>
</span>
<ChevronsUpDown
size={14}
className="shrink-0 text-muted-foreground"
/>
</>
)}
</>
) : (
children
)}
</D.Trigger>
);
}
export function UserSwitcherOptions({
children,
...props
}: Partial<React.ComponentProps<typeof D.RadioGroup>> & {
children?: React.ReactNode;
}) {
const { items, onValueChange, setValue, active } =
useUserSwitcherComposition();
return (
<D.RadioGroup
value={active.id}
{...props}
onValueChange={(value) => {
((id) => {
setValue(id);
onValueChange?.(id);
})(value);
props.onValueChange?.(value);
}}
>
{children === undefined
? items.map((item) => (
<D.RadioItem
key={item.id}
value={item.id}
className="flex cursor-default items-center gap-3 rounded-md px-3 py-2.5 outline-none data-[highlighted]:bg-muted"
>
<span
style={{ background: item.color ?? "#d5dec9" }}
className="grid size-8 shrink-0 place-items-center rounded-full text-xs text-[#263326]"
>
{item.initials}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm">{item.name}</span>
<span className="block truncate text-xs text-muted-foreground">
{item.detail}
</span>
</span>
<D.ItemIndicator>
<Check size={14} />
</D.ItemIndicator>
</D.RadioItem>
))
: children}
</D.RadioGroup>
);
}
export function UserSwitcherPopup({
children,
...props
}: Partial<React.ComponentProps<typeof D.Portal>> & {
children?: React.ReactNode;
}) {
const { onAddAccount, onSignOut, settingsHref, setNotice } =
useUserSwitcherComposition();
return (
<D.Portal {...props}>
{children === undefined ? (
<D.Content
side="top"
align="start"
sideOffset={8}
collisionPadding={12}
className="jez-popover z-50 w-72 max-w-[calc(100vw-24px)] rounded-xl border border-border bg-background p-1.5 text-foreground shadow-xl"
>
<D.Label className="px-3 py-2 text-xs text-muted-foreground">
Switch account
</D.Label>
<UserSwitcherOptions />
<D.Separator className="my-1 h-px bg-border" />
<D.Item
onSelect={() =>
onAddAccount
? onAddAccount()
: setNotice("Demo: connect your account sign-in flow.")
}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm outline-none data-[highlighted]:bg-muted"
>
<Plus size={15} />
Add an account
</D.Item>
<D.Item asChild>
<a
href={settingsHref}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm outline-none data-[highlighted]:bg-muted"
>
<Settings size={15} />
Account settings
</a>
</D.Item>
<D.Item
onSelect={() =>
onSignOut
? onSignOut()
: setNotice("Demo: connect your sign-out handler.")
}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-danger outline-none data-[highlighted]:bg-muted"
>
<LogOut size={15} />
Sign out
</D.Item>
</D.Content>
) : (
children
)}
</D.Portal>
);
}
registry/ui/sidebar.tsx
Scroll horizontally for long lines
"use client";
import * as React from "react";
import { Dialog, Slot } from "radix-ui";
import { PanelLeft, X } from "lucide-react";
import { cn } from "./utils";
type SidebarState = {
triggerRef: React.RefObject<HTMLElement | null>;
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const Context = React.createContext<SidebarState | null>(null);
export function useSidebar() {
const value = React.useContext(Context);
if (!value) throw new Error("Sidebar components require SidebarProvider.");
return value;
}
export function SidebarProvider({
children,
defaultOpen = true,
open: controlled,
onOpenChange,
openMobile: controlledMobile,
onOpenMobileChange,
className,
style,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
openMobile?: boolean;
onOpenMobileChange?: (open: boolean) => void;
}) {
const triggerRef = React.useRef<HTMLElement | null>(null);
const [local, setLocal] = React.useState(defaultOpen),
[mobile, setMobile] = React.useState(false),
[isMobile, setIsMobile] = React.useState(false);
const open = controlled ?? local,
openMobile = controlledMobile ?? mobile;
const setOpen = (v: boolean) => {
setLocal(v);
onOpenChange?.(v);
};
const setOpenMobile = (v: boolean) => {
if (v)
triggerRef.current =
document.activeElement instanceof HTMLElement
? document.activeElement
: null;
setMobile(v);
onOpenMobileChange?.(v);
};
React.useEffect(() => {
const media = matchMedia("(max-width: 767px)");
const update = () => setIsMobile(media.matches);
update();
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
return (
<Context.Provider
value={{
triggerRef,
open,
setOpen,
openMobile,
setOpenMobile,
isMobile,
toggleSidebar: () =>
isMobile ? setOpenMobile(!openMobile) : setOpen(!open),
}}
>
<div
data-sidebar="provider"
className={cn("flex min-h-[620px] w-full min-w-0", className)}
style={
{
"--sidebar-width": "16rem",
"--sidebar-icon-width": "4rem",
...style,
} as React.CSSProperties
}
{...props}
>
{children}
</div>
</Context.Provider>
);
}
export function Sidebar({
children,
className,
collapsible = "icon",
variant = "sidebar",
label = "Navigation",
...props
}: React.ComponentProps<"aside"> & {
collapsible?: "icon" | "offcanvas" | "none";
variant?: "sidebar" | "inset" | "floating";
label?: string;
}) {
const { triggerRef, open, openMobile, setOpenMobile, isMobile } =
useSidebar();
const collapsed = !open && collapsible !== "none";
if (isMobile)
return (
<Dialog.Root open={openMobile} onOpenChange={setOpenMobile}>
<Dialog.Portal>
<Dialog.Overlay className="jez-overlay fixed inset-0 z-40 bg-black/35" />
<Dialog.Content
onCloseAutoFocus={(e) => {
e.preventDefault();
triggerRef.current?.focus();
}}
aria-describedby={undefined}
className="jez-sheet fixed inset-y-0 left-0 z-50 flex w-[min(20rem,90vw)] flex-col border-r border-border bg-background text-foreground shadow-xl"
>
<Dialog.Title className="sr-only">{label}</Dialog.Title>
<Dialog.Close
aria-label="Close sidebar"
className="absolute right-2 top-2 z-10 rounded-md bg-background p-1.5"
>
<X size={16} />
</Dialog.Close>
<aside
data-sidebar="sidebar"
data-state="expanded"
aria-label={label}
className={cn(
"group/sidebar flex min-h-0 flex-1 flex-col",
className,
)}
{...props}
>
{children}
</aside>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
return (
<aside
data-sidebar="sidebar"
data-state={collapsed ? "collapsed" : "expanded"}
aria-label={label}
className={cn(
"group/sidebar flex shrink-0 flex-col border-r border-border bg-background text-foreground transition-[width] duration-200 motion-reduce:transition-none",
collapsed
? collapsible === "offcanvas"
? "hidden"
: "w-[var(--sidebar-icon-width)]"
: "w-[var(--sidebar-width)]",
variant === "floating" && "m-2 rounded-xl border shadow-sm",
variant === "inset" && "border-0 bg-transparent",
className,
)}
{...props}
>
{children}
</aside>
);
}
export function SidebarHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-sidebar="header"
className={cn("shrink-0 p-3", className)}
{...props}
/>
);
}
export function SidebarContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-sidebar="content"
className={cn(
"min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-2",
className,
)}
{...props}
/>
);
}
export function SidebarFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-sidebar="footer"
className={cn("mt-auto shrink-0 border-t border-border p-3", className)}
{...props}
/>
);
}
export function SidebarGroup({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-sidebar="group"
className={cn("relative py-3", className)}
{...props}
/>
);
}
export function SidebarGroupLabel({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn(
"mb-2 px-3 text-[11px] font-medium text-muted-foreground group-data-[state=collapsed]/sidebar:sr-only",
className,
)}
{...props}
/>
);
}
export function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return <div className={cn("w-full", className)} {...props} />;
}
export function SidebarMenu({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-sidebar="menu"
className={cn("m-0 grid list-none gap-1 p-0", className)}
{...props}
/>
);
}
export function SidebarMenuItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-sidebar="menu-item"
className={cn("relative min-w-0", className)}
{...props}
/>
);
}
export function SidebarMenuButton({
asChild = false,
isActive = false,
className,
onClick,
children,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean; isActive?: boolean }) {
const { setOpenMobile } = useSidebar();
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
type={asChild ? undefined : "button"}
data-active={isActive}
aria-current={isActive ? "page" : undefined}
className={cn(
"flex min-h-9 w-full min-w-0 items-center gap-3 rounded-lg px-3 py-2 text-left text-sm outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 data-[active=true]:bg-primary/10 data-[active=true]:font-medium data-[active=true]:text-primary [&_svg]:size-4 [&_svg]:shrink-0 group-data-[state=collapsed]/sidebar:justify-center group-data-[state=collapsed]/sidebar:px-2 group-data-[state=collapsed]/sidebar:[&>span]:sr-only",
className,
)}
onClick={(e) => {
onClick?.(e);
if (!e.defaultPrevented) setOpenMobile(false);
}}
{...props}
>
{children}
</Comp>
);
}
export function SidebarMenuSub({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
className={cn(
"my-1 ml-5 grid list-none gap-1 border-l border-border pl-3 group-data-[state=collapsed]/sidebar:hidden",
className,
)}
{...props}
/>
);
}
export function SidebarMenuSubItem(props: React.ComponentProps<"li">) {
return <SidebarMenuItem {...props} />;
}
export function SidebarMenuSubButton(
props: React.ComponentProps<typeof SidebarMenuButton>,
) {
return <SidebarMenuButton {...props} />;
}
export function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
className={cn(
"pointer-events-none absolute right-3 top-2.5 text-xs tabular-nums text-muted-foreground group-data-[state=collapsed]/sidebar:hidden",
className,
)}
{...props}
/>
);
}
export function SidebarInset({
className,
...props
}: React.ComponentProps<"main">) {
return (
<main
className={cn("min-w-0 flex-1 bg-background", className)}
{...props}
/>
);
}
export function SidebarTrigger({
className,
onClick,
children,
...props
}: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar();
return (
<button
type="button"
aria-label="Toggle sidebar"
className={cn(
"grid size-9 place-items-center rounded-md hover:bg-muted focus-visible:outline-2 focus-visible:outline-primary",
className,
)}
onClick={(e) => {
onClick?.(e);
if (!e.defaultPrevented) toggleSidebar();
}}
{...props}
>
{children ?? <PanelLeft size={17} />}
</button>
);
}
Dependencies
lucide-react@0.577.0 · radix-ui@1.6.7 · clsx@2.1.1 · tailwind-merge@3.5.0
Accessibility & behaviour
Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.
Read the accessibility and performance guide →