- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Dialog
- Drawer
- Dropdown
- Empty
- Error State
- Frame
- Input
- Input Amount
- Input Group
- Input OTP
- Input Password
- Input Phone
- Input Search
- Input Secret
- Loading
- PageContainer
- PageHeader
- Popover
- Select
- StatsCard
- Switch
- Tabs
- TextArea
- Toast
- Tooltip
Playground
Workspace
Manage your deployment environments.
Deployment
Production is up to date.
Preview
Changes are ready for review.
import { Frame, FrameHeader, FrameTitle, FrameDescription, FramePanel, FrameFooter } from "@/components/ui/frame";
export const FrameDemo = () => (
<Frame className="min-h-80 w-full max-w-md" variant="default" spacing="default" stacked={false} dense={false}>
<FrameHeader>
<FrameTitle>{"Workspace"}</FrameTitle>
<FrameDescription>{"Manage your deployment environments."}</FrameDescription>
</FrameHeader>
<FramePanel fit={false}>
<h4 className="text-sm font-medium">Deployment</h4>
<p className="text-sm text-muted-foreground">Production is up to date.</p>
</FramePanel>
<FramePanel fit={false}>
<h4 className="text-sm font-medium">Preview</h4>
<p className="text-sm text-muted-foreground">Changes are ready for review.</p>
</FramePanel>
<FrameFooter>Last synced just now</FrameFooter>
</Frame>
);Installation
Initialize shadcn in your React project, then install this component from the Vandor UI registry:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/frame.json
Dependencies
Installed automatically by the registry CLI. For manual setup:
- cn@^0.4.0
$ pnpm add cn@^0.4.0Usage
import {
Frame,
FrameHeader,
FrameTitle,
FrameDescription,
FramePanel,
FrameFooter,
} from "@/components/ui/frame";
<Frame>
<FrameHeader>
<FrameTitle>Workspace</FrameTitle>
<FrameDescription>Your deployment environments.</FrameDescription>
</FrameHeader>
<FramePanel>Production is up to date.</FramePanel>
<FramePanel>Preview is ready for review.</FramePanel>
<FrameFooter>Updated just now</FrameFooter>
</Frame>;Use Frame for related settings, status sections, or activity rows sharing one outer surface. It is layout only, not an accordion, app shell, or interactive card. Parts are optional; a Frame can contain a single panel or header/content/footer inside one panel.
Examples
Separated panels
Workspace
Your deployment environments.
Production
All systems operational.
Preview
Ready for review.
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/ui/frame";
export const FrameDemo = () => (
<Frame className="w-full max-w-sm">
<FrameHeader>
<FrameTitle>Workspace</FrameTitle>
<FrameDescription>Your deployment environments.</FrameDescription>
</FrameHeader>
<FramePanel>
<h4 className="text-sm font-medium">Production</h4>
<p className="text-sm text-muted-foreground">All systems operational.</p>
</FramePanel>
<FramePanel>
<h4 className="text-sm font-medium">Preview</h4>
<p className="text-sm text-muted-foreground">Ready for review.</p>
</FramePanel>
<FrameFooter>Updated just now</FrameFooter>
</Frame>
);The muted outer container groups card-colored panels. spacing="sm" | "default" | "lg" sets panel padding and header/footer horizontal padding to 12/16/24px, with an outer inset and gap of 2/4/8px. Header and footer vertical padding stays 12px. Each panel grows into spare height by default.
Stacked and dense
Recent activity
Connected panels with compact chrome.
Deployment completed
Health checks passed
Preview published
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/ui/frame";
export const FrameStackedDemo = () => (
<Frame stacked dense spacing="sm" className="w-full max-w-sm">
<FrameHeader>
<FrameTitle>Recent activity</FrameTitle>
<FrameDescription>Connected panels with compact chrome.</FrameDescription>
</FrameHeader>
<FramePanel>
<p className="text-sm">Deployment completed</p>
</FramePanel>
<FramePanel>
<p className="text-sm">Health checks passed</p>
</FramePanel>
<FramePanel>
<p className="text-sm">Preview published</p>
</FramePanel>
<FrameFooter>Showing the latest three events</FrameFooter>
</Frame>
);stacked joins adjacent direct FramePanel siblings with a shared border and square inner corners. Wrapping a panel or placing another element between panels interrupts joining. dense removes outer padding and gaps but preserves panel content padding. Use both for a flush connected list; dense alone does not merge panel seams. Neither option clips overflow or changes keyboard behavior.
Ghost, custom radius and content-sized panels
Release notes
No outer border, custom concentric corners.
This panel keeps its content height instead of filling the frame.
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from "@/components/ui/frame";
export const FrameGhostDemo = () => (
<Frame
variant="ghost"
className="min-h-64 w-full max-w-sm [--frame-radius:var(--radius-lg)]"
>
<FrameHeader>
<FrameTitle>Release notes</FrameTitle>
<FrameDescription>
No outer border, custom concentric corners.
</FrameDescription>
</FrameHeader>
<FramePanel fit>
<p className="text-sm">
This panel keeps its content height instead of filling the frame.
</p>
</FramePanel>
</Frame>
);variant="ghost" removes only the outer border. FramePanel fit disables that panel's flex growth; this is visible only when the frame has spare height. It does not force a fixed height or truncate content.
Set className="[--frame-radius:var(--radius-lg)]" on Frame to customize its outer radius. The default is var(--radius-xl). Inner corners derive from the outer radius minus the inset and border, clamped to zero; dense panels use the outer radius. --frame-panel-radius can override the derived radius when necessary. These are CSS variables, not React props. Use classes on individual parts to override their padding or surfaces.
Consumer setup and optional stories
Requires React 19, Tailwind CSS 4 with preflight, cn, and shadcn-compatible muted, foreground, card, card-foreground, border, muted-foreground, and radius tokens. No provider, Base UI runtime, animation stylesheet, or client directive is needed. Frame can be imported from a Next.js Server Component. The CLI uses your configured UI alias; review overwrite prompts before replacing an existing file. For manual setup, install cn and copy the single Source file into your UI directory. Website demos and playgrounds are not distributed.
Install Frame first, then optionally install its portable stories:
$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/frame-stories.json
Use your existing @storybook/react with matching Storybook package versions (types and composition checked with 10.6.1). Discover **/*.stories.tsx in your UI directory and import your global Tailwind/theme stylesheet in the preview. No addons or providers are required. Stories install separately without reinstalling Frame, adding Storybook packages, or changing .storybook/. This project has no Storybook server; Storybook UI is not verified here.
Behavior & Accessibility
Frame and FramePanel render div; FrameHeader renders header, FrameTitle h3, FrameDescription p, and FrameFooter footer. Each forwards its corresponding native attributes, handlers, and React 19 ref. FrameTitle's h3 must fit your page hierarchy; use another native heading inside FrameHeader when a different level is needed. Headers and footers inside sectioning contexts follow native landmark rules, so use an appropriate surrounding section rather than assuming automatic ARIA labeling.
There is no managed state, focus behavior, disabled state, live region, or motion. Add accessible labels and keyboard behavior to any controls you place inside panels. Titles and descriptions are not automatically associated with a region; supply application-owned IDs and ARIA attributes when needed. Panels wrap long text, but wide tables and code blocks should own horizontal scrolling. Focus rings and overflow are not clipped by Frame.
The playground controls configure the same markup as its runnable generated code. Reset restores all configuration; the demo has no transient interaction state. Copy includes all imports and safely serialized title/description content.
Compared with ReUI, FrameTitle is a native heading, and the supported spacing choices are sm, default, and lg. This is an independently implemented API, not a drop-in copy of every ReUI variant or CSS variable.
Props
| Prop | Type / Description | Default |
|---|---|---|
| children | ReactNodeEach part: native children. Keep panels as direct siblings for stacked layouts. | Not set |
| className | stringEach part: merged classes. Set [--frame-radius:var(--radius-lg)] on Frame to customize concentric corners. | Not set |
| dense | booleanFrame: removes outer padding and gaps, not panel content padding. Use stacked as well for shared panel seams. | false |
| description | ReactNode (FrameDescription children)Demo content for FrameDescription children, not a Frame prop. Empty text omits the description in this example. | Not set |
| fit | booleanFramePanel only: disables flex growth. Visible when the frame has spare height; the playground uses a minimum height. | false |
| ref | Ref<HTMLElement> (element-specific)Each part forwards its React 19 ref to its native element: div, header, h3, p or footer. | Not set |
| spacing | "sm" | "default" | "lg"Frame: panel padding and header/footer horizontal padding (12/16/24px); outer inset and gap (2/4/8px). | "default" |
| stacked | booleanFrame: removes gaps and joins adjacent direct FramePanel siblings with one border. Wrappers interrupt joining. | false |
| title | ReactNode (FrameTitle children)Demo content for FrameTitle children, not a Frame prop. FrameTitle renders an h3. | Not set |
| variant | "default" | "ghost"Frame: ghost removes only the outer border, preserving the muted surface and panel borders. | "default" |
Frame and FramePanel forward native div attributes, handlers and React 19 refs. FrameHeader uses header, FrameTitle h3, FrameDescription p and FrameFooter footer. Title and description playground controls configure children, not root props. No implicit ARIA relationships or interactive behavior are added.
Source
import { cn } from "cn";
import type { ComponentProps } from "react";
const spacingClasses = {
default: "[--frame-space:1rem] [--frame-inset:0.25rem]",
lg: "[--frame-space:1.5rem] [--frame-inset:0.5rem]",
sm: "[--frame-space:0.75rem] [--frame-inset:0.125rem]",
} as const;
export type FrameProps = ComponentProps<"div"> & {
variant?: "default" | "ghost";
spacing?: keyof typeof spacingClasses;
stacked?: boolean;
dense?: boolean;
};
/** Layout only. Adjacent direct panels share a seam when stacked. */
export const Frame = ({
className,
variant = "default",
spacing = "default",
stacked = false,
dense = false,
...props
}: FrameProps) => (
<div
data-slot="frame"
data-variant={variant}
data-spacing={spacing}
data-stacked={stacked}
data-dense={dense}
className={cn(
"flex min-w-0 flex-col rounded-(--frame-radius) bg-muted/50 text-foreground [--frame-radius:var(--radius-xl)] [--frame-panel-radius:max(0px,calc(var(--frame-radius)-var(--frame-inset)-1px))] gap-(--frame-inset) p-(--frame-inset)",
spacingClasses[spacing],
variant === "default" && "border border-border",
variant === "ghost" &&
"[--frame-panel-radius:max(0px,calc(var(--frame-radius)-var(--frame-inset)))]",
stacked &&
"gap-0 [&>[data-slot=frame-panel]:has(+[data-slot=frame-panel])]:rounded-b-none [&>[data-slot=frame-panel]+[data-slot=frame-panel]]:rounded-t-none [&>[data-slot=frame-panel]+[data-slot=frame-panel]]:border-t-0",
dense &&
"p-0 [--frame-inset:0rem] [--frame-panel-radius:var(--frame-radius)]",
dense &&
variant === "default" &&
"[&>[data-slot=frame-panel]]:border-x-0 [&>[data-slot=frame-panel]:first-child]:border-t-0 [&>[data-slot=frame-panel]:last-child]:border-b-0",
className
)}
{...props}
/>
);
export type FramePanelProps = ComponentProps<"div"> & { fit?: boolean };
export const FramePanel = ({
className,
fit = false,
...props
}: FramePanelProps) => (
<div
data-slot="frame-panel"
data-fit={fit}
className={cn(
"min-w-0 rounded-(--frame-panel-radius) border border-border bg-card p-(--frame-space) text-card-foreground [overflow-wrap:anywhere]",
!fit && "grow",
className
)}
{...props}
/>
);
export const FrameHeader = ({
className,
...props
}: ComponentProps<"header">) => (
<header
data-slot="frame-header"
className={cn(
"flex min-w-0 flex-col gap-1 px-(--frame-space) py-3 [overflow-wrap:anywhere]",
className
)}
{...props}
/>
);
export const FrameTitle = ({
children,
className,
...props
}: ComponentProps<"h3">) => (
<h3
data-slot="frame-title"
className={cn("text-sm font-semibold", className)}
{...props}
>
{children}
</h3>
);
export const FrameDescription = ({
className,
...props
}: ComponentProps<"p">) => (
<p
data-slot="frame-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
export const FrameFooter = ({
className,
...props
}: ComponentProps<"footer">) => (
<footer
data-slot="frame-footer"
className={cn(
"flex min-w-0 flex-wrap items-center gap-2 px-(--frame-space) py-3 text-sm text-muted-foreground [overflow-wrap:anywhere]",
className
)}
{...props}
/>
);
Credits
- ReUI Frame: API and composition inspiration. Independent Vandor implementation using native React elements and theme tokens, without copying ReUI source.