Skip to content

StickyPanel

Layout

A bounded document pane with a sticky desktop sidebar and stacked mobile layout.

Sticky release summaryKeyboard-scrollable documentStacked mobileLogical start/end sidebar
Open preview

Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.

Usage

Provide short sidebar content and a longer document/body. CSS sticky keeps the sidebar inside this component’s scroll region at desktop widths, while mobile uses a normal stacked layout. The component does not pin content to the window or move DOM nodes. Keep sidebar content shorter than the pane height. side=end reverses the desktop column arrangement using logical layout, and height caps the independently scrollable region.

Import
import { StickyPanel } from "@/components/ui/sticky-panel";

Shipped as editable source in src/components/ui/sticky-panel.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.

Component source

The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.

src/components/ui/sticky-panel.tsx
import { useId, type ReactNode } from "react";
import { cn } from "@dashboardpack/core/lib/utils";
export interface StickyPanelProps {
  label: string;
  sidebar: ReactNode;
  children: ReactNode;
  height?: number;
  side?: "start" | "end";
  className?: string;
}
export function StickyPanel({
  label,
  sidebar,
  children,
  height = 400,
  side = "start",
  className,
}: StickyPanelProps) {
  const id = useId();
  return (
    <div
      role="region"
      aria-label={label}
      tabIndex={0}
      style={{
        maxHeight: Math.max(160, Number.isFinite(height) ? height : 400),
      }}
      className={cn(
        "overflow-auto rounded-xl border bg-background outline-none focus-visible:ring-2 focus-visible:ring-ring",
        className,
      )}
    >
      <div
        className={cn(
          "grid items-start gap-5 p-4 md:grid-cols-[200px_minmax(0,1fr)]",
          side === "end" && "md:grid-cols-[minmax(0,1fr)_200px]",
        )}
      >
        <aside
          aria-labelledby={id}
          className={cn(
            "min-w-0 self-start md:sticky md:top-4",
            side === "end" && "md:order-2",
          )}
        >
          <span id={id} className="sr-only">
            {label} sidebar
          </span>
          {sidebar}
        </aside>
        <div className="min-w-0 space-y-5">{children}</div>
      </div>
    </div>
  );
}

API reference

Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.

StickyPanel props
PropType / DefaultDescription
labelstring

Default: required

Accessible pane and sidebar context.
sidebarReactNode

Default: required

Short sidebar content.
childrenReactNode

Default: required

Document/body content.
heightnumber

Default: 400

Maximum pane height, with a minimum of 160 pixels.
sidestart | end

Default: start

Desktop sidebar placement.
classNamestring

Default: —

Extend the container with semantic theme utilities.

Build with these too