Documentation

The tabbied package

Tabbied's generative artworks as a library: a framework-agnostic core plus a React component, powered by css-doodle. Render any preset (or your own definition) at any size, recolor it, reseed it, and export it to PNG.

tabbied · MITnpmGitHub

Introduction#

The tabbied package ships three entry points:

  • tabbied/react — the TabbiedArtwork component. It renders an artwork into a normal, CSS-sizeable box, like an <img>.
  • tabbied/artworks — 100+ preset designs as tree-shakeable ArtworkDefinition exports.
  • 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.

terminal
npm install tabbied

Quick 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 with cellSize (px) or density (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 prefer grid unless you specifically want that.
  • fixed — renders at explicit width/height props (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.

fit="grid"
fit="cover"
fit="contain"
// 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 props

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"
  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

PropTypeDefaultDescription
artworkrequiredArtworkDefinitionThe artwork to render — a preset from tabbied/artworks or your own definition.
seedstringrandomPattern seed. Omit for a random seed per mount; reseed via the handle.
palettestring[]preset paletteActive colors, background (color0) first. Shorter palettes cycle their inks into the unused slots.
optionsRecord<string, OptionValue>authoredOption values keyed by option id; unset options use authored defaults.
fit'grid' | 'stretch' | 'cover' | 'contain' | 'fixed'per artworkHow the artwork fills its box (see Sizing & fit modes).
cellSizenumber36fit="grid" — target cell size in px.
density0 | 1 | 2 | 3 | 44fit="grid" — authored density level, an alternative to cellSize.
width / heightnumber360 × 540fit="fixed" — canvas size in px.
coverRender{ width, height, cropTop? }800 × 800cover/contain render resolution override.
redrawIntervalnumberoffRe-randomize the seed every N ms (uncontrolled seed only). Paused off-screen, in hidden tabs, and under reduced motion.
pausedbooleanfalsePause redrawInterval ticks without resetting the timer.
decorativebooleantruetrue renders an aria-hidden image; false exposes role="img" with ariaLabel.
onReady() => voidCalled once the first pattern render is committed.
className / stylestring / CSSPropertiesApplied to the wrapper box.

Handle (ref)

MemberDescription
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.
elementThe 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'.