Introduction#
The tabbied package ships three entry points:
tabbied/react— theTabbiedPatterncomponent. It renders an pattern into a normal, CSS-sizeable box, like an<img>.tabbied/patterns— 100+ preset designs as tree-shakeablePatternDefinitionexports.tabbied— the framework-agnostic core (createPattern) plus all shared types and sizing helpers.
Patterns are deterministic: the same pattern, seed, grid and options always draw the same design, at any size. That makes patterns safe to use as reproducible brand assets — a seed is a design you can keep.
Installation#
React is an optional peer dependency — you only need it for the tabbied/react entry point. The only hard dependency is css-doodle, which is installed automatically.
npm install tabbiedQuick start#
Import the component and a preset, then render it inside a sized box. On the server and the first client paint it shows the pattern's background color (correct size, zero layout shift); the live pattern takes over once it mounts.
import { TabbiedPattern } from 'tabbied/react';
import { radius } from 'tabbied/patterns';
export function Banner() {
// The box fills its parent by default; height={320} pins one axis.
return <TabbiedPattern pattern={radius} seed="k9Pz" height={320} />;
}Importing presets#
pattern takes an PatternDefinition object. Each preset is a side-effect-free named export, so importing only the designs you render keeps the rest of the catalog out of your bundle.
// Import only what you render — bundlers ship just those presets.
import { radius, windowpane } from 'tabbied/patterns';
// Building a gallery? The full record pulls in every design.
import { patterns } from 'tabbied/patterns';Browse every design (and its options) in the gallery — the preset export name is the slug in the editor URL.
Sizing & fit modes#
Sizing splits in two: the box props say how big the element is, and fit says how the drawing meets that box. A pattern has no intrinsic size, so by default the box simply fills its containing block — drop one into a sized parent and you are done.
<TabbiedPattern pattern={radius} />No sizing props — it fills the 120px-tall box it was dropped into.maxWidth={320} aspectRatio={3 / 2}Fills the width up to 320px; the ratio sets the height, with no sized parent involved.// Default: fill the containing block. .panel is 100% wide, 400px tall.
<div className="panel">
<TabbiedPattern pattern={radius} />
</div>
// Fill the width, cap it, and let the ratio set the height.
<TabbiedPattern pattern={radius} maxWidth={960} aspectRatio={3 / 2} />
// Pin one axis; numbers are px, strings are CSS.
<TabbiedPattern pattern={radius} height={320} />
<TabbiedPattern pattern={radius} height="40vh" maxHeight={520} />
// Hand sizing back to a class name.
<TabbiedPattern pattern={radius} fill={false} className="hero-art" />The box props are the CSS properties they are named after, resolved onto the wrapper element (on the server render too, so there is no layout shift on mount). Numbers are px; strings are used as written.
fill(defaulttrue) —width: 100%; height: 100%. An explicitwidth/heighttakes over that axis;fill={false}leaves the box to a class name or the surrounding layout.maxWidth/maxHeight— upper bounds on the box.aspectRatio— derives the height from the width, so it pairs withmaxWidthin a parent that has no fixed height.
One caveat comes with the territory: height: 100% only resolves against a parent with a definite height. In a parent that sizes to its content, reach for height or aspectRatio instead of fill.
Whatever the box turns out to be, the pattern is fitted into it without distortion — nothing is ever scaled by a different factor horizontally than vertically. fit picks which non-distorting strategy is used:
grid(default) — re-derives the cell grid from the measured container, so any box is tiled edge-to-edge with whole, near-square cells. Tune the density withcellSize(px) ordensity(0–4).cover— draws a fixed-resolution render and scales it uniformly into the box, preserving the authored proportions of fixed-px strokes and shadows. The render follows the box's aspect ratio and re-derives its grid, so the pattern is never cut off mid-cell.fixed— renders at an explicit canvas size,width/heightin px (default 360 × 540). This is what the Tabbied editor uses.
Every design supports all three, so fit is a plain choice — omit it and you get grid.
Each column below is one mode, drawing the same pattern at the same seed into a landscape box and a portrait one. The differences only show up when the box stops matching the drawing — which is most of the time.
fit="grid"The cell grid is re-derived per box, so cells stay square in both.fit="cover"One render, scaled uniformly to fill. Fixed-px strokes keep their proportions.fit="fixed"A canvas of its own size — here 150 × 225, which each box crops.// grid (default): the cell grid adapts to the container size
<TabbiedPattern pattern={radius} fit="grid" />
// cover: a fixed-resolution render scaled uniformly to fill the box.
// Every design is cell-tiled, so the render adapts to the box's shape
// and tiles it with whole cells — nothing is cropped mid-cell
<TabbiedPattern pattern={radius} fit="cover" />
// fixed: an explicit canvas size in px (what the Tabbied editor uses)
<TabbiedPattern pattern={radius} fit="fixed" width={360} height={540} />Colors & palettes#
Pass palette to recolor a design — the background color (color0) comes first, followed by the inks. Passing fewer colors than the pattern was authored with is fine: the unused slots cycle back through your inks, so a two-color palette redraws the whole design in your two colors.
<TabbiedPattern
pattern={radius}
seed="k9Pz"
// color0 (the background) comes first
palette={['#0b132b', '#5bc0be', '#6fffe9', '#ff6b6b']}
fit="cover"
height={280}
/>// Any CSS color works for a slot — including 'transparent',
// which drops the background entirely.
<TabbiedPattern
pattern={radius}
palette={['transparent', '#232529', '#ff3d8b']}
/>Trying a custom palette across every design? The gallery lets you save named palettes (exportable as JSON) and preview all presets with them — including with a transparent background.
Options#
Every preset exposes adjustable options — the same controls the Tabbied editor shows. Pass them keyed by option id; anything you omit uses the authored default. Option ids and their allowed values live on the definition itself (pattern.options), so you can build your own controls against them.
// Option ids come from the preset (the same controls the editor shows).
// Radius takes a grid size and a shape frequency.
<TabbiedPattern
pattern={radius}
seed="k9Pz"
options={{ grid: '4x6', frequency: 0.6 }}
fit="cover"
height={280}
/>Under fit="grid" (and adaptive cover) the grid option is derived from the container, so a pinned grid value acts as a density hint rather than an exact count.
Seeds, redraw & export#
The seed prop pins the pattern: omit it for a random variation per mount, or set it to freeze a design you like. Grab a ref to the component's handle to drive it imperatively — redraw() re-randomizes (or sets) the seed, morphing designs with CSS transitions between variations, and exportImage() saves a PNG. Try it:
import { useRef } from 'react';
import { TabbiedPattern, type TabbiedPatternHandle } from 'tabbied/react';
import { radius } from 'tabbied/patterns';
export function Reseedable() {
const ref = useRef<TabbiedPatternHandle>(null);
return (
<>
<TabbiedPattern ref={ref} pattern={radius} fit="cover" />
<button onClick={() => ref.current?.redraw()}>Redraw</button>
<button onClick={() => ref.current?.exportImage()}>Export PNG</button>
</>
);
}exportImage() accepts { scale, name, download, detail } and resolves when css-doodle has produced the file — bump scale for print-resolution exports.
exportSvg() converts the rendered pattern to a native vector SVG (real shapes and gradients, no foreignObject) that opens in design tools and scales to any resolution; pass { download: true } to save a .svg. A few designs use smooth conic-gradient sweeps SVG can't express — they set svgExport: false in their definition, which supportsSvgExport(pattern) checks.
Ambient animation#
Set redrawInterval to reseed on a timer — the gallery's shimmer. Ticks are dropped while the tab is hidden or the element is scrolled out of the viewport, so a long page of animated patterns only pays for what's on screen. Use the paused prop for your own gating on top (it preserves the timer phase), and see Accessibility for what prefers-reduced-motion switches off.
// Reseed on a timer (the gallery's shimmer). Ticks are skipped while
// the tab is hidden or the element is outside the viewport. Under
// prefers-reduced-motion the timer never starts and the designs' own
// cell transitions are muted, so nothing here moves.
<TabbiedPattern
pattern={quilt}
fit="cover"
redrawInterval={2000}
height={280}
/>Accessibility#
By default the pattern is decorative: the box is aria-hidden and invisible to assistive tech. Set decorative={false} to expose it as an image with role="img" and an accessible name (ariaLabel, falling back to the pattern's display name).
// Decorative (default): hidden from assistive tech.
<TabbiedPattern pattern={radius} />
// Meaningful image: exposed with role="img" and a label.
<TabbiedPattern
pattern={radius}
decorative={false}
ariaLabel="Generative pattern of quarter circles"
/>Reduced motion
A pattern has two sources of movement, and prefers-reduced-motion: reduce suppresses both — with no configuration and no props to pass:
- the
redrawIntervaltimer never starts, and - the designs' own cell transitions are muted, so anything that re-renders cuts to the new arrangement instead of morphing into it.
The second half does more work than it sounds like. Every design carries a ~400ms transition, and a re-render is not always something the reader asked for: grid and cover re-derive their cell grid when the box changes, so turning a phone or dragging a window would otherwise animate every cell on the page. That is the passive motion the preference exists for. A redraw() you call yourself is muted on the same terms.
Nothing is lost either way — the pattern renders identically, it just stops easing between states. The preference is observed, not read once, so toggling it while the page is open takes effect immediately.
Server rendering#
TabbiedPattern is a client component (it registers a browser custom element on import) with a built-in server placeholder: on the server and the first client paint it renders the wrapper box filled with the pattern's background color — correct dimensions, zero layout shift, no hydration mismatch. In the Next.js App Router you can use it directly from Server Components; no ssr: false ceremony needed.
// App Router: works directly in a Server Component tree — the
// component itself is the client boundary.
import { TabbiedPattern } from 'tabbied/react';
import { radius } from 'tabbied/patterns';
export default function Page() {
return <TabbiedPattern pattern={radius} height={320} />;
}Vanilla JavaScript#
The React component is a thin wrapper over the framework-free engine. createPattern(host, config) mounts a pattern into any element and returns a controller with update(), redraw(), exportImage(), exportSvg() and destroy(). It accepts the same config the component takes as props, minus the box props — the host element is yours to size, or run them through resolveBoxStyle(). That includes redrawInterval and paused: the timer and its reduced-motion, tab-visibility and viewport gates live in the controller, so patterns animate here without reimplementing any of it.
import { createPattern } from 'tabbied';
import { radius } from 'tabbied/patterns';
const controller = createPattern(document.querySelector('#stage'), {
pattern: radius,
seed: 'k9Pz',
redrawInterval: 5200, // optional: reseed on a timer, gates included
// Measured fits (grid/cover) mount asynchronously, once the
// host's size is known — drive the controller from onReady.
onReady: async () => {
controller.redraw(); // re-randomize the seed
await controller.exportImage();
},
});
// later, when the pattern is removed:
controller.destroy();API reference#
<TabbiedPattern /> props
| Prop | Type | Default | Description |
|---|---|---|---|
| patternrequired | PatternDefinition | — | The pattern to render — a preset from tabbied/patterns or your own definition. |
| seed | string | random | Randomization seed. Omit for a random seed per mount; reseed via the handle. |
| palette | string[] | preset palette | Active colors, background (color0) first. Shorter palettes cycle their inks into the unused slots. |
| options | Record<string, OptionValue> | authored | Option values keyed by option id; unset options use authored defaults. |
| fit | 'grid' | 'cover' | 'fixed' | 'grid' | How the drawing meets its box — never by distorting it (see Sizing & fit modes). |
| fill | boolean | true | Fill the containing block (width: 100%; height: 100%). |
| maxWidth / maxHeight | number | string | — | Upper bounds on the box. Numbers are px. |
| aspectRatio | number | string | — | CSS aspect-ratio — derives the height from the width. |
| cellSize | number | 36 | fit="grid" — target cell size in px. |
| density | 0 | 1 | 2 | 3 | 4 | 4 | fit="grid" — authored density level, an alternative to cellSize. |
| width / height | number | string | fill | Box size; numbers are px. Under fit="fixed" the numeric form is also the canvas size (default 360 × 540). |
| coverRender | { width, height } | 800 × 800 | cover render resolution override. |
| redrawInterval | number | off | Re-randomize the seed every N ms (uncontrolled seed only). Paused off-screen, in hidden tabs, and under reduced motion. |
| paused | boolean | false | Pause redrawInterval ticks without resetting the timer. |
| decorative | boolean | true | true renders an aria-hidden image; false exposes role="img" with ariaLabel. |
| onReady | () => void | — | Called once the first pattern render is committed. |
| className / style | string / CSSProperties | — | Applied to the wrapper box. |
Handle (ref)
| Member | Description |
|---|---|
| redraw(seed?: string) | Re-randomize (or set) the seed, animating designs with CSS transitions. |
| exportImage(options?) | PNG export via css-doodle. Returns a promise; rejects before the pattern has mounted. |
| exportSvg(options?) | Native vector SVG export (no foreignObject). Resolves with { svg, width, height, warnings }; { download: true } saves a file. Unavailable for definitions with svgExport: false. |
| element | The raw <css-doodle> element, for power users. |
PatternDefinition
Presets are plain data. You can author your own — the renderer only cares about the shape:
import type { PatternDefinition } from 'tabbied';
const myPattern: PatternDefinition = {
name: 'My design',
slug: 'my-design',
palette: ['#101418', '#3e8bff', '#3fffb2'],
options: [
{
id: 'grid',
displayName: 'Columns and rows',
type: 'ButtonSelectGroup',
default: '6x9',
options: ['2x3', '4x6', '6x9'],
replace: '${grid}',
},
],
code: {
style: '--rule: ( background: var(--color1); );',
doodle:
':doodle { @grid: ${grid}; @size: ${width} ${height}; } ' +
':container { background: var(--color0); }',
},
};The full type (palette slots, option kinds, per-pattern sizing metadata) ships with the package — import type { PatternDefinition } from 'tabbied'.