StickyPanel
LayoutA bounded document pane with a sticky desktop sidebar and stacked mobile layout.
Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.
"use client";
import { StickyPanel } from "@/components/ui/sticky-panel";
export default function StickyPanelExample() {
return (
<div className="w-full max-w-3xl space-y-4">
<div>
<h3 className="font-semibold">Keep the context in view</h3>
<p className="mt-1 text-sm text-muted-foreground">
The summary stays within this scroll pane on desktop. Mobile layouts
stack naturally.
</p>
</div>
<StickyPanel
label="Release document"
height={360}
sidebar={
<div className="rounded-lg border bg-card p-4">
<p className="text-xs font-medium text-muted-foreground">
LAUNCH CHECKLIST
</p>
<p className="mt-3 text-sm font-semibold">Apex Studio</p>
<ul className="mt-3 space-y-2 text-xs text-muted-foreground">
<li>6 sections</li>
<li>Ready for review</li>
<li>Owner: Alex Morgan</li>
</ul>
</div>
}
>
{[
"Start with purpose",
"Define the system",
"Make the details work",
"Build for everyone",
"Test the result",
"Share with your team",
].map((title, index) => (
<section
key={title}
className="min-h-48 space-y-3 rounded-lg border bg-card p-5"
>
<p className="text-xs font-medium text-primary">0{index + 1}</p>
<h4 className="font-semibold">{title}</h4>
<p className="text-sm leading-7 text-muted-foreground">
A useful release starts with a clear direction. Give each decision
the attention it deserves, and make room for your team to do
thoughtful work.
</p>
</section>
))}
</StickyPanel>
</div>
);
}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 { 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.
| Prop | Type / Default | Description |
|---|---|---|
| label | stringDefault: required | Accessible pane and sidebar context. |
| sidebar | ReactNodeDefault: required | Short sidebar content. |
| children | ReactNodeDefault: required | Document/body content. |
| height | numberDefault: 400 | Maximum pane height, with a minimum of 160 pixels. |
| side | start | endDefault: start | Desktop sidebar placement. |
| className | stringDefault: — | Extend the container with semantic theme utilities. |