Frame
A panel of rows set into a tray, with the tray's top strip carrying the title.
Use it for a widget that is a list of rows under a title: deploys, settings, activity, service status. A panel of rows sits in a tray, and the tray's strip along the top carries the title, so the header needs no rule or background of its own.
band holds a bordered card in a thin muted band, for a widget that shows one figure or chart rather than a list. inset floats the panel in a recessed band and gives FrameFooter a place for actions. plain drops the tray, for a frame inside a card or another container that already draws an edge.
It has no state of its own and renders on the server. A row with href is a link and one with a @click listener is a button.
For a single surface with a heading and a body, use Card.
Installation
Copy the folder into components/ui, where you can edit it. It needs class-variance-authority and the cn helper, both already in a shadcn-vue project.
components/ui/frame/
├── Frame.vue
├── FrameHeader.vue … FrameFooter.vue
└── index.tsUsage
<script setup lang="ts">
import { Badge } from "@/components/ui/badge";
import {
Frame,
FrameAction,
FrameActions,
FrameContent,
FrameDescription,
FrameHeader,
FrameMedia,
FramePanel,
FrameRow,
FrameTitle,
} from "@/components/ui/frame";
</script>
<template>
<Frame>
<FrameHeader>
<FrameTitle>Deploys</FrameTitle>
<FrameAction>Last 24 hours</FrameAction>
</FrameHeader>
<FramePanel>
<FrameRow>
<FrameMedia><Icon name="lucide:rocket" /></FrameMedia>
<FrameContent>
<FrameTitle as="p">main · a41c2e9</FrameTitle>
<FrameDescription>Production · 2 minutes ago</FrameDescription>
</FrameContent>
<FrameActions><Badge>Ready</Badge></FrameActions>
</FrameRow>
</FramePanel>
</Frame>
</template>Composition
<Frame>
<FrameHeader>
<FrameTitle />
<FrameDescription />
<FrameAction />
</FrameHeader>
<FramePanel>
<FrameSection>
<FrameRow>
<FrameMedia />
<FrameContent>
<FrameTitle />
<FrameDescription />
</FrameContent>
<FrameActions />
</FrameRow>
</FrameSection>
</FramePanel>
<FrameFooter />
</Frame>- FrameHeader: the strip above the panel: a title, an optional description, and FrameAction on the trailing side.
- FrameTitle: a quiet caption in the header. Inside FrameContent it is the row's subject instead: full colour, and cut to one line. Use as="p" there.
- FrameDescription: a line under a title, in the header or in a row.
- FrameAction: the header's trailing side: a short status, a link, a small button.
- FramePanel: the card the rows sit on. It draws the hairline between its children, except under band, where it holds one figure and draws none.
- FrameRow: one line of the panel. href makes it a link, @click a button, and chevron marks one that leads somewhere.
- FrameMedia: a row's leading icon or avatar. It keeps its size.
- FrameContent: the middle of a row. It takes the space left and can shrink past its content, so a long line truncates instead of pushing the actions off the edge.
- FrameActions: a row's trailing side: a badge, a value, a switch. It keeps its size.
- FrameSection: a titled group of rows, for a panel holding more than one.
- FrameFooter: the strip below the panel. Under inset its children become equal-width pills.
Band
variant="band" holds a bordered card in a thin muted band whose top strip names what is inside. It suits a widget with one thing to show: here a KPI and its chart.
Inset
The panel floats in a recessed band, and the band below it holds the footer's actions as equal pills.
Rows that lead somewhere
Give a row href and it is a link with hover and focus states. chevron adds the trailing arrow.
Sections
FrameSection groups rows under a small heading, and divides its own rows the way the panel does.
Inside a card
plain drops the tray, so a frame inside a card has one edge rather than two. Hold it 8px off the card's edge and give the panel the card's radius less that gap.
API Reference
Frame
FrameTitle
FrameRow
FrameSection
Every part also takes the attributes of the element it renders, and a class.
Notes
The panel draws a hairline between each of its children, so rows never need to know whether they come first.
Under band and inset the panel's corners are the shell's less the band, so the two curves stay concentric.
The variant reaches the parts through a data attribute and CSS rather than provide/inject, which is what keeps the frame free of client state.