Motif
BackgroundsA drawn picture for a slot that has no photograph yet: a sun over the sea, a split gate, a flower, a grid of tiles, a phone going live, a plate set for dinner, waves under a moon or three shapes.
The issues, episodes, lessons or videos of one person's work, as a list of rows in the order you give them (newest first). Each row has its number in the mono face, a picture, the title as the row's link, the date and the length. The picture is the piece's own: a waveform drawn from its title (look="wave", for audio), a drawing on a colour block (look="tile") or a frame with a play mark and the running time (look="frame", for video). A pointer on a row tints it, sets its waveform or its drawing moving and swells a frame's play mark, and with detail each row also carries its summary. A piece with audio gets a play button: one small script plays the file in the page, one piece at a time, and shows how much is played on its waveform. Without audio the script has nothing to bind and every row is a link.
Episodes
Episode 48
Episode 47
Episode 46
Episode 45
Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.
Variants · each row writes the attribute the prop writes; the section's own CSS answers
JavaScript of its own
893 B raw
510 B gzip.
Uses the shared runtime (1.9 kB raw, once per site). With those included: 2.6 kB raw.
CSS 19.5 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 105 · Firefox 121 · Safari 16; needs container-units, has. Elsewhere: without :has() the row's link shows no focus ring; without container units the titles keep the page's text size and every row stays in its narrow layout, the date under the words.
The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:
npx shadcn@latest add @moonarc-pro/piece-list---
import PieceList from '@/components/moonarc/pro/PieceList.astro';
---
<PieceList
look="wave"
eyebrow="Episodes"
detail
more={{ href: '/episodes/', label: 'All episodes' }}
pieces={[
{ number: 'Episode 18', title: 'Pricing a first course', date: '3 October 2026', length: '48 min', summary: 'What to charge before anyone has bought.', href: '/episodes/18/', audio: '/audio/episode-18.mp3' },
{ number: 'Episode 17', title: 'The newsletter that paid for itself', date: '19 September 2026', length: '41 min', summary: 'Six months of numbers, in the order they happened.', href: '/episodes/17/' },
]}
>
<span slot="headline">Every episode, <em>newest first</em></span>
</PieceList>Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: motif, waveform.
| Prop | Type | Default | Description |
|---|---|---|---|
pieces | { title, href, number?, date?, length?, summary?, id?, art?, block?, audio? }[] | none | The pieces, in the order shown. href is where it is read, heard or watched. number is set in the mono face before the title ("No. 42", "Episode 18"); length is "8 min read" or "48 min". id is an anchor a link elsewhere can point at, and the row is tinted when it is the target. art is the drawing for look="tile" and "frame", picked from the title when unset; block is the picture's colour (the first four in turn when unset). audio is a file to play in the page, for look="wave". |
look | 'wave' | 'tile' | 'frame' | 'tile' | The picture of each row: wave draws a waveform from the title, tile a Motif drawing on a colour block, frame a 16:10 video frame with a play mark and the running time. |
eyebrow | string | none | A short line above the heading. Slot "eyebrow" wins. |
headline | string | none | The heading. Slot "headline" wins and may hold an <em>, set in the emphasis face and colour. |
copy | string | none | A line under the heading. |
more | { href: string; label: string } | none | A link beside the heading, e.g. to the whole archive. |
detail | boolean | false | Show each piece's summary under its title. |
playLabel | string | 'Play {title}' | The play button's accessible name; {title} is the piece's title. |
level | 1 | 2 | 3 | 2 | Heading level of the section's heading; the pieces' titles are one below. |
tone | 'surface' | 'muted' | 'ink' | 'brand' | unset (paints nothing) | The section's ground; the picture tiles keep their blocks on every tone. |
density | 'compact' | 'cozy' | 'roomy' | 'cozy' | Block padding ×0.8 / ×1 / ×1.25 through --ma-density. |
The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.
.ma-pieces.ma-pieces__body.ma-pieces__copy.ma-pieces__eyebrow.ma-pieces__head.ma-pieces__headline.ma-pieces__icon-pause.ma-pieces__icon-play.ma-pieces__link.ma-pieces__list.ma-pieces__mark.ma-pieces__media.ma-pieces__media--frame.ma-pieces__media--tile.ma-pieces__media--wave.ma-pieces__meta.ma-pieces__more.ma-pieces__number.ma-pieces__play.ma-pieces__row.ma-pieces__summary.ma-pieces__time.ma-pieces__title.ma-pieces__titles.ma-wave
slot="eyebrow"slot="headline"
The waveforms and drawings stay at rest on hover and while playing. A row changes its background and lifts, and a frame's play mark swells, at once, without a transition. Playing and the played share still work.
ClientRouterThe script binds the play buttons once per swap through the runtime's onMount; the audio stops when the page is swapped away. The rest is CSS.
Replaces: Podcast site episode lists (Transistor, Buzzsprout) · Newsletter archive pages (Substack, Ghost themes). See the migration table.
A drawn picture for a slot that has no photograph yet: a sun over the sea, a split gate, a flower, a grid of tiles, a phone going live, a plate set for dinner, waves under a moon or three shapes.
Press play, then navigate: position, volume and playback carry over.
An audio player that keeps playing while you navigate: the same <audio> element is carried into the next page by Astro's router, so position, volume and playback never restart.
A row of bars drawn from a word, like the shape of an episode's sound.
Writing as a list: the day, the title at display size and the reading time on one line per post, the whole line a link, and the post's cover floating by the pointer while the line is hovered.
The first screen of one person's work, as a bento.
Sample projects
The work as a numbered index: one line per project with the title at display size and the client, the discipline and the year in mono, the whole line a link.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown