For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to any URL or sending an Accept: text/markdown header. An agent skill is available at /.well-known/agent-skills/site-skill.md.
1
Support

Frame

Group related content in separated or connected panels with a shared header and footer.

Playground

Live preview

Workspace

Manage your deployment environments.

Deployment

Production is up to date.

Preview

Changes are ready for review.

Last synced just now
Stacked joins adjacent direct panels. Dense removes outer spacing, not panel padding. Fit keeps panels at their content height. Reset restores all controls; there is no transient state.
Customize Frame
Customize
frame-demo.tsx
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:

Dependencies1 required packages
  • cn@^0.4.0
$ pnpm add cn@^0.4.0

Usage

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.

Updated just now
frame-demo.tsx
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

Showing the latest three events
frame-stacked-demo.tsx
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.

frame-ghost-demo.tsx
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

Component props, types, defaults, and descriptions
PropType / DescriptionDefault
childrenReactNode

Each part: native children. Keep panels as direct siblings for stacked layouts.

Not set
classNamestring

Each part: merged classes. Set [--frame-radius:var(--radius-lg)] on Frame to customize concentric corners.

Not set
denseboolean

Frame: removes outer padding and gaps, not panel content padding. Use stacked as well for shared panel seams.

false
descriptionReactNode (FrameDescription children)

Demo content for FrameDescription children, not a Frame prop. Empty text omits the description in this example.

Not set
fitboolean

FramePanel only: disables flex growth. Visible when the frame has spare height; the playground uses a minimum height.

false
refRef<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"
stackedboolean

Frame: removes gaps and joins adjacent direct FramePanel siblings with one border. Wrappers interrupt joining.

false
titleReactNode (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.