Skip to content
kerfjsGitHub

Frame

A box that holds its shape, for images, video and previews.

SourcePackage @kerf-js/veneerTry it

Preview

tsx
import { Frame, Grid, Text } from '@kerf-js/veneer'export function Ratios() {  return (  <Grid    min="xs"    cols={6}    gap="related"  >    {RATIOS.map((ratio) => (      <Frame        key={ratio}        ratio={ratio}      >        <Text          role="caption"          subtle        >          {ratio}        </Text>      </Frame>    ))}  </Grid>  )}const min = 60_000const RATIOS = [  '21:9',  '16:9',  '3:2',  '4:3',  'square',  '3:4',] as const

Installation

npm install @kerf-js/veneer

Try it

Change the controls: the preview and its code update together. The address keeps what you set, so you can link to it.

Live preview
Controls
Codeupdates as you change the controls
import { Frame } from '@kerf-js/veneer'<Frame>  <img src="/preview.png" alt="" /></Frame>

Usage

tsx
import { Frame } from '@kerf-js/veneer'

Media crops to fit

An image, video, canvas or SVG directly inside fills the frame and crops.

tsx
import { Frame, Grid } from '@kerf-js/veneer'export function Media() {  return (  <Grid    min="xs"    cols={3}    gap="related"  >    <Frame>{art('1', '3')}</Frame>    <Frame ratio="square">{art('2', '5')}</Frame>    <Frame ratio="3:4">{art('4', '7')}</Frame>  </Grid>  )}const min = 60_000const art = (a: string, b: string) => (  <svg    viewBox="0 0 320 180"    preserveAspectRatio="xMidYMid slice"    aria-hidden="true"  >    <rect      width="320"      height="180"      fill={`var(--veneer-color-data-${a})`}    />    <circle      cx="96"      cy="150"      r="130"      fill={`var(--veneer-color-data-${b})`}    />    <rect      x="176"      y="36"      width="104"      height="108"      rx="8"      fill="var(--veneer-color-surface)"      opacity="0.85"    />  </svg>)

Deploy previews

Use ratio to give each preview the same shape, here 16:9.

tsx
import {  Card,  Frame,  Grid,  Stack,  Text} from '@kerf-js/veneer'export function Previews() {  return (  <Grid    min="sm"    cols={3}    gap="related"  >    {DEPLOYS.slice(0, 3).map((d, n) => (      <Card key={d.id}>        <Stack gap="related">          <Frame ratio="16:9">            {art(String(n + 1), String(n + 4))}          </Frame>          <Stack gap="hair">            <Text role="heading">{d.project}</Text>            <Text              role="caption"              subtle            >              {d.message}            </Text>          </Stack>        </Stack>      </Card>    ))}  </Grid>  )}const DEPLOYS = [  {    id: 'd-4821',    project: 'storefront',    branch: 'main',    commit: '9f3ac21',    message: 'Cache product images at the edge',    author: 'Priya Raman',    status: 'Live',    minutes: 2,    at: '2026-10-31T16:42',  },  {    id: 'd-4820',    project: 'search-api',    branch: 'ranking-v2',    commit: '1be07d4',    message: 'Rank exact matches first',    author: 'Lena Fischer',    status: 'Building',    minutes: 6,    at: '2026-10-31T16:10',  },  {    id: 'd-4819',    project: 'mobile-bff',    branch: 'main',    commit: 'c44e190',    message: 'Retry timed out upstream calls',    author: 'Sam Okafor',    status: 'Failed',    minutes: 4,    at: '2026-10-31T15:27',  },  {    id: 'd-4818',    project: 'checkout',    branch: 'main',    commit: '70d2fa8',    message: 'Add Apple Pay to the last step',    author: 'Tomás Oliveira',    status: 'Live',    minutes: 3,    at: '2026-10-31T14:03',  },  {    id: 'd-4817',    project: 'edge-cache',    branch: 'main',    commit: 'e81b6c2',    message: 'Lower the stale window to 30s',    author: 'Jun Tanaka',    status: 'Live',    minutes: 1,    at: '2026-10-31T11:55',  },  {    id: 'd-4816',    project: 'billing',    branch: 'main',    commit: '2ad9e03',    message: 'Prorate seats on plan change',    author: 'Mara Lindqvist',    status: 'Live',    minutes: 3,    at: '2026-10-29T17:20',  },]const min = 60_000const change = (a: number, b: number) => {  const sign = a >= b ? '+' : ''  const percent = (((a - b) / b) * 100).toFixed(1)  return `${sign}${percent}%`}const art = (a: string, b: string) => (  <svg    viewBox="0 0 320 180"    preserveAspectRatio="xMidYMid slice"    aria-hidden="true"  >    <rect      width="320"      height="180"      fill={`var(--veneer-color-data-${a})`}    />    <circle      cx="96"      cy="150"      r="130"      fill={`var(--veneer-color-data-${b})`}    />    <rect      x="176"      y="36"      width="104"      height="108"      rx="8"      fill="var(--veneer-color-surface)"      opacity="0.85"    />  </svg>)

When to use

Use it for

  • Images and video that must not shift the page as they load
  • Thumbnails of mixed sizes that should line up

Reach for something else

  • Card, when it needs a surface and padding

Accessibility

  • Frame draws nothing itself, so give an image its alt as usual.

API Reference

PropTypeDefaultDescription
childrenunknown—What it contains.
as"div" | "section" | "article" | "main" | "header" | "footer" | "nav" | "…—The element it renders as.
gap"section" | "none" | "hair" | "tight" | "related" | "group" | "page"—The space between children, from veneer's space scale.
ratio"square" | "4:3" | "3:2" | "16:9" | "21:9" | "3:4" | "9:16"'16:9'Width to height. 16:9 by default.

RelatedCardGrid