Offcanvas
Slide-out panel from left or right edge with backdrop, scroll lock, keyboard dismiss, and multiple sizes — ideal for forms, navigation, and detail views.
Preview
Basic right offcanvas
import { Offcanvas, OffcanvasTitle, OffcanvasDescription, OffcanvasBody } from '@/registry/ui-kit/tailwind-offcanvas/react'
import { useState } from 'react'
function Example() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Open panel</button>
<Offcanvas open={open} onClose={() => setOpen(false)}>
<div className="px-6 py-4">
<OffcanvasTitle>Panel title</OffcanvasTitle>
<OffcanvasDescription>Some description text.</OffcanvasDescription>
</div>
<OffcanvasBody className="px-6 py-4">
<p>Content goes here.</p>
</OffcanvasBody>
</Offcanvas>
</>
)
}Component API
| Prop | Default | Description |
|---|---|---|
open | false | Whether the offcanvas panel is visible. |
onClose | — | Callback fired when the panel should close. |
position | "right" | Which edge the panel slides in from. |
size | "md" | Max-width of the panel. |
OffcanvasTitle | — | Heading rendered inside the panel. |
OffcanvasDescription | — | Secondary text below the title. |
OffcanvasBody | — | Scrollable content area of the panel. |
Source Code
"use client";
import React, { useEffect } from "react";
const sizeMap: Record<string, string> = {
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
};
/* ── Offcanvas ──────────────────────────────────────── */
export function Offcanvas({
open,
onClose,
position = "right",
size = "md",
children,
className = "",
}: {
open: boolean;
onClose: () => void;
position?: "left" | "right";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
className?: string;
}) {
// Escape key
useEffect(() => {
function handleKey(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
if (open) {
document.addEventListener("keydown", handleKey);
return () => document.removeEventListener("keydown", handleKey);
}
}, [open, onClose]);
// Body scroll lock
useEffect(() => {
if (open) {
const original = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = original;
};
}
}, [open]);
if (!open) return null;
const sizeClass = sizeMap[size] || sizeMap.md;
const isRight = position === "right";
return (
<div className="fixed inset-0 z-50">
{/* Backdrop */}
<div
className="fixed inset-0 bg-zinc-950/25 backdrop-blur-sm dark:bg-zinc-950/50"
aria-hidden="true"
onClick={onClose}
/>
{/* Panel */}
<div
className={`fixed inset-y-0 ${isRight ? "right-0" : "left-0"} flex w-full ${sizeClass} flex-col ${isRight ? "border-l" : "border-r"} border-zinc-200 bg-white shadow-xl dark:border-zinc-700 dark:bg-zinc-900 ${className}`}
>
{/* Header with close button */}
<div className="flex items-center justify-between border-b border-zinc-100 px-6 py-4 dark:border-zinc-800">
<div className="flex-1" />
<button
type="button"
onClick={onClose}
className="rounded-lg p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Close"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{children}
</div>
</div>
);
}
/* ── OffcanvasTitle ─────────────────────────────────── */
export function OffcanvasTitle({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<h3 className={`text-base font-semibold text-zinc-900 dark:text-zinc-100 ${className}`}>
{children}
</h3>
);
}
/* ── OffcanvasDescription ───────────────────────────── */
export function OffcanvasDescription({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<p className={`text-sm text-zinc-500 dark:text-zinc-400 ${className}`}>
{children}
</p>
);
}
/* ── OffcanvasBody ──────────────────────────────────── */
export function OffcanvasBody({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div className={`flex-1 overflow-y-auto ${className}`}>
{children}
</div>
);
}