KQ-1536 · BEACH/SCRAWL
Beach Scrawl
A shoreline seen from above and drawn in characters — swell glyphs for the sea, sparse dots for dry sand, denser marks for wet — where an unseen hand writes a message in single-stroke capitals, hopping pen-up between strokes on the springs.flick constants, and waves run up on a decelerating ballistic curve to a reach that varies along the shore, spread a foam lace at the top of their run and drain away. Every water front that crosses a groove scours it and spreads it into its neighbours, so each wave leaves the writing softer until the tide has taken it and the hand writes again. Drag to press your own grooves into the sand; focus the beach and type, and the hand writes your letters at a caret (Backspace rakes the last one smooth, Enter smooths the line).
Waylight Inn · Coldbrook Bay
Low tide at 16:40. Rooms from 140.
writing in the sand · “LOW TIDE”
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/beach-scrawl
Props
- message
- string
- default "Low tide"
- What the unseen hand writes, in single-stroke capitals (A–Z, 0–9, . - ' & +). Empty for none.
- loop
- boolean
- default true
- Write the message again once the tide has taken it.
- waveRate
- number
- default 7
- How often the tide comes, in waves a minute (1 to 30).
- charSet
- "shore" | "dots" | "hatch"
- default "shore"
- The glyph vocabulary for sand, water and foam.
- wetness
- number
- default 0.5
- How far the swash runs up the beach and how long the sand stays dark, 0 to 1.
- erosion
- number
- default 0.5
- How much each wave takes from the writing, 0 (nothing) to 1 (about two waves).
- writeSpeed
- number
- default 2.5
- How fast the hand writes, in letters a second.
- cell
- number | "auto"
- default "auto"
- Glyph size in px (6 to 18), or "auto": 10 on a wide beach down to 7 on a phone, so a long word still has enough cells to read.
- brush
- number
- default 7
- Width of a pressed stroke in px; the hand's stick narrows for small letters.
- sandColor
- string
- default "color-mix(in oklab, var(--warn) 45%, var(--ink-3))"
- Sand ink, any CSS colour. Wet sand mixes it toward the sea; grooves toward the foreground.
- seaColor
- string
- default "var(--accent-bright)"
- Sea ink, any CSS colour.
- foamColor
- string
- default "color-mix(in oklab, var(--accent-bright) 30%, var(--foreground))"
- Foam ink, any CSS colour.
- interactive
- boolean
- default true
- Let the visitor write: drag to draw, type to write.
- trigger
- "mount" | "inView"
- default "inView"
- When the hand starts writing: at mount, or the first time the beach is on screen.
- paused
- boolean
- default false
- Hold the tide and the hand where they are.
- seed
- number
- default 1
- Seeds the sand, the waves and their reach.
- dpr
- number
- default 2
- Cap on the canvas's device pixel ratio, at most 2.
- onWritten
- (text: string) => void
- default —
- Fires when the hand finishes writing the message, with the text it wrote.
- onWashed
- () => void
- default —
- Fires when the tide has taken every mark from the sand.
- onWave
- (count: number) => void
- default —
- Fires as each wave starts up the beach, with the count so far.
- label
- string
- default "Beach"
- The beach's accessible name; what the sand reads is added to it.
- className
- string
- default "h-56 w-full"
- Sizes and shapes the box.
| Prop | Type | Default | Description |
|---|---|---|---|
| message | string | "Low tide" | What the unseen hand writes, in single-stroke capitals (A–Z, 0–9, . - ' & +). Empty for none. |
| loop | boolean | true | Write the message again once the tide has taken it. |
| waveRate | number | 7 | How often the tide comes, in waves a minute (1 to 30). |
| charSet | "shore" | "dots" | "hatch" | "shore" | The glyph vocabulary for sand, water and foam. |
| wetness | number | 0.5 | How far the swash runs up the beach and how long the sand stays dark, 0 to 1. |
| erosion | number | 0.5 | How much each wave takes from the writing, 0 (nothing) to 1 (about two waves). |
| writeSpeed | number | 2.5 | How fast the hand writes, in letters a second. |
| cell | number | "auto" | "auto" | Glyph size in px (6 to 18), or "auto": 10 on a wide beach down to 7 on a phone, so a long word still has enough cells to read. |
| brush | number | 7 | Width of a pressed stroke in px; the hand's stick narrows for small letters. |
| sandColor | string | "color-mix(in oklab, var(--warn) 45%, var(--ink-3))" | Sand ink, any CSS colour. Wet sand mixes it toward the sea; grooves toward the foreground. |
| seaColor | string | "var(--accent-bright)" | Sea ink, any CSS colour. |
| foamColor | string | "color-mix(in oklab, var(--accent-bright) 30%, var(--foreground))" | Foam ink, any CSS colour. |
| interactive | boolean | true | Let the visitor write: drag to draw, type to write. |
| trigger | "mount" | "inView" | "inView" | When the hand starts writing: at mount, or the first time the beach is on screen. |
| paused | boolean | false | Hold the tide and the hand where they are. |
| seed | number | 1 | Seeds the sand, the waves and their reach. |
| dpr | number | 2 | Cap on the canvas's device pixel ratio, at most 2. |
| onWritten | (text: string) => void | — | Fires when the hand finishes writing the message, with the text it wrote. |
| onWashed | () => void | — | Fires when the tide has taken every mark from the sand. |
| onWave | (count: number) => void | — | Fires as each wave starts up the beach, with the count so far. |
| label | string | "Beach" | The beach's accessible name; what the sand reads is added to it. |
| className | string | "h-56 w-full" | Sizes and shapes the box. |