Sidebar
Basic
// Package sidebar holds the site's example gsx components for ui/sidebar.
package sidebar
import (
"github.com/gsxhq/gsxui/ui"
"github.com/gsxhq/gsxui/ui/icon"
)
// Basic mirrors shadcn's own sidebar-demo.tsx shape: a header, a grouped
// menu (SidebarGroup > SidebarGroupLabel + SidebarMenu), a footer, a
// trigger, and SidebarInset holding the "page" content next to it.
component Basic() {
<ui.SidebarProvider open={true} class="min-h-[32rem] rounded-lg border">
<ui.Sidebar open={true}>
<ui.SidebarHeader>
<div class="px-2 py-1 text-sm font-semibold">Acme Inc</div>
<ui.SidebarInput placeholder="Search navigation"/>
</ui.SidebarHeader>
<ui.SidebarSeparator/>
<ui.SidebarContent>
<ui.SidebarGroup>
<ui.SidebarGroupLabel>Application</ui.SidebarGroupLabel>
<ui.SidebarGroupAction aria-label="Add item">
<icon.Plus/>
</ui.SidebarGroupAction>
<ui.SidebarGroupContent>
<ui.SidebarMenu>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton href="#home" isActive={true} tooltip="Home">
<icon.House/>
<span>Home</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton variant="outline" size="sm">
<icon.Inbox/>
<span>Inbox</span>
</ui.SidebarMenuButton>
<ui.SidebarMenuAction showOnHover={true} aria-label="More">
<icon.Plus/>
</ui.SidebarMenuAction>
<ui.SidebarMenuSub>
<ui.SidebarMenuSubItem>
<ui.SidebarMenuSubButton size="sm" isActive={true}>Unread</ui.SidebarMenuSubButton>
</ui.SidebarMenuSubItem>
<ui.SidebarMenuSubItem>
<ui.SidebarMenuSubButton size="md">Archived</ui.SidebarMenuSubButton>
</ui.SidebarMenuSubItem>
</ui.SidebarMenuSub>
</ui.SidebarMenuItem>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton size="lg">
<icon.Calendar/>
<span>Calendar</span>
</ui.SidebarMenuButton>
<ui.SidebarMenuBadge>24</ui.SidebarMenuBadge>
</ui.SidebarMenuItem>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton>
<icon.Search/>
<span>Search</span>
</ui.SidebarMenuButton>
<ui.SidebarMenuAction aria-label="Pin">
<icon.Plus/>
</ui.SidebarMenuAction>
</ui.SidebarMenuItem>
</ui.SidebarMenu>
<ui.SidebarMenuSkeleton showIcon={true}/>
<ui.SidebarMenuSkeleton/>
</ui.SidebarGroupContent>
</ui.SidebarGroup>
</ui.SidebarContent>
<ui.SidebarFooter>
<ui.SidebarMenu>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton tooltip="Settings">
<icon.Settings/>
<span>Settings</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
</ui.SidebarMenu>
</ui.SidebarFooter>
<ui.SidebarRail/>
</ui.Sidebar>
<ui.SidebarInset>
<header class="flex h-12 items-center gap-2 border-b px-4">
<ui.SidebarTrigger/>
<span class="text-sm text-muted-foreground">Dashboard</span>
</header>
<div class="p-4 text-sm text-muted-foreground">
Toggle the sidebar with the button above, the rail at its edge, or Cmd/Ctrl+B.
</div>
</ui.SidebarInset>
</ui.SidebarProvider>
}
Variants
variant=sidebar (default)
variant=floating
variant=inset
collapsible=offcanvas (default)
collapsible=icon
collapsible=none (always expanded, no rail)
right side, collapsed
icon collapsed
package sidebar
import (
"github.com/gsxhq/gsxui/ui"
"github.com/gsxhq/gsxui/ui/icon"
)
// brand uses the same overflow behavior as the rest of an icon-collapsible
// menu: the fixed A mark remains visible while the full company name is
// clipped by SidebarMenuButton's compact state.
component brand() {
<ui.SidebarMenu>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton
size="lg"
tooltip="Acme Inc"
aria-label="Acme Inc"
data-sidebar-example-brand
>
<span
class="flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
data-sidebar-example-brand-mark
>
A
</span>
<span class="truncate font-semibold" data-sidebar-example-brand-name>Acme Inc</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
</ui.SidebarMenu>
}
// menu is the shared three-item menu every mini demo below reuses, so the
// only thing that visibly differs between them is the variant/collapsible
// axis each one is labeled for.
component menu() {
<ui.SidebarMenu>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton isActive={true} tooltip="Home">
<icon.House/>
<span>Home</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton tooltip="Inbox">
<icon.Inbox/>
<span>Inbox</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton tooltip="Settings">
<icon.Settings/>
<span>Settings</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
</ui.SidebarMenu>
}
// demo renders one SidebarProvider. The gallery runs every exported case
// below in its own iframe because each desktop Sidebar intentionally owns
// its document viewport; mounting several cases in one document would make
// their fixed containers overlap.
// SidebarRail is only meaningful alongside offcanvas/icon collapsing (it
// positions absolute against the desktop tree's own fixed sidebar-
// container, which collapsible="none"'s flat div never renders — see
// ui/sidebar.gsx's own Sidebar doc comment), so rail gates it off for that
// one case.
component demo(side string, variant string, collapsible string, open bool, rail bool) {
<ui.SidebarProvider open={open}>
<ui.Sidebar open={open} side={side} variant={variant} collapsible={collapsible}>
<ui.SidebarHeader>
<brand/>
</ui.SidebarHeader>
<ui.SidebarContent>
<ui.SidebarGroup>
<menu/>
</ui.SidebarGroup>
</ui.SidebarContent>
{ if rail {
<ui.SidebarRail/>
} }
</ui.Sidebar>
<ui.SidebarInset>
<header class="flex h-12 items-center gap-2 border-b px-4">
<ui.SidebarTrigger/>
</header>
</ui.SidebarInset>
</ui.SidebarProvider>
}
// Variants demonstrates the three `variant`s (sidebar/floating/inset) and
// the three `collapsible` modes (offcanvas/icon/none) shadcn's own registry
// exposes as separate props on the same component. It is the default case;
// the gallery registers the sibling cases below as named preview documents
// under this same source block.
component Variants() {
<demo side="" variant="" collapsible="" open={true} rail={true}/>
}
component VariantFloating() {
<demo side="" variant="floating" collapsible="" open={true} rail={true}/>
}
component VariantInset() {
<demo side="" variant="inset" collapsible="" open={true} rail={true}/>
}
component CollapsibleOffcanvas() {
<demo side="" variant="" collapsible="offcanvas" open={true} rail={true}/>
}
component CollapsibleIcon() {
<demo side="" variant="" collapsible="icon" open={true} rail={true}/>
}
component CollapsibleNone() {
<demo side="" variant="" collapsible="none" open={true} rail={false}/>
}
component RightCollapsed() {
<demo side="right" variant="" collapsible="offcanvas" open={false} rail={true}/>
}
component IconCollapsed() {
<demo side="" variant="" collapsible="icon" open={false} rail={true}/>
}
Persisted (cookie round-trip)
package sidebar
import (
"github.com/gsxhq/gsxui/ui"
"github.com/gsxhq/gsxui/ui/icon"
)
// Persisted is the copyable recipe for the one thing ui/sidebar.js
// deliberately does NOT do: remember open/collapsed across page loads.
// shadcn's own React SidebarProvider writes a non-HttpOnly `sidebar_state`
// cookie on every toggle and reads it back via a Server Component; gsxui
// drops that from the component itself (ui/sidebar.gsx's own package doc
// comment, decision 1) because a component that unilaterally picks a
// storage mechanism fights every consumer who'd rather use a Go session,
// Alpine, htmx, or nothing at all. This example is the two halves a real
// app wires up instead:
//
// 1. SERVER: read the `sidebar_state` cookie into `open` before you
// render SidebarProvider — a plain net/http example (swap in your own
// router's request/cookie types, the shape is identical):
//
// func Page(w http.ResponseWriter, r *http.Request) {
// open := true // no cookie yet: shadcn's own default is expanded
// if c, err := r.Cookie("sidebar_state"); err == nil {
// open = c.Value == "true"
// }
// ui.SidebarProvider(open, pageBody(), nil).Render(r.Context(), w)
// }
//
// 2. CLIENT: write the cookie back whenever the sidebar toggles — a
// four-line listener for the gsxui:change event sidebar.js emits on
// SidebarProvider's own wrapper (see the <script> below, which is
// this exact snippet, live). The target guard is NOT optional: gsxui:
// change is also emitted by ui/tabs.js, ui/toggle.js, ui/toggle-
// group.js and ui/resizable.js (review round 1, IMPORTANT 2) — without
// it, switching a tab or flipping a toggle ANYWHERE on a page that
// also has a sidebar would overwrite this cookie with
// "sidebar_state=undefined":
//
// document.addEventListener("gsxui:change", (e) => {
// if (!e.target.matches?.('[data-gsxui-slot-sidebar-wrapper]')) return;
// document.cookie = `sidebar_state=${e.detail.open}; path=/; max-age=604800`;
// });
//
// 604800 is SIDEBAR_COOKIE_MAX_AGE from the shadcn source (7 days) — a
// plain number here, not a gsxui constant, since the component ships no
// cookie code at all to hang one off of. This demo can't show the actual
// reload-persists round trip (a static example gallery page, not a live
// per-visitor route), but toggling it below DOES write the real cookie in
// your browser — inspect it in devtools, or check `document.cookie` in the
// console.
component Persisted() {
<div>
<ui.SidebarProvider open={true}>
<ui.Sidebar open={true} collapsible="icon">
<ui.SidebarHeader>
<brand/>
</ui.SidebarHeader>
<ui.SidebarContent>
<ui.SidebarGroup>
<ui.SidebarMenu>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton isActive={true} tooltip="Home">
<icon.House/>
<span>Home</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton tooltip="Settings">
<icon.Settings/>
<span>Settings</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
</ui.SidebarMenu>
</ui.SidebarGroup>
</ui.SidebarContent>
<ui.SidebarRail/>
</ui.Sidebar>
<ui.SidebarInset>
<header class="flex h-12 items-center gap-2 border-b px-4">
<ui.SidebarTrigger/>
<span class="text-sm text-muted-foreground">Toggle me, then check document.cookie</span>
</header>
</ui.SidebarInset>
</ui.SidebarProvider>
<script>
document.addEventListener("gsxui:change", (e) => {
if (!e.target.matches?.('[data-gsxui-slot-sidebar-wrapper]')) return;
document.cookie = `sidebar_state=; path=/; max-age=604800`;
});
</script>
</div>
}
RTL
package sidebar
import (
"github.com/gsxhq/gsxui/ui"
"github.com/gsxhq/gsxui/ui/icon"
)
// Rtl mirrors Basic's own shape (header, grouped menu, footer, trigger,
// inset content) but under dir="rtl" with side="right" — shadcn's own RTL
// convention keeps the physical side on the visual right so it still reads
// as inline-start in a right-to-left document. Everything else (icons,
// spacing, keyboard order) adapts through the same logical properties and
// rtl:rotate-180 rules Basic already exercises.
component Rtl() {
<div dir="rtl">
<ui.SidebarProvider open={true} class="min-h-[32rem] rounded-lg border">
<ui.Sidebar open={true} side="right">
<ui.SidebarHeader>
<div class="px-2 py-1 text-sm font-semibold">شركة أكمي</div>
</ui.SidebarHeader>
<ui.SidebarSeparator/>
<ui.SidebarContent>
<ui.SidebarGroup>
<ui.SidebarGroupLabel>التطبيق</ui.SidebarGroupLabel>
<ui.SidebarGroupContent>
<ui.SidebarMenu>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton isActive={true} tooltip="الرئيسية">
<icon.House/>
<span>الرئيسية</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton tooltip="البريد الوارد">
<icon.Inbox/>
<span>البريد الوارد</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
<ui.SidebarMenuItem>
<ui.SidebarMenuButton tooltip="الإعدادات">
<icon.Settings/>
<span>الإعدادات</span>
</ui.SidebarMenuButton>
</ui.SidebarMenuItem>
</ui.SidebarMenu>
</ui.SidebarGroupContent>
</ui.SidebarGroup>
</ui.SidebarContent>
<ui.SidebarRail/>
</ui.Sidebar>
<ui.SidebarInset>
<header class="flex h-12 items-center gap-2 border-b px-4">
<ui.SidebarTrigger/>
<span class="text-sm text-muted-foreground">لوحة التحكم</span>
</header>
<div class="p-4 text-sm text-muted-foreground">
بدّل الشريط الجانبي بالزر أعلاه، أو المقبض على حافته، أو Cmd/Ctrl+B.
</div>
</ui.SidebarInset>
</ui.SidebarProvider>
</div>
}