Skip to content
Domphy

sidebar06

A Sidebar block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call sidebar06() with no arguments for a working demo, or edit the code below live.

Props

PropTypeDescription
header{ icon?: string; title?: string; subtitle?: string }
navItemsSidebar06NavItem[]
optInCardSidebar06OptInCard | null
breadcrumbItemsSidebarBreadcrumbItem[]
childrenDomphyElement | DomphyElement[]
Implementation notes

Floating-dropdown nav built on @domphy/ui's popover()+menu(). Only one dropdown open at a time via cross-subscribed State<boolean> instances (opening one closes all siblings). Trigger row gets accent background/foreground via the '&[aria-expanded=true]' CSS selector. Opt-in card + footer included. Same off-canvas mobile shell as sidebar05. PARTIAL because responsive dropdown placement (below-trailing on mobile vs beside-leading on desktop) is approximated with a matchMedia listener flipping a shared placement State between 'bottom-end'/'right-start', combined with @domphy/floating's flip()/shift() middleware for on-screen collision handling — not a bespoke anchor-engine breakpoint rule. Doctor self-check: 0 issues. Direct-source-diff fix (2026-07-05): The opt-in card was button-only — upstream's is a real <form> with an email input above the Subscribe button. Fixed.

Status: partial · Reference: shadcn/ui original

// shadcn/ui "sidebar-06" — clean-room reimplementation from the public behavior
// description only (no upstream source viewed). Nav rows never expand inline;
// clicking a top-level row opens a floating dropdown of its children instead,
// keeping the sidebar's vertical rhythm constant. Only one dropdown is open
// at a time, and its placement flips between "below" (mobile) and "beside"
// (desktop) so it never collides with the sidebar's own edge.

import type { DomphyElement, ElementNode, Listener, State } from "@domphy/core";
import { behavior, toState } from "@domphy/core";
import { button, inputText, menu, popover, small, strong } from "@domphy/ui";

/** The two placements this sidebar's dropdowns flip between (subset of
 * @domphy/floating's full `Placement` union — narrowed locally so this
 * package doesn't need a direct dependency on @domphy/floating). */
type DropdownPlacement = "right-start" | "bottom-end";

import { themeColor, themeDensity, themeSpacing } from "@domphy/theme";
import {
  glyphChild,
  ICON_MARK,
  ICON_MORE,
  type SidebarBreadcrumbItem,
  sidebarBackdrop,
  sidebarIcon,
  sidebarMainContent,
  sidebarStickyHeader,
  srOnlyLabel,
} from "./sidebar05-08-shared.js";

/** A child row inside a top-level item's floating dropdown; navigates to
 * `href` on click (matching upstream's `<a href={item.url}>` child rows). */
type Sidebar06ChildLink = { title: string; href?: string };

/** A top-level nav row whose children live in a floating dropdown, not inline. */
type Sidebar06NavItem = {
  title: string;
  items: Sidebar06ChildLink[];
};

/** Small bordered opt-in card shown near the bottom of the sidebar content. */
type Sidebar06OptInCard = {
  title: string;
  description: string;
  buttonLabel: string;
  onSubmit?: () => void;
};

type Sidebar06Props = {
  header?: { icon?: string; title?: string; subtitle?: string };
  navItems?: Sidebar06NavItem[];
  optInCard?: Sidebar06OptInCard | null;
  breadcrumbItems?: SidebarBreadcrumbItem[];
  children?: DomphyElement | DomphyElement[];
};

const DEFAULT_NAV_ITEMS: Sidebar06NavItem[] = [
  {
    title: "Playground",
    items: [{ title: "History" }, { title: "Starred" }, { title: "Settings" }],
  },
  {
    title: "Models",
    items: [{ title: "Genesis" }, { title: "Explorer" }, { title: "Quantum" }],
  },
  {
    title: "Documentation",
    items: [
      { title: "Introduction" },
      { title: "Get Started" },
      { title: "Tutorials" },
      { title: "Changelog" },
    ],
  },
  {
    title: "Settings",
    items: [
      { title: "General" },
      { title: "Team" },
      { title: "Billing" },
      { title: "Limits" },
    ],
  },
];

const DEFAULT_OPT_IN_CARD: Sidebar06OptInCard = {
  title: "Subscribe",
  description: "Get the latest updates delivered to your inbox.",
  buttonLabel: "Subscribe",
};

/**
 * shadcn/ui "sidebar-06" — a nav where each top-level item opens a floating
 * dropdown of its children (instead of an inline accordion). Call with no
 * arguments for a fully working demo.
 */
function sidebar06(props: Sidebar06Props = {}): DomphyElement<"div"> {
  const {
    header = { icon: ICON_MARK, title: "Acme Inc", subtitle: "v1.0.0" },
    navItems = DEFAULT_NAV_ITEMS,
    optInCard = DEFAULT_OPT_IN_CARD,
    breadcrumbItems = [{ label: "Models" }, { label: "Genesis" }],
    children,
  } = props;

  const sidebarOpen = toState(true);

  // Responsive dropdown placement: below-and-trailing on mobile, beside-and-
  // leading on desktop, so the floating panel never collides with the
  // sidebar's own edge. Shared by every row's popover.
  const placement = toState<DropdownPlacement>("right-start");

  // Only one dropdown open at a time: opening any row's popover closes all
  // the others (each row owns its own `open` State, cross-wired below).
  const openStates = navItems.map(() => toState(false));
  openStates.forEach((state, index) => {
    state.addListener((value) => {
      if (!value) return;
      openStates.forEach((other, otherIndex) => {
        if (otherIndex !== index && other.get()) other.set(false);
      });
    });
  });

  const navRows: DomphyElement<"li">[] = navItems.map((item, index) => {
    const dropdownContent: DomphyElement<"div"> = {
      div: null,
      style: { minWidth: themeSpacing(40) },
      $: [
        menu({
          items: item.items.map((child, childIndex) => ({
            label: child.title,
            key: `${item.title}-${childIndex}`,
            onClick: () => {
              if (child.href) window.location.href = child.href;
            },
          })),
        }),
      ],
    } as unknown as DomphyElement<"div">;

    return {
      li: [
        {
          button: [
            {
              span: item.title,
              style: { flex: "1", textAlign: "left" },
            } as unknown as DomphyElement,
            sidebarIcon(ICON_MORE),
          ],
          type: "button",
          ariaLabel: `${item.title} menu`,
          style: {
            display: "flex",
            alignItems: "center",
            width: "100%",
            gap: (l: Listener) => themeSpacing(themeDensity(l) * 2),
            paddingBlock: (l: Listener) => themeSpacing(themeDensity(l) * 2),
            paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
            borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 1),
            border: "none",
            cursor: "pointer",
            color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
            backgroundColor: (l: Listener) =>
              themeColor(l, "inherit", "neutral"),
            "&:hover": {
              backgroundColor: (l: Listener) =>
                themeColor(l, "shift-2", "neutral"),
            },
            // Upstream: data-[state=open]:bg-sidebar-accent /
            // text-sidebar-accent-foreground — the neutral gray accent, not a
            // brand-tinted highlight.
            "&[aria-expanded=true]": {
              backgroundColor: (l: Listener) =>
                themeColor(l, "shift-3", "neutral"),
              color: (l: Listener) => themeColor(l, "shift-12", "neutral"),
            },
          },
          $: [
            popover({
              open: openStates[index],
              placement,
              content: dropdownContent,
            }),
          ],
        } as unknown as DomphyElement,
      ],
      _key: item.title ?? index,
    } as DomphyElement<"li">;
  });

  const optInCardElement: DomphyElement<"div"> | null = optInCard
    ? ({
        div: [
          {
            strong: optInCard.title,
            $: [strong({ color: "neutral" })],
          } as unknown as DomphyElement,
          {
            small: optInCard.description,
            $: [small({ color: "neutral" })],
          } as unknown as DomphyElement,
          {
            form: [
              srOnlyLabel("Email", "sidebar06-optin-email"),
              {
                input: null,
                id: "sidebar06-optin-email",
                type: "email",
                ariaLabel: "Email",
                placeholder: "Email",
                style: { width: "100%" },
                // inputText() hardcodes type="text" (incl. an _onSchedule that
                // re-asserts it); compose a later _onSchedule to restore the
                // email type so upstream's `type="email"` validation is kept.
                $: [
                  inputText({ color: "neutral", accentColor: "neutral" }),
                  {
                    _onSchedule: (
                      _node: ElementNode,
                      element: HTMLInputElement,
                    ) => {
                      element.type = "email";
                    },
                  },
                ],
              } as unknown as DomphyElement,
              {
                button: optInCard.buttonLabel,
                type: "submit",
                dataTone: "shift-17",
                style: {
                  width: "100%",
                  // Solid near-black submit (upstream sidebar-primary button).
                  backgroundColor: (l: Listener) =>
                    themeColor(l, "inherit", "neutral"),
                  color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
                },
                $: [button({ color: "neutral" })],
              } as unknown as DomphyElement,
            ],
            // Handle submit on the form (covers both button click and
            // Enter-in-field) so the demo never triggers a real navigation.
            onSubmit: (e: Event) => {
              e.preventDefault();
              optInCard.onSubmit?.();
            },
            style: { display: "grid", gap: themeSpacing(2.5) },
          } as unknown as DomphyElement,
        ],
        dataTone: "shift-0",
        // Upstream's <SidebarFooter> wraps this card and renders as a plain
        // `<div data-slot="sidebar-footer">` (not a semantic <footer>).
        dataSlot: "sidebar-footer",
        style: {
          display: "flex",
          flexDirection: "column",
          gap: themeSpacing(2),
          margin: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          padding: (l: Listener) => themeSpacing(themeDensity(l) * 3),
          borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 2),
          outline: (l: Listener) =>
            `1px solid ${themeColor(l, "shift-3", "neutral")}`,
          outlineOffset: "-1px",
          backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
          color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
        },
      } as unknown as DomphyElement<"div">)
    : null;

  const asideElement: DomphyElement<"aside"> = {
    aside: [
      {
        // Upstream wraps the brand mark + label in a clickable <a href="#">
        // inside a SidebarMenuButton size="lg" (hover accent). Mirror the
        // team-switcher helper's SidebarHeader(p-2) + menu-button(p-2, rounded)
        // structure so the hover surface is the compact rounded button, not the
        // full-width header.
        div: [
          {
            a: [
              {
                span: glyphChild(header.icon ?? ICON_MARK),
                dataTone: "shift-17",
                style: {
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                  width: themeSpacing(8),
                  height: themeSpacing(8),
                  flexShrink: "0",
                  borderRadius: (l: Listener) =>
                    themeSpacing(themeDensity(l) * 2),
                  backgroundColor: (l: Listener) =>
                    themeColor(l, "inherit", "neutral"),
                  color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
                },
              } as unknown as DomphyElement,
              {
                div: [
                  {
                    strong: header.title ?? "Acme Inc",
                    $: [strong({ color: "neutral" })],
                  } as unknown as DomphyElement,
                  {
                    small: header.subtitle ?? "v1.0.0",
                    $: [small({ color: "neutral" })],
                  } as unknown as DomphyElement,
                ],
                style: {
                  display: "flex",
                  flexDirection: "column",
                  gap: themeSpacing(0.5),
                  minWidth: "0",
                },
              } as unknown as DomphyElement,
            ],
            href: "#",
            style: {
              display: "flex",
              alignItems: "center",
              width: "100%",
              gap: (l: Listener) => themeSpacing(themeDensity(l) * 2),
              padding: (l: Listener) => themeSpacing(themeDensity(l) * 2),
              borderRadius: (l: Listener) => themeSpacing(themeDensity(l) * 1),
              textDecoration: () => "none",
              overflow: "hidden",
              color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
              "&:hover": {
                backgroundColor: (l: Listener) =>
                  themeColor(l, "shift-2", "neutral"),
              },
            },
          } as unknown as DomphyElement,
        ],
        style: {
          flexShrink: "0",
          padding: (l: Listener) => themeSpacing(themeDensity(l) * 2),
        },
      } as unknown as DomphyElement,
      {
        nav: [
          {
            ul: navRows,
            style: {
              listStyle: "none",
              margin: "0",
              padding: "0",
              display: "flex",
              flexDirection: "column",
              gap: themeSpacing(0.5),
            },
          } as unknown as DomphyElement,
        ],
        style: {
          flex: "1",
          minHeight: "0",
          overflowY: "auto",
          paddingInline: (l: Listener) => themeSpacing(themeDensity(l) * 3),
        },
      } as unknown as DomphyElement,
      ...(optInCardElement ? [optInCardElement] : []),
      // Upstream <SidebarRail /> — thin invisible edge strip that toggles the
      // sidebar on click. Sits at the trailing edge inside the relatively-
      // positioned aside (same pattern as sidebar07).
      {
        div: null,
        ariaHidden: "true",
        onClick: () => sidebarOpen.set(!sidebarOpen.get()),
        style: {
          position: "absolute",
          insetBlock: "0",
          insetInlineEnd: "0",
          width: themeSpacing(1),
          cursor: "col-resize",
        },
      } as unknown as DomphyElement,
    ],
    dataTone: "shift-1",
    // matchMedia → placement bridge via behavior(): `attach` runs once for the
    // real aside node, and every later generation's fresh `placement` state is
    // routed into `update()` — a `_onMount` listener would keep writing to
    // generation 1's disconnected state after any ancestor re-render.
    ...behavior<{ placement: State<DropdownPlacement> }>(
      "sidebar06-responsive-placement",
      (_node, initialProps) => {
        if (typeof window === "undefined" || !window.matchMedia) {
          return undefined;
        }
        let placementState = initialProps.placement;
        const media = window.matchMedia("(max-width: 768px)");
        const apply = () =>
          placementState.set(media.matches ? "bottom-end" : "right-start");
        apply();
        const listener = () => apply();
        media.addEventListener("change", listener);
        return {
          update: (next) => {
            placementState = next.placement;
            apply();
          },
          destroy: () => media.removeEventListener("change", listener),
        };
      },
      { placement },
    ),
    style: {
      position: "relative",
      display: "flex",
      flexDirection: "column",
      flexShrink: "0",
      width: (l: Listener) => (sidebarOpen.get(l) ? themeSpacing(64) : "0"),
      overflow: "hidden",
      transition: "width 0.2s linear",
      borderInlineEnd: (l: Listener) =>
        `1px solid ${themeColor(l, "shift-3", "neutral")}`,
      backgroundColor: (l: Listener) => themeColor(l, "inherit", "neutral"),
      color: (l: Listener) => themeColor(l, "shift-9", "neutral"),
      "@media (max-width: 768px)": {
        position: "fixed",
        insetBlock: "0",
        insetInlineStart: "0",
        zIndex: "15",
        width: themeSpacing(72),
        transform: (l: Listener) =>
          sidebarOpen.get(l) ? "translateX(0)" : "translateX(-100%)",
        transition: "transform 0.2s ease",
        boxShadow: (l: Listener) =>
          `0 0 ${themeSpacing(6)} ${themeColor(l, "shift-3", "neutral")}`,
      },
    },
  } as unknown as DomphyElement<"aside">;

  const mainElement: DomphyElement<"main"> = {
    main: [
      sidebarStickyHeader({
        onToggle: () => sidebarOpen.set(!sidebarOpen.get()),
        breadcrumbItems,
      }),
      sidebarMainContent(children),
    ],
    style: {
      display: "flex",
      flexDirection: "column",
      flex: "1",
      minWidth: "0",
      minHeight: "0",
      overflow: "auto",
    },
  } as unknown as DomphyElement<"main">;

  return {
    div: [
      asideElement,
      mainElement,
      sidebarBackdrop(sidebarOpen, () => sidebarOpen.set(false)),
    ],
    style: {
      display: "flex",
      height: "100dvh",
      overflow: "hidden",
      position: "relative",
    },
  } as unknown as DomphyElement<"div">;
}

export { sidebar06 };
export type {
  Sidebar06ChildLink,
  Sidebar06NavItem,
  Sidebar06OptInCard,
  Sidebar06Props,
};

← Back to shadcn/ui catalog