Introduction#
The tabbied package ships three entry points:
tabbied/react— theTabbiedArtworkcomponent. It renders an artwork into a normal, CSS-sizeable box, like an<img>.tabbied/artworks— 100+ preset designs as tree-shakeableArtworkDefinitionexports.tabbied— the framework-agnostic core (createArtwork) plus all shared types and sizing helpers.
Patterns are deterministic: the same artwork, seed, grid and options always draw the same design, at any size. That makes artworks 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 artwork's background color (correct size, zero layout shift); the live pattern takes over once it mounts.
import { TabbiedArtwork } from 'tabbied/react';
import { radius } from 'tabbied/artworks';
export function Banner() {
return (
<TabbiedArtwork
artwork={radius}
seed="k9Pz"
fit="cover"
style={{ width: '100%', height: 320 }}
/>
);
}Importing presets#
artwork takes an ArtworkDefinition 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, symmetry } from 'tabbied/artworks';
// Building a gallery? The full record pulls in every design.
import { artworks } from 'tabbied/artworks';Browse every design (and its options) in the gallery — the preset export name is the slug in the editor URL.
Sizing & fit modes#
Size the component like any block element — CSS width and height, grid/flex tracks, aspect ratios. The fit prop decides how the artwork relates to that box:
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 into the box, preserving the authored proportions of fixed-px strokes and shadows. For grid-driven artworks the render follows the box's aspect ratio and re-derives its grid, so the pattern is never cut off mid-cell; special layouts (e.g. Symmetry's centered composition) scale-and-crop instead.contain— letterboxes the render at its authored ratio (the artwork's background color fills the bars).stretch— keeps the authored grid and stretches it to fill. Cells distort with the box, so prefergridunless you specifically want that.fixed— renders at explicitwidth/heightprops (what the Tabbied editor uses).
Each artwork declares a sensible default, so fitis optional. Requesting a fit an artwork can't support falls back to its default with a console warning.
// grid (default): the cell grid adapts to the container size
<TabbiedArtwork artwork={radius} fit="grid" />
// cover: a fixed-resolution render scaled to fill the box; grid-driven
// artworks adapt the render to the box's shape (whole cells, no mid-cell
// crop), special layouts like Symmetry scale-and-crop
<TabbiedArtwork artwork={radius} fit="cover" />
// contain: letterboxed at the artwork's authored ratio
<TabbiedArtwork artwork={symmetry} fit="contain" />
// stretch keeps the authored grid and distorts cells with the box;
// fixed renders at explicit width/height propsColors & palettes#
Pass palette to recolor a design — the background color (color0) comes first, followed by the inks. Passing fewer colors than the artwork 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.
<TabbiedArtwork
artwork={radius}
seed="k9Pz"
// color0 (the background) comes first
palette={['#0b132b', '#5bc0be', '#6fffe9', '#ff6b6b']}
fit="cover"
style={{ width: '100%', height: 280 }}
/>// Any CSS color works for a slot — including 'transparent',
// which drops the background entirely.
<TabbiedArtwork
artwork={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 (artwork.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, a shape frequency, and a shadow toggle.
<TabbiedArtwork
artwork={radius}
seed="k9Pz"
options={{ grid: '4x6', shadow: true }}
fit="cover"
style={{ width: '100%', 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 seedprop 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 { TabbiedArtwork, type TabbiedArtworkHandle } from 'tabbied/react';
import { radius } from 'tabbied/artworks';
export function Reseedable() {
const ref = useRef<TabbiedArtworkHandle>(null);
return (
<>
<TabbiedArtwork ref={ref} artwork={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.
Ambient animation#
Set redrawIntervalto 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 artworks only pays for what's on screen; the whole timer is skipped under prefers-reduced-motion. Use the paused prop for your own gating on top (it preserves the timer phase).
// Reseed on a timer (the gallery's shimmer). Ticks are skipped while
// the tab is hidden or the element is outside the viewport, and the
// whole timer is skipped under prefers-reduced-motion.
<TabbiedArtwork
artwork={quilt}
fit="cover"
redrawInterval={2000}
style={{ width: '100%', height: 280 }}
/>Accessibility#
By default the artwork 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 artwork's display name).
// Decorative (default): hidden from assistive tech.
<TabbiedArtwork artwork={radius} />
// Meaningful image: exposed with role="img" and a label.
<TabbiedArtwork
artwork={radius}
decorative={false}
ariaLabel="Generative pattern of quarter circles"
/>Motion is opt-in only (redrawInterval) and always disabled for users with prefers-reduced-motion.
Server rendering#
TabbiedArtworkis 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 artwork'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 { TabbiedArtwork } from 'tabbied/react';
import { radius } from 'tabbied/artworks';
export default function Page() {
return <TabbiedArtwork artwork={radius} style={{ height: 320 }} />;
}Vanilla JavaScript#
The React component is a thin wrapper over the framework-free engine. createArtwork(host, config) mounts an artwork into any element and returns a controller with update(), redraw(), exportImage() and destroy(). It accepts the same config the component takes as props.
import { createArtwork } from 'tabbied';
import { radius } from 'tabbied/artworks';
const controller = createArtwork(document.querySelector('#stage'), {
artwork: radius,
seed: 'k9Pz',
// Measured fits (grid/cover/contain) 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 artwork is removed:
controller.destroy();API reference#
<TabbiedArtwork /> props
| Prop | Type | Default | Description |
|---|---|---|---|
| artworkrequired | ArtworkDefinition | — | The artwork to render — a preset from tabbied/artworks or your own definition. |
| seed | string | random | Pattern 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' | 'stretch' | 'cover' | 'contain' | 'fixed' | per artwork | How the artwork fills its box (see Sizing & fit modes). |
| 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 | 360 × 540 | fit="fixed" — canvas size in px. |
| coverRender | { width, height, cropTop? } | 800 × 800 | cover/contain 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 artwork has mounted. |
| element | The raw <css-doodle> element, for power users. |
ArtworkDefinition
Presets are plain data. You can author your own — the renderer only cares about the shape:
import type { ArtworkDefinition } from 'tabbied';
const myArtwork: ArtworkDefinition = {
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-artwork sizing metadata) ships with the package — import type { ArtworkDefinition }from 'tabbied'.