Kosmesis
Components

Sidebar

A collapsible application sidebar composing a mobile sheet and a desktop rail. No Morphos equivalent.

Installation

npx kosmesis add sidebar
pnpm dlx kosmesis add sidebar
yarn dlx kosmesis add sidebar
bunx kosmesis add sidebar

Install the following dependencies:

npm install @morphos/core
pnpm add @morphos/core
yarn add @morphos/core
bun add @morphos/core

Copy and paste the following code into your project.

sidebar.tsx
import { cva, type VariantProps } from "class-variance-authority";import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { Component, Emit, Prop, State } from "@praxisjs/decorators";import { Portal } from "@praxisjs/runtime";import type { Children } from "@praxisjs/shared";import { Keys } from "@morphos/core";import { cn } from "@/lib/utils";/** * Purely presentational + a small state container — no Morphos equivalent. Radix doesn't have a * sidebar primitive either; shadcn/ui's own `Sidebar` is a composition of `Sheet` (mobile), * `Button`, `Separator`, and plain divs, which is exactly what this does. There's no context * system in PraxisJS, so `SidebarState` follows the same "instantiate once, pass to every part * as a prop" pattern as every Morphos compound component: * * ```tsx * @State() sidebar = new SidebarState() * // ... * <SidebarProvider sidebar={this.sidebar}> *   <Sidebar sidebar={this.sidebar}>...</Sidebar> *   <SidebarInset>...</SidebarInset> * </SidebarProvider> * ``` */@Component()export class SidebarState extends StatefulComponent {  @Prop() defaultOpen = true;  @Prop() onOpenChange?: (open: boolean) => void;  @State() _open = true;  @State() _openMobile = false;  onBeforeMount() {    this._open = this.defaultOpen;  }  get open(): boolean {    return this._open;  }  get openMobile(): boolean {    return this._openMobile;  }  @Emit("onOpenChange")  toggle(): boolean {    this._open = !this._open;    return this._open;  }  toggleMobile(): void {    this._openMobile = !this._openMobile;  }  /** Toggles the mobile sheet on narrow viewports, the desktop rail otherwise. Used by `SidebarTrigger`. */  toggleResponsive(): void {    const isMobile = typeof window !== "undefined" && window.matchMedia("(max-width: 767px)").matches;    if (isMobile) {      this.toggleMobile();    } else {      this.toggle();    }  }  closeMobile(): void {    this._openMobile = false;  }  setMobile(value: boolean): void {    this._openMobile = value;  }  /** Pure state container — never mounted via JSX, only instantiated directly. */  render() {    return null;  }}export interface SidebarProviderProps {  sidebar: SidebarState;  class?: string;  children?: Children;}@Component()export class SidebarProvider extends StatelessComponent<SidebarProviderProps> {  render() {    const { sidebar, class: cls, children } = this.props;    return (      <div        data-slot="sidebar-provider"        data-state={() => (sidebar.open ? "expanded" : "collapsed")}        style={{ "--sidebar-width": "16rem", "--sidebar-width-icon": "3rem" }}        class={cn("group/sidebar-wrapper flex min-h-svh w-full", cls)}      >        {children}      </div>    );  }}export interface SidebarProps {  sidebar: SidebarState;  side?: "left" | "right";  collapsible?: "offcanvas" | "icon" | "none";  class?: string;  children?: Children;}@Component()export class Sidebar extends StatelessComponent<SidebarProps> {  render() {    const { sidebar, side = "left", collapsible = "offcanvas", class: cls, children } = this.props;    if (collapsible === "none") {      return (        <div          data-slot="sidebar"          class={cn("flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground", cls)}        >          {children}        </div>      );    }    return (      <>        <div style={{ display: "contents" }}>          {() =>            sidebar.openMobile && (              <Portal>                <div data-morphos-backdrop="" class="md:hidden" onClick={() => { sidebar.closeMobile(); }} />                <div                  role="dialog"                  aria-modal={"true" as const}                  data-slot="sidebar-mobile"                  data-side={side}                  class={cn(                    "fixed inset-y-0 z-50 flex h-svh w-(--sidebar-width) flex-col bg-sidebar p-0 text-sidebar-foreground md:hidden",                    side === "left" ? "left-0 border-r" : "right-0 border-l",                  )}                  onKeyDown={(event: KeyboardEvent) => {                    if (event.key === Keys.Escape) sidebar.closeMobile();                  }}                >                  {children}                </div>              </Portal>            )          }        </div>        <div          data-slot="sidebar-container"          data-state={() => (sidebar.open ? "expanded" : "collapsed")}          data-collapsible={() => (sidebar.open ? "" : collapsible)}          data-side={side}          class="hidden text-sidebar-foreground md:block"        >          <div            data-slot="sidebar-gap"            class={cn(              "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",              "group-data-[collapsible=offcanvas]:w-0",              "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",            )}          />          <div            data-slot="sidebar-inner"            class={cn(              "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",              side === "left"                ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"                : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",              "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",              side === "left" ? "border-r" : "border-l",              cls,            )}          >            <div data-slot="sidebar" class="flex h-full w-full flex-col bg-sidebar">              {children}            </div>          </div>        </div>      </>    );  }}export interface SidebarTriggerProps {  sidebar: SidebarState;  class?: string;  children?: Children;}@Component()export class SidebarTrigger extends StatelessComponent<SidebarTriggerProps> {  render() {    const { sidebar, class: cls, children } = this.props;    return (      <button        type="button"        data-slot="sidebar-trigger"        aria-label="Toggle sidebar"        class={cn(          "inline-flex size-7 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground",          cls,        )}        onClick={() => { sidebar.toggleResponsive(); }}      >        {children ?? "☰"}      </button>    );  }}export interface SidebarSlotProps {  class?: string;  children?: Children;}@Component()export class SidebarInset extends StatelessComponent<SidebarSlotProps> {  render() {    const { class: cls, children } = this.props;    return (      <main        data-slot="sidebar-inset"        class={cn("relative flex w-full flex-1 flex-col bg-background", cls)}      >        {children}      </main>    );  }}@Component()export class SidebarHeader extends StatelessComponent<SidebarSlotProps> {  render() {    const { class: cls, children } = this.props;    return <div data-slot="sidebar-header" class={cn("flex flex-col gap-2 p-2", cls)}>{children}</div>;  }}@Component()export class SidebarFooter extends StatelessComponent<SidebarSlotProps> {  render() {    const { class: cls, children } = this.props;    return <div data-slot="sidebar-footer" class={cn("flex flex-col gap-2 p-2", cls)}>{children}</div>;  }}@Component()export class SidebarContent extends StatelessComponent<SidebarSlotProps> {  render() {    const { class: cls, children } = this.props;    return (      <div        data-slot="sidebar-content"        class={cn("flex min-h-0 flex-1 flex-col gap-2 overflow-auto", cls)}      >        {children}      </div>    );  }}@Component()export class SidebarGroup extends StatelessComponent<SidebarSlotProps> {  render() {    const { class: cls, children } = this.props;    return (      <div data-slot="sidebar-group" class={cn("relative flex w-full min-w-0 flex-col p-2", cls)}>        {children}      </div>    );  }}@Component()export class SidebarGroupLabel extends StatelessComponent<SidebarSlotProps> {  render() {    const { class: cls, children } = this.props;    return (      <div        data-slot="sidebar-group-label"        class={cn("flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70", cls)}      >        {children}      </div>    );  }}@Component()export class SidebarGroupContent extends StatelessComponent<SidebarSlotProps> {  render() {    const { class: cls, children } = this.props;    return <div data-slot="sidebar-group-content" class={cn("w-full text-sm", cls)}>{children}</div>;  }}@Component()export class SidebarMenu extends StatelessComponent<SidebarSlotProps> {  render() {    const { class: cls, children } = this.props;    return (      <ul data-slot="sidebar-menu" class={cn("flex w-full min-w-0 flex-col gap-1", cls)}>        {children}      </ul>    );  }}@Component()export class SidebarMenuItem extends StatelessComponent<SidebarSlotProps> {  render() {    const { class: cls, children } = this.props;    return (      <li data-slot="sidebar-menu-item" class={cn("group/menu-item relative", cls)}>        {children}      </li>    );  }}const sidebarMenuButtonVariants = cva(  "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",  {    variants: {      variant: {        default: "",        outline: "border border-sidebar-border shadow-xs hover:shadow-sm",      },      size: {        default: "h-8 text-sm",        sm: "h-7 text-xs",        lg: "h-12 text-sm",      },    },    defaultVariants: {      variant: "default",      size: "default",    },  },);export interface SidebarMenuButtonProps extends VariantProps<typeof sidebarMenuButtonVariants> {  as?: "button" | "a";  href?: string;  isActive?: boolean;  onClick?: (event: MouseEvent) => void;  class?: string;  children?: Children;}@Component()export class SidebarMenuButton extends StatelessComponent<SidebarMenuButtonProps> {  render() {    const { as: Tag = "button", href, isActive, variant, size, onClick, class: cls, children } = this.props;    return (      <Tag        type={Tag === "button" ? "button" : undefined}        href={Tag === "a" ? href : undefined}        data-slot="sidebar-menu-button"        data-active={isActive ? "" : undefined}        onClick={onClick}        class={cn(sidebarMenuButtonVariants({ variant, size }), cls)}      >        {children}      </Tag>    );  }}@Component()export class SidebarSeparator extends StatelessComponent<SidebarSlotProps> {  render() {    const { class: cls } = this.props;    return <div role="separator" data-slot="sidebar-separator" class={cn("mx-2 my-2 h-px bg-sidebar-border", cls)} />;  }}

Install the following dependencies:

npm install @morphos/core @praxisjs/css
pnpm add @morphos/core @praxisjs/css
yarn add @morphos/core @praxisjs/css
bun add @morphos/core @praxisjs/css

Copy and paste the following code into your project.

sidebar.tsx
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, Emit, Prop, State } from "@praxisjs/decorators";import { Portal } from "@praxisjs/runtime";import type { Children } from "@praxisjs/shared";import { Keys } from "@morphos/core";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);/** * Purely presentational + a small state container — no Morphos equivalent. Radix doesn't have a * sidebar primitive either; shadcn/ui's own `Sidebar` is a composition of `Sheet` (mobile), * `Button`, `Separator`, and plain divs, which is exactly what this does. There's no context * system in PraxisJS, so `SidebarState` follows the same "instantiate once, pass to every part * as a prop" pattern as every Morphos compound component: * * ```tsx * @State() sidebar = new SidebarState() * // ... * <SidebarProvider sidebar={this.sidebar}> *   <Sidebar sidebar={this.sidebar}>...</Sidebar> *   <SidebarInset>...</SidebarInset> * </SidebarProvider> * ``` */@Component()export class SidebarState extends StatefulComponent {  @Prop() defaultOpen = true;  @Prop() onOpenChange?: (open: boolean) => void;  @State() _open = true;  @State() _openMobile = false;  onBeforeMount() {    this._open = this.defaultOpen;  }  get open(): boolean {    return this._open;  }  get openMobile(): boolean {    return this._openMobile;  }  @Emit("onOpenChange")  toggle(): boolean {    this._open = !this._open;    return this._open;  }  toggleMobile(): void {    this._openMobile = !this._openMobile;  }  /** Toggles the mobile sheet on narrow viewports, the desktop rail otherwise. Used by `SidebarTrigger`. */  toggleResponsive(): void {    const isMobile = typeof window !== "undefined" && window.matchMedia("(max-width: 767px)").matches;    if (isMobile) {      this.toggleMobile();    } else {      this.toggle();    }  }  closeMobile(): void {    this._openMobile = false;  }  setMobile(value: boolean): void {    this._openMobile = value;  }  /** Pure state container — never mounted via JSX, only instantiated directly. */  render() {    return null;  }}class SidebarStyles extends Stylesheet {  $provider = this.css({ display: "flex", minHeight: "100svh", width: "100%" });  $sidebarStatic = this.css({ display: "flex", height: "100%", width: "16rem", flexDirection: "column", backgroundColor: t.sidebar, color: t.sidebarForeground });  $mobileBackdrop = this.css({}).media("(min-width: 768px)", { display: "none" });  $mobilePanel = this.css({    position: "fixed",    top: "0",    bottom: "0",    zIndex: 50,    display: "flex",    height: "100svh",    width: "16rem",    flexDirection: "column",    backgroundColor: t.sidebar,    color: t.sidebarForeground,  })    .media("(min-width: 768px)", { display: "none" })    .on('&[data-side="left"]', { left: "0", borderRight: `1px solid ${t.sidebarBorder}` })    .on('&[data-side="right"]', { right: "0", borderLeft: `1px solid ${t.sidebarBorder}` });  $desktopContainer = this.css({ display: "none", color: t.sidebarForeground }).media("(min-width: 768px)", { display: "block" });  $desktopGap = this.css({ position: "relative", width: "16rem", backgroundColor: "transparent", transition: "width 200ms ease-linear" })    .on('&[data-collapsible="offcanvas"]', { width: "0" })    .on('&[data-collapsible="icon"]', { width: "3rem" });  $desktopInner = this.css({    position: "fixed",    top: "0",    bottom: "0",    zIndex: 10,    display: "none",    height: "100svh",    width: "16rem",    transition: "left 200ms ease-linear, right 200ms ease-linear, width 200ms ease-linear",  })    .media("(min-width: 768px)", { display: "flex" })    .on('&[data-collapsible="icon"]', { width: "3rem" });  $desktopInnerLeft = this.css({ left: "0", borderRight: `1px solid ${t.sidebarBorder}` }).on(    '[data-collapsible="offcanvas"] &',    { left: "-16rem" },  );  $desktopInnerRight = this.css({ right: "0", borderLeft: `1px solid ${t.sidebarBorder}` }).on(    '[data-collapsible="offcanvas"] &',    { right: "-16rem" },  );  $sidebarInner = this.css({ display: "flex", height: "100%", width: "100%", flexDirection: "column", backgroundColor: t.sidebar });  $trigger = this.css({    display: "inline-flex",    width: "1.75rem",    height: "1.75rem",    alignItems: "center",    justifyContent: "center",    borderRadius: `calc(${t.radius} - 2px)`,  }).hover({ backgroundColor: t.accent, color: t.accentForeground });  $inset = this.css({ position: "relative", display: "flex", width: "100%", flex: "1 1 0%", flexDirection: "column", backgroundColor: t.background });  $header = this.css({ display: "flex", flexDirection: "column", gap: "0.5rem", padding: "0.5rem" });  $footer = this.css({ display: "flex", flexDirection: "column", gap: "0.5rem", padding: "0.5rem" });  $content = this.css({ display: "flex", minHeight: "0", flex: "1 1 0%", flexDirection: "column", gap: "0.5rem", overflow: "auto" });  $group = this.css({ position: "relative", display: "flex", width: "100%", minWidth: "0", flexDirection: "column", padding: "0.5rem" });  $groupLabel = this.css({    display: "flex",    height: "2rem",    flexShrink: 0,    alignItems: "center",    borderRadius: `calc(${t.radius} - 2px)`,    padding: "0 0.5rem",    fontSize: "0.75rem",    fontWeight: 500,    color: `color-mix(in oklab, ${t.sidebarForeground} 70%, transparent)`,  });  $groupContent = this.css({ width: "100%", fontSize: "0.875rem" });  $menu = this.css({ display: "flex", width: "100%", minWidth: "0", flexDirection: "column", gap: "0.25rem" });  $menuItem = this.css({ position: "relative" });  $separator = this.css({ margin: "0.5rem", height: "1px", backgroundColor: t.sidebarBorder });  $menuButton = this.css({    display: "flex",    width: "100%",    alignItems: "center",    gap: "0.5rem",    overflow: "hidden",    borderRadius: `calc(${t.radius} - 2px)`,    padding: "0.5rem",    textAlign: "left",    fontSize: "0.875rem",    outline: "none",    transition: "width 120ms ease, height 120ms ease, padding 120ms ease",  })    .hover({ backgroundColor: t.sidebarAccent, color: t.sidebarAccentForeground })    .focusVisible({ boxShadow: `0 0 0 2px ${t.sidebarRing}` })    .disabled({ pointerEvents: "none", opacity: 0.5 })    .on("&[data-active]", { backgroundColor: t.sidebarAccent, fontWeight: 500, color: t.sidebarAccentForeground })    .on("& > svg", { width: "1rem", height: "1rem", flexShrink: 0 });  $menuButtonVariantOutline = this.css({ border: `1px solid ${t.sidebarBorder}`, boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)" });  $menuButtonSizeDefault = this.css({ height: "2rem", fontSize: "0.875rem" });  $menuButtonSizeSm = this.css({ height: "1.75rem", fontSize: "0.75rem" });  $menuButtonSizeLg = this.css({ height: "3rem", fontSize: "0.875rem" });}export interface SidebarProviderProps {  sidebar: SidebarState;  class?: string;  children?: Children;}@Component()export class SidebarProvider extends StatelessComponent<SidebarProviderProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { sidebar, class: cls, children } = this.props;    return (      <div data-slot="sidebar-provider" data-state={() => (sidebar.open ? "expanded" : "collapsed")} class={cx(this.$s.$provider, cls)}>        {children}      </div>    );  }}export interface SidebarProps {  sidebar: SidebarState;  side?: "left" | "right";  collapsible?: "offcanvas" | "icon" | "none";  class?: string;  children?: Children;}@Component()export class Sidebar extends StatelessComponent<SidebarProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { sidebar, side = "left", collapsible = "offcanvas", class: cls, children } = this.props;    if (collapsible === "none") {      return (        <div data-slot="sidebar" class={cx(this.$s.$sidebarStatic, cls)}>          {children}        </div>      );    }    return (      <>        <div style={{ display: "contents" }}>          {() =>            sidebar.openMobile && (              <Portal>                <div data-morphos-backdrop="" class={this.$s.$mobileBackdrop} onClick={() => { sidebar.closeMobile(); }} />                <div                  role="dialog"                  aria-modal={"true" as const}                  data-slot="sidebar-mobile"                  data-side={side}                  class={this.$s.$mobilePanel}                  onKeyDown={(event: KeyboardEvent) => {                    if (event.key === Keys.Escape) sidebar.closeMobile();                  }}                >                  {children}                </div>              </Portal>            )          }        </div>        <div          data-slot="sidebar-container"          data-state={() => (sidebar.open ? "expanded" : "collapsed")}          data-collapsible={() => (sidebar.open ? "" : collapsible)}          data-side={side}          class={this.$s.$desktopContainer}        >          <div data-slot="sidebar-gap" class={this.$s.$desktopGap} />          <div            data-slot="sidebar-inner"            class={cx(this.$s.$desktopInner, side === "left" ? this.$s.$desktopInnerLeft : this.$s.$desktopInnerRight, cls)}          >            <div data-slot="sidebar" class={this.$s.$sidebarInner}>              {children}            </div>          </div>        </div>      </>    );  }}export interface SidebarTriggerProps {  sidebar: SidebarState;  class?: string;  children?: Children;}@Component()export class SidebarTrigger extends StatelessComponent<SidebarTriggerProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { sidebar, class: cls, children } = this.props;    return (      <button type="button" data-slot="sidebar-trigger" aria-label="Toggle sidebar" class={cx(this.$s.$trigger, cls)} onClick={() => { sidebar.toggleResponsive(); }}>        {children ?? "☰"}      </button>    );  }}export interface SidebarSlotProps {  class?: string;  children?: Children;}@Component()export class SidebarInset extends StatelessComponent<SidebarSlotProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { class: cls, children } = this.props;    return (      <main data-slot="sidebar-inset" class={cx(this.$s.$inset, cls)}>        {children}      </main>    );  }}@Component()export class SidebarHeader extends StatelessComponent<SidebarSlotProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { class: cls, children } = this.props;    return <div data-slot="sidebar-header" class={cx(this.$s.$header, cls)}>{children}</div>;  }}@Component()export class SidebarFooter extends StatelessComponent<SidebarSlotProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { class: cls, children } = this.props;    return <div data-slot="sidebar-footer" class={cx(this.$s.$footer, cls)}>{children}</div>;  }}@Component()export class SidebarContent extends StatelessComponent<SidebarSlotProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { class: cls, children } = this.props;    return (      <div data-slot="sidebar-content" class={cx(this.$s.$content, cls)}>        {children}      </div>    );  }}@Component()export class SidebarGroup extends StatelessComponent<SidebarSlotProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { class: cls, children } = this.props;    return (      <div data-slot="sidebar-group" class={cx(this.$s.$group, cls)}>        {children}      </div>    );  }}@Component()export class SidebarGroupLabel extends StatelessComponent<SidebarSlotProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { class: cls, children } = this.props;    return (      <div data-slot="sidebar-group-label" class={cx(this.$s.$groupLabel, cls)}>        {children}      </div>    );  }}@Component()export class SidebarGroupContent extends StatelessComponent<SidebarSlotProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { class: cls, children } = this.props;    return <div data-slot="sidebar-group-content" class={cx(this.$s.$groupContent, cls)}>{children}</div>;  }}@Component()export class SidebarMenu extends StatelessComponent<SidebarSlotProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { class: cls, children } = this.props;    return (      <ul data-slot="sidebar-menu" class={cx(this.$s.$menu, cls)}>        {children}      </ul>    );  }}@Component()export class SidebarMenuItem extends StatelessComponent<SidebarSlotProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { class: cls, children } = this.props;    return (      <li data-slot="sidebar-menu-item" class={cx(this.$s.$menuItem, cls)}>        {children}      </li>    );  }}export type SidebarMenuButtonVariant = "default" | "outline";export type SidebarMenuButtonSize = "default" | "sm" | "lg";export interface SidebarMenuButtonProps {  variant?: SidebarMenuButtonVariant;  size?: SidebarMenuButtonSize;  as?: "button" | "a";  href?: string;  isActive?: boolean;  onClick?: (event: MouseEvent) => void;  class?: string;  children?: Children;}@Component()export class SidebarMenuButton extends StatelessComponent<SidebarMenuButtonProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { as: Tag = "button", variant = "default", size = "default", href, isActive, onClick, class: cls, children } = this.props;    const variants: Record<SidebarMenuButtonVariant, string> = { default: "", outline: this.$s.$menuButtonVariantOutline };    const sizes: Record<SidebarMenuButtonSize, string> = {      default: this.$s.$menuButtonSizeDefault,      sm: this.$s.$menuButtonSizeSm,      lg: this.$s.$menuButtonSizeLg,    };    return (      <Tag        type={Tag === "button" ? "button" : undefined}        href={Tag === "a" ? href : undefined}        data-slot="sidebar-menu-button"        data-active={isActive ? "" : undefined}        onClick={onClick}        class={cx(this.$s.$menuButton, variants[variant], sizes[size], cls)}      >        {children}      </Tag>    );  }}@Component()export class SidebarSeparator extends StatelessComponent<SidebarSlotProps> {  @Styled(SidebarStyles) $s!: SidebarStyles;  render() {    const { class: cls } = this.props;    return <div role="separator" data-slot="sidebar-separator" class={cx(this.$s.$separator, cls)} />;  }}

Examples

Usage

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarState,
  SidebarTrigger,
} from "@/components/ui/sidebar";

@Component()
class AppShell extends StatefulComponent {
  @State() sidebar = new SidebarState();

  render() {
    return (
      <SidebarProvider sidebar={this.sidebar}>
        <Sidebar sidebar={this.sidebar}>
          <SidebarHeader>My App</SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuButton as="a" href="/" isActive>Home</SidebarMenuButton>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton as="a" href="/settings">Settings</SidebarMenuButton>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
          <SidebarFooter>v1.0.0</SidebarFooter>
        </Sidebar>
        <SidebarInset>
          <SidebarTrigger sidebar={this.sidebar} />
          <main>Page content</main>
        </SidebarInset>
      </SidebarProvider>
    );
  }
}

No Morphos primitive to wrap here — Radix doesn't have a sidebar primitive either, so shadcn/ui's own Sidebar is a composition of Sheet (mobile), buttons, and plain divs, and this mirrors that. SidebarState is a small custom state container (not a Morphos class) — it's fully self-contained, no dual-instantiation concerns.

Notes

  • SidebarTrigger checks a (max-width: 767px) media query at click time to decide whether to toggle the desktop rail or the mobile sheet.
  • Theming uses dedicated --sidebar, --sidebar-foreground, --sidebar-accent, etc. CSS variables (already wired up by kosmesis init), separate from the page's --background, so a dark sidebar next to a light page (or vice versa) is just a token override away.

Props

SidebarState

Not mounted via JSX — instantiate once (@State() sidebar = new SidebarState()) and pass it to every part below.

PropTypeDefault
defaultOpenbooleantrue
onOpenChange(open: boolean) => void

SidebarProvider

PropTypeDefault
sidebarSidebarState— required
PropTypeDefault
sidebarSidebarState— required
side"left" | "right""left"
collapsible"offcanvas" | "icon" | "none""offcanvas"

SidebarTrigger

PropTypeDefault
sidebarSidebarState— required
childrenChildren"☰"

SidebarMenuButton

PropTypeDefault
as"button" | "a""button"
hrefstring— only used when as="a"
isActiveboolean
onClick(event: MouseEvent) => void
variant"default" | "outline""default"
size"default" | "sm" | "lg""default"

SidebarInset, SidebarHeader, SidebarFooter, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, and SidebarSeparator are layout-only — no props beyond class/children.

On this page