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() {
// The box fills its parent by default; height={320} pins one axis.
return <TabbiedArtwork artwork={radius} seed="k9Pz" 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#
Sizing splits in two: the box props say how big the element is, and fit says how the drawing meets that box. An artwork has no intrinsic size, so by default the box simply fills its containing block — drop one into a sized parent and you are done.
<TabbiedArtwork artwork={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">
<TabbiedArtwork artwork={radius} />
</div>
// Fill the width, cap it, and let the ratio set the height.
<TabbiedArtwork artwork={radius} maxWidth={960} aspectRatio={3 / 2} />
// Pin one axis; numbers are px, strings are CSS.
<TabbiedArtwork artwork={radius} height={320} />
<TabbiedArtwork artwork={radius} height="40vh" maxHeight={520} />
// Hand sizing back to a class name.
<TabbiedArtwork artwork={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 artwork 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. 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).fixed— renders at an explicit canvas size,width/heightin px (default 360 × 540). This is 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.
Removed in 0.2.0: fit="stretch", which kept the authored grid and let cells deform with the box. Use grid (the default) for a box-shaped grid, or cover to scale a render uniformly.
Each column below is one mode, drawing the same artwork 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="contain"The whole render, letterboxed. The bars are the artwork background.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
<TabbiedArtwork artwork={radius} fit="grid" />
// cover: a fixed-resolution render scaled uniformly 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" />
// fixed: an explicit canvas size in px (what the Tabbied editor uses)
<TabbiedArtwork artwork={radius} fit="fixed" width={360} height={540} />Artworks whose layout isn't a repeating grid opt out of grid entirely — Symmetry draws one centered composition, so it only offers cover, contain and fixed:
fit="cover"Scaled up until it fills, cropping the overflow.fit="contain"Scaled down until it fits, letterboxing the rest.// Symmetry is one composition rather than a repeating grid, so it
// declares sizing.allowed = ['cover', 'contain', 'fixed'] and defaults
// to cover. Asking for "grid" falls back to that default with a warning.
<TabbiedArtwork artwork={symmetry} fit="cover" />
<TabbiedArtwork artwork={symmetry} fit="contain" />Colors & 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"
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"
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}
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} 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' | 'cover' | 'contain' | 'fixed' | per artwork | 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, 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'.