Skip to content
kerfjsGitHub

Shape

A ready-made loading placeholder for a list, card, table or block of text.

SourcePackage @kerf-js/veneerTry it

Preview

tsx
import { Shape, Stack } from '@kerf-js/veneer'export function List() {  return (  <Stack>    <Shape      kind="list"      rows={3}    />  </Stack>  )}

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
Kind
Rows3
Codeupdates as you change the controls
import { Shape } from '@kerf-js/veneer'<Shape kind="list" />

Usage

tsx
import { Shape } from '@kerf-js/veneer'<Stack>  <Shape    kind="list"    rows={3}  /></Stack>

Text

Use kind="text" with lines to draw a placeholder paragraph.

tsx
import { Shape, Stack } from '@kerf-js/veneer'export function Paragraph() {  return (  <Stack>    <Shape      kind="text"      lines={4}    />  </Stack>  )}

Card

media is the picture's ratio. Leave it out for none.

tsx
import { Shape, Stack } from '@kerf-js/veneer'export function Tile() {  return (  <Stack>    <Shape      kind="card"      media="16/9"      lines={2}    />  </Stack>  )}

Table

Use kind="table" with rows and cols to draw a placeholder table.

tsx
import { Shape, Stack } from '@kerf-js/veneer'export function Table() {  return (  <Stack>    <Shape      kind="table"      rows={4}      cols={4}    />  </Stack>  )}

Media

Use kind="media" with media to draw a placeholder image at a set ratio.

tsx
import { Shape, Stack } from '@kerf-js/veneer'export function Media() {  return (  <Stack>    <Shape      kind="media"      media="21/9"    />  </Stack>  )}

Waiting on data

Give it the content's counts, so the swap moves nothing.

tsx
import { Boundary, resource } from '@kerf-js/core'import { Row, Shape, Stack, Text } from '@kerf-js/veneer'export function ShapeBoundary() {  return (  <Boundary    pending={      <Shape        kind="text"        lines={4}        label="Loading owners"      />    }  >    <Team />  </Boundary>  )}type Status =  | 'Live'  | 'Building'  | 'Failed'  | 'Paused'const PROJECTS: {  id: string  name: string  owner: string  region: string  status: Status  deploys: number  uptime: number  latency: number  cost: number  updated: string}[] = [  {    id: 'storefront',    name: 'storefront',    owner: 'Priya Raman',    region: 'eu-west',    status: 'Live',    deploys: 412,    uptime: 99.98,    latency: 84,    cost: 1240,    updated: '2026-10-31',  },  {    id: 'checkout',    name: 'checkout',    owner: 'Tomás Oliveira',    region: 'eu-west',    status: 'Live',    deploys: 288,    uptime: 99.95,    latency: 112,    cost: 980,    updated: '2026-10-31',  },  {    id: 'search-api',    name: 'search-api',    owner: 'Lena Fischer',    region: 'us-east',    status: 'Building',    deploys: 196,    uptime: 99.91,    latency: 146,    cost: 1610,    updated: '2026-10-30',  },  {    id: 'billing',    name: 'billing',    owner: 'Mara Lindqvist',    region: 'us-east',    status: 'Live',    deploys: 154,    uptime: 99.99,    latency: 73,    cost: 720,    updated: '2026-10-29',  },  {    id: 'mobile-bff',    name: 'mobile-bff',    owner: 'Sam Okafor',    region: 'ap-south',    status: 'Failed',    deploys: 233,    uptime: 99.42,    latency: 208,    cost: 860,    updated: '2026-10-29',  },  {    id: 'notifier',    name: 'notifier',    owner: 'Jun Tanaka',    region: 'eu-north',    status: 'Live',    deploys: 97,    uptime: 99.97,    latency: 58,    cost: 310,    updated: '2026-10-27',  },  {    id: 'reports',    name: 'reports',    owner: 'Noor Haddad',    region: 'us-west',    status: 'Paused',    deploys: 41,    uptime: 99.8,    latency: 321,    cost: 190,    updated: '2026-10-22',  },  {    id: 'edge-cache',    name: 'edge-cache',    owner: 'Jun Tanaka',    region: 'global',    status: 'Live',    deploys: 318,    uptime: 100,    latency: 21,    cost: 1430,    updated: '2026-10-31',  },  {    id: 'auth',    name: 'auth',    owner: 'Mara Lindqvist',    region: 'eu-west',    status: 'Live',    deploys: 176,    uptime: 99.99,    latency: 66,    cost: 540,    updated: '2026-10-28',  },  {    id: 'ingest',    name: 'ingest',    owner: 'Lena Fischer',    region: 'us-east',    status: 'Live',    deploys: 264,    uptime: 99.93,    latency: 131,    cost: 2120,    updated: '2026-10-30',  },]const later = <T,>(value: T) =>  new Promise<T>((done) => {    setTimeout(done, 1800, value)  })function Team() {  const list = resource(() => later(PROJECTS.slice(0, 4)))  return (    <Stack gap="related">      {(list.value ?? []).map((p) => (        <Row          key={p.id}          justify="between"        >          <Text>{p.owner}</Text>          <Text subtle>{p.name}</Text>        </Row>      ))}    </Stack>  )}

When to use

Use it for

  • As the pending of a Boundary
  • Any loading state that looks like a list, card or table

Reach for something else

  • Skeleton, when the layout is unusual and you need to build it by hand

Accessibility

  • It is a busy status named by label, and nothing inside it can be focused.

API Reference

PropTypeDefaultDescription
kindShapeKind—Which layout: text, list, card, media or table.
linesnumber—Text and card: lines of body text.
rowsnumber—List and table: how many rows.
colsnumber—Table: how many columns.
mediastring | number—Card and media: the ratio, like "16/9".
avatarbooleanfalseList: a circle at the start of each row.
labelstring'Loading'What screen readers hear.

RelatedSkeletonSpinner