# Frame

> Place related content inside a bounded media or interface frame.

Source: https://www.honestui.com/docs/components/frame

```tsx
import {
  Frame,
  FrameDescription,
  FrameHeader,
  FramePanel,
  FrameTitle,
} from "@/components/honest-ui/ui/frame"

export function FrameDemo() {
  return (
    <Frame className="w-full max-w-lg">
      <FrameHeader>
        <FrameTitle>Section header</FrameTitle>
        <FrameDescription>
          Brief description about the section
        </FrameDescription>
      </FrameHeader>
      <FramePanel>
        <h2 className="text-sm font-semibold">Section title</h2>
        <p className="text-sm text-muted-foreground">
          Section description
        </p>
      </FramePanel>
    </Frame>
  )
}

```

## Overview [#overview]

Use Frame when you need a simple bordered surface around related content: previews, examples, media, code-adjacent output, and small grouped regions that do not need full card structure.

Frame is lighter than Card. It draws one boundary around its children and gets out of the way; it has no selection model, no loading state, and no actions of its own. If the content needs header actions or footer buttons, Card is usually the better fit — see [Frame versus Card](#frame-versus-card).

## Anatomy [#anatomy]

A Frame is a rounded muted tray (`rounded-2xl` on a `bg-muted` background) that stacks its parts vertically. Inside it:

* **FrameHeader** renders a real `<header>` element and holds the title row.
* **FrameTitle** is a semibold label naming what the frame contains.
* **FrameDescription** is a muted line under the title.
* **FramePanel** is the inner card surface where the actual content sits.
* **FrameFooter** renders a real `<footer>` element for timestamps, sources, or captions.

FramePanel is table-aware: when it contains a Table, it removes its own padding, border, background, and shadow so the table's edges align with the panel. See [Framed table](#framed-table).

## Frame versus Card [#frame-versus-card]

Reach for Frame when the boundary itself is the point: showing output next to code, isolating a device mockup, boxing an embedded tool. Reach for Card when people need to act on the content — Card gives you a title, description, content area, action slots, and interaction conventions that Frame deliberately lacks. A frame around something with three action buttons usually means you wanted a Card.

Because Frame is purely presentational, it has no states to manage: no loading, disabled, destructive, or error variants exist, and none should be faked with styling. Any state belongs to the components you place inside it.

## Accessibility [#accessibility]

Frame adds no meaning by itself. It renders `<div>`s (plus semantic `<header>` and `<footer>` elements), so screen readers announce nothing about the grouping unless you provide structure. Put a real heading element inside FrameHeader when the framed content needs one — see [Don't do this](#dont-do-this) — and use `figure`/`figcaption` or a labelled `role="region"` when assistive technology users need to navigate to it.

Nothing in a Frame receives focus by default; keyboard behavior comes entirely from the interactive elements you place inside. Colors come from theme tokens (`bg-muted`, `bg-card`, border tokens), so frames adapt to dark mode automatically, and padding uses logical properties so layouts mirror in right-to-left locales. Long content wraps normally; the frame grows rather than clipping.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;frame&#x22;]" />
  

  
    
      
        Copy and paste the following code into your project.
      

      ### components/ui/frame.tsx

```tsx
import * as React from "react"

import { cn } from "@/lib/utils"

function Frame({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="frame"
      className={cn(
        "relative flex flex-col rounded-2xl bg-muted p-1",
        className
      )}
      {...props}
    />
  )
}

function FramePanel({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="frame-panel"
      className={cn(
        "relative bg-clip-padding not-has-[table]:rounded-xl not-has-[table]:border not-has-[table]:bg-card not-has-[table]:p-5 not-has-[table]:shadow-xs before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] has-[table]:before:hidden dark:bg-clip-border dark:before:shadow-[0_-1px_--theme(--color-white/8%)]",
        className
      )}
      {...props}
    />
  )
}

function FrameHeader({ className, ...props }: React.ComponentProps<"header">) {
  return (
    <header
      data-slot="frame-panel-header"
      className={cn("flex flex-col px-5 py-4", className)}
      {...props}
    />
  )
}

function FrameTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="frame-panel-title"
      className={cn("text-sm font-semibold", className)}
      {...props}
    />
  )
}

function FrameDescription({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="frame-panel-description"
      className={cn("text-sm text-muted-foreground", className)}
      {...props}
    />
  )
}

function FrameFooter({ className, ...props }: React.ComponentProps<"footer">) {
  return (
    <footer
      data-slot="frame-panel-footer"
      className={cn("flex flex-col gap-1 px-5 py-4", className)}
      {...props}
    />
  )
}

export {
  Frame,
  FramePanel,
  FrameHeader,
  FrameTitle,
  FrameDescription,
  FrameFooter,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import {
  Frame,
  FrameDescription,
  FrameFooter,
  FrameHeader,
  FramePanel,
  FrameTitle,
} from "@/components/ui/frame";
```

```tsx
<Frame>
  <FrameHeader>
    <FrameTitle>Preview</FrameTitle>
    <FrameDescription>Desktop layout at 1440 pixels</FrameDescription>
  </FrameHeader>
  <FramePanel>Preview content</FramePanel>
  <FrameFooter>Last updated 2 minutes ago</FrameFooter>
</Frame>
```

## Don't do this [#dont-do-this]

### Making the whole frame clickable [#making-the-whole-frame-clickable]

```tsx
// Bad
<div onClick={() => router.push("/reports/traffic")}>
  <Frame>…report contents…</Frame>
</div>
```

```tsx
// Good
<Frame>
  <FrameHeader>
    <FrameTitle>Traffic report</FrameTitle>
  </FrameHeader>
  <FramePanel>…report contents…</FramePanel>
  <FrameFooter>
    <Button render={<Link href="/reports/traffic" />}>Open report</Button>
  </FrameFooter>
</Frame>
```

A click handler on a wrapper div produces a huge cursor-target that no keyboard user can reach, no assistive technology will announce, and no browser treats as a link — middle-click, <kbd>Cmd</kbd>-click, and copy-link all break. Decide which single element is the action, make it a real link or button, and let the rest of the frame stay inert.

### Treating FrameTitle as a heading [#treating-frametitle-as-a-heading]

```tsx
// Bad
<FrameHeader>
  <FrameTitle>Billing summary</FrameTitle>
</FrameHeader>
```

```tsx
// Good
<FrameHeader>
  <h3 className="text-sm font-semibold">Billing summary</h3>
</FrameHeader>
```

FrameTitle renders a styled `<div>`, so screen-reader users navigating by headings will never find the section even though it looks like one visually. When the framed region belongs in the page outline, render a real `h2`–`h6` with the same styles instead.

### Framing by decoration instead of meaning [#framing-by-decoration-instead-of-meaning]

```tsx
// Bad
<Frame><p>Welcome to the dashboard.</p></Frame>
<Frame><p>Your storage is 62% full.</p></Frame>
```

```tsx
// Good
<p>Welcome to the dashboard.</p>
<Card title="Storage">Your storage is 62% full.</Card>
```

Frames read as bounded objects: previews, outputs, embeddable regions. Wrapping ordinary prose in them teaches people that boxes mean artifacts, and soon every box competes with every other box. Keep running text unframed and reserve Frame for content that genuinely sits outside the document flow.

## Examples [#examples]

### Device preview [#device-preview]

A narrow frame isolates a mobile mockup from the surrounding page.

```tsx
import { Frame, FrameHeader, FramePanel, FrameTitle } from "@/components/honest-ui/ui/frame"

export function FrameDevicePreview() {
  return (
    <Frame className="w-64">
      <FrameHeader><FrameTitle>Mobile preview</FrameTitle></FrameHeader>
      <FramePanel>
        <div className="mx-auto h-40 w-24 rounded-2xl border bg-muted p-2">
          <div className="h-full rounded-xl bg-background" />
        </div>
      </FramePanel>
    </Frame>
  )
}

```

### Code output [#code-output]

Build logs and command output read as artifacts when they sit in a frame with a timestamped footer.

```tsx
import { Frame, FrameFooter, FrameHeader, FramePanel, FrameTitle } from "@/components/honest-ui/ui/frame"

export function FrameCodeOutput() {
  return (
    <Frame className="w-full max-w-sm">
      <FrameHeader><FrameTitle>Build output</FrameTitle></FrameHeader>
      <FramePanel className="font-mono text-xs text-muted-foreground">Compiled in 824ms</FramePanel>
      <FrameFooter className="text-xs text-muted-foreground">Last run just now</FrameFooter>
    </Frame>
  )
}

```

### Analytics panel [#analytics-panel]

Metrics keep their own boundary without inheriting card actions.

```tsx
import { Frame, FrameHeader, FramePanel, FrameTitle } from "@/components/honest-ui/ui/frame"

export function FrameAnalyticsPanel() {
  return (
    <Frame className="w-full max-w-sm">
      <FrameHeader><FrameTitle>Traffic overview</FrameTitle></FrameHeader>
      <FramePanel>
        <div className="grid grid-cols-3 gap-2 text-center text-sm">
          <div><div className="font-semibold">42k</div><div className="text-muted-foreground">Visits</div></div>
          <div><div className="font-semibold">8.2%</div><div className="text-muted-foreground">CVR</div></div>
          <div><div className="font-semibold">312</div><div className="text-muted-foreground">Trials</div></div>
        </div>
      </FramePanel>
    </Frame>
  )
}

```

### Framed table [#framed-table]

When FramePanel contains only a Table, it drops its own chrome so the table's borders do the work.

```tsx
import { Badge } from "@/components/honest-ui/ui/badge"
import { Frame, FramePanel } from "@/components/honest-ui/ui/frame"
import {
  Table,
  TableBody,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/honest-ui/ui/table"

export function TableFramed() {
  return (
    <Frame className="w-full min-w-0 max-w-4xl">
      <FramePanel>
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>Project</TableHead>
              <TableHead>Status</TableHead>
              <TableHead>Team</TableHead>
              <TableHead className="text-right">Budget</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            <TableRow>
              <TableCell className="font-medium">
                Website Redesign
              </TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-emerald-500"
                    aria-hidden="true"
                  />
                  Paid
                </Badge>
              </TableCell>
              <TableCell>Frontend Team</TableCell>
              <TableCell className="text-right">$12,500</TableCell>
            </TableRow>
            <TableRow>
              <TableCell className="font-medium">Mobile App</TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-muted-foreground/64"
                    aria-hidden="true"
                  />
                  Unpaid
                </Badge>
              </TableCell>
              <TableCell>Mobile Team</TableCell>
              <TableCell className="text-right">$8,750</TableCell>
            </TableRow>
            <TableRow>
              <TableCell className="font-medium">
                API Integration
              </TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-amber-500"
                    aria-hidden="true"
                  />
                  Pending
                </Badge>
              </TableCell>
              <TableCell>Backend Team</TableCell>
              <TableCell className="text-right">$5,200</TableCell>
            </TableRow>
            <TableRow>
              <TableCell className="font-medium">
                Database Migration
              </TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-emerald-500"
                    aria-hidden="true"
                  />
                  Paid
                </Badge>
              </TableCell>
              <TableCell>DevOps Team</TableCell>
              <TableCell className="text-right">$3,800</TableCell>
            </TableRow>
            <TableRow>
              <TableCell className="font-medium">
                User Dashboard
              </TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-emerald-500"
                    aria-hidden="true"
                  />
                  Paid
                </Badge>
              </TableCell>
              <TableCell>UX Team</TableCell>
              <TableCell className="text-right">$7,200</TableCell>
            </TableRow>
            <TableRow>
              <TableCell className="font-medium">
                Security Audit
              </TableCell>
              <TableCell>
                <Badge variant="outline">
                  <span
                    className="size-1.5 rounded-full bg-red-500"
                    aria-hidden="true"
                  />
                  Failed
                </Badge>
              </TableCell>
              <TableCell>Security Team</TableCell>
              <TableCell className="text-right">$2,100</TableCell>
            </TableRow>
          </TableBody>
          <TableFooter>
            <TableRow>
              <TableCell colSpan={3}>Total Budget</TableCell>
              <TableCell className="text-right font-medium">$39,550</TableCell>
            </TableRow>
          </TableFooter>
        </Table>
      </FramePanel>
    </Frame>
  )
}

```

## API reference [#api-reference]

All parts accept the native props of their underlying element plus `className`. No part adds custom props.

| Part               | Renders    | Purpose                                                           |
| ------------------ | ---------- | ----------------------------------------------------------------- |
| `Frame`            | `<div>`    | Root tray; rounded muted surface, vertical flex stack             |
| `FrameHeader`      | `<header>` | Title row above the panel                                         |
| `FrameTitle`       | `<div>`    | Semibold label for the frame's contents                           |
| `FrameDescription` | `<div>`    | Muted supporting line under the title                             |
| `FramePanel`       | `<div>`    | Inner card surface; adapts automatically when it contains a table |
| `FrameFooter`      | `<footer>` | Caption, timestamp, or source line below the panel                |

Frame introduces no Base UI primitive, no state, and no data attributes. Choose the semantic elements inside each slot based on the content.
