Shape
A ready-made loading placeholder for a list, card, table or block of text.
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/veneerTry 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
Rows
Lines
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
| Prop | Type | Default | Description |
|---|---|---|---|
kind | ShapeKind | — | Which layout: text, list, card, media or table. |
lines | number | — | Text and card: lines of body text. |
rows | number | — | List and table: how many rows. |
cols | number | — | Table: how many columns. |
media | string | number | — | Card and media: the ratio, like "16/9". |
avatar | boolean | false | List: a circle at the start of each row. |
label | string | 'Loading' | What screen readers hear. |