npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tabbied

v0.10.0

Published

Tabbied generative patterns: a framework-agnostic core, a <tabbied-pattern> web component, React and Vue components and a Svelte action, powered by css-doodle.

Readme

tabbied

Generative patterns as data: a framework-agnostic core, a <tabbied-pattern> web component, a React and a Vue component and a Svelte action, powered by css-doodle. Render any of Tabbied's preset designs (or your own definition) at any size, reseed them, and export them to PNG.

Try the designs at tabbied.com.

Install

npm install tabbied

React and Vue are optional peer dependencies - you only need them for the tabbied/react and tabbied/vue entry points. tabbied/svelte imports no framework at all, and the core works in any framework (or none).

The package is ESM only. import works everywhere; require() works on Node 20.19+ and 22.12+ (which can require an ES module) and throws ERR_REQUIRE_ESM on older Node.

Entry points

| Import | What it provides | | ------------------ | -------------------------------------------------------------------------------------- | | tabbied | The framework-agnostic core: createPattern, sizing/seed helpers, and the type definitions. | | tabbied/react | The TabbiedPattern React component (and its handle/prop types). | | tabbied/svelte | The tabbied Svelte action, tabbiedAttributes() for the server render, and patternController(). | | tabbied/vue | The TabbiedPattern Vue 3 component. | | tabbied/element | The <tabbied-pattern> custom element, definePatterns() and setPatternsBase(). dist/element/tabbied-element.js is the same element for a CDN, with css-doodle bundled in; it loads two chunks from its own folder, so self-host the whole dist/element/. | | tabbied/element/react-jsx | Types only: <tabbied-pattern> in React's JSX, for a TypeScript app that uses the tag directly. | | tabbied/patterns/<slug> | One design as a default export, with no imports: what the element loads by slug. | | tabbied/patterns | The preset PatternDefinitions (import individually) plus the full patterns record. | | tabbied/snippets | buildSnippet(setup, input): the code that puts one design on a page in React, Vue, Svelte, the web component, plain HTML or plain JavaScript, as the site's editor and the MCP server write it. | | tabbied/svg-export | The native SVG converter (doodleToSvg) on its own, for exporting a rendered <css-doodle> you mounted yourself. exportSvg() on a controller or handle loads it for you. | | tabbied/catalog.json | Every design as data - description, palette, options, SVG-export support. See Finding a design. |

Finding a design

The presets are referenced by slug, and there are a lot of them - the slug alone (cleat, gnomonwedge, karst) won't tell you what a design looks like. catalog.json is the searchable index: one entry per design with a description, closed-vocabulary tags / mood / density / goodFor metadata to filter on, the authored palette, every option and its accepted values, whether it exports to SVG, and a stable preview image URL (https://tabbied.com/previews/<slug>.webp) so you can look before you pick.

// Bundlers (Vite, webpack, Next) import JSON directly. In native Node ESM,
// add the import attribute: `with { type: 'json' }`.
import catalog from 'tabbied/catalog.json';

const heroCandidates = catalog.designs.filter(
  (design) =>
    design.goodFor.includes('hero-background') && design.density === 'sparse'
);

Or from a shell, without writing any code:

npx tabbied list --tag dots --density dense
npx tabbied info radius

The same data is served at tabbied.com/catalog.json, alongside llms.txt and llms-full.txt - an API contract plus a one-line entry per design, sized to fit in one fetch. If you are pointing a coding assistant at this package, give it that URL.

React

import { TabbiedPattern, type TabbiedPatternHandle } from 'tabbied/react';
import { radius } from 'tabbied/patterns';
import { useRef } from 'react';

export function Example() {
  const ref = useRef<TabbiedPatternHandle>(null);

  return (
    <>
      <TabbiedPattern ref={ref} pattern={radius} seed="k9Pz" height={320} />
      <button onClick={() => ref.current?.redraw()}>Redraw</button>
      <button onClick={() => ref.current?.exportImage()}>Export PNG</button>
      <button onClick={() => ref.current?.exportSvg({ download: true })}>
        Export SVG
      </button>
    </>
  );
}

SVG export

exportSvg() converts the rendered pattern to a native vector SVG - real <rect>/<path>/gradient elements, no <foreignObject> - so the file opens in design tools and scales to any resolution. It resolves with { svg, width, height, warnings }; pass { download: true } to also save a .svg file, and { clip: { width, height } } to keep only the top-left box of a canvas the host clips (the grid fit oversizes its canvas to whole tracks, so this is what makes the file show what the page showed). A few designs paint smooth conic-gradient sweeps that SVG cannot represent - they set svgExport: false on their definition, and supportsSvgExport(pattern) tells you whether to offer the option. Effects exported through SVG filters (blur, glow shadows, blend modes) render correctly in browsers but may degrade when imported into design tools; such cases are listed in warnings.

Designs with known export limitations describe them in pattern.svgExportNote (and, for limitations introduced by a toggle option such as a filter-based shadow, option.svgExportNote) - surface these to the user before downloading, the way the Tabbied editor's confirmation dialog does.

The converter itself (about 12 KB gzipped once minified) is not part of the main bundle: exportSvg() loads it on demand via a dynamic import, so apps that never export pay nothing for the feature. To call the converter directly (e.g. on a <css-doodle> you manage yourself), import it from the subpath:

import { doodleToSvg } from 'tabbied/svg-export';

Importing presets (tree-shaking)

pattern takes a PatternDefinition object. Import only the presets you actually render from tabbied/patterns and your bundler ships just those - not the entire catalog:

import { radius, windowpane } from 'tabbied/patterns';

Each preset is a side-effect-free named export, so unused ones are dropped at build time. Need the whole set (e.g. to build a gallery)? Import the patterns record - import { patterns } from 'tabbied/patterns' - and accept that it pulls in every design.

The component is a client component (it registers a browser custom element on import), so in the Next.js App Router render it from a client boundary or rely on its built-in measurable placeholder until it mounts.

Common props

| Prop | Description | | --------- | --------------------------------------------------------------------------- | | pattern | A PatternDefinition - import a preset from tabbied/patterns or pass your own. | | seed | Randomization seed. Omit for a random seed per mount; reseed via the handle. | | palette | Active colors, background (color0) first. Defaults to the preset palette. | | options | Option values keyed by option id; unset options use authored defaults. | | fit | How the drawing meets its box: grid (default), cover, or fixed. | | box props | How big the box is: fill, width, height, maxWidth, maxHeight, aspectRatio. |

See the inline JSDoc on TabbiedPatternProps for the full list.

Sizing the box

A pattern has no intrinsic size, so it takes the size of the box you give it. By default that box fills its containing block - drop one into a sized parent and you're done:

// .panel is 100% wide and 400px tall; the pattern fills it.
<div className="panel">
  <TabbiedPattern pattern={radius} />
</div>

| Prop | Type | Default | Description | | -------------- | ------------------ | ------- | --------------------------------------------------------------- | | fill | boolean | true | width: 100%; height: 100%. fill={false} leaves sizing to CSS. | | width | number \| string | - | Box width. Numbers are px. Overrides fill on that axis. | | height | number \| string | - | Box height. Numbers are px. Overrides fill on that axis. | | maxWidth | number \| string | - | Upper bound on the width. | | maxHeight | number \| string | - | Upper bound on the height. | | aspectRatio | number \| string | - | CSS aspect-ratio, e.g. 3 / 2. Derives the height from the width. |

// Fill the width, cap it, and let the ratio set the height.
<TabbiedPattern pattern={radius} maxWidth={960} aspectRatio={3 / 2} />

// Full-bleed banner.
<TabbiedPattern pattern={radius} height="40vh" />

// Sized by a class name instead.
<TabbiedPattern pattern={radius} fill={false} className="hero-art" />

Mixing them works the way the CSS does - they are the CSS, resolved onto the wrapper element (server render included, so there's no layout shift on mount). 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.

With the core API the host element is yours, so size it however you like - or run the same props through the same helper:

import { resolveBoxStyle } from 'tabbied';

Object.assign(host.style, resolveBoxStyle({ maxWidth: 960, aspectRatio: 3 / 2 }));

Fit modes

fit says how the drawing meets the box. No fit distorts the pattern - nothing is ever scaled by a different factor horizontally than vertically.

  • grid (default) - re-derives the cell grid from the measured box: whole, near-square cells edge to edge at any box shape.
  • cover - draws a fixed-resolution render and scales it uniformly to fill the box (preserving the 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/height in 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.

Web component

<tabbied-pattern
  pattern="radius"
  seed="k9Pz"
  palette="#0B1020, #3E8BFF, #3FFFB2"
  style="display: block; aspect-ratio: 3 / 2; background: #0B1020"
></tabbied-pattern>

<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/element/tabbied-element.js"></script>

<tabbied-pattern> takes the React props as attributes, under the names hydratePatterns() reads without the data- prefix: pattern, seed, palette, options, fit, density, cell-size, width, height, cover-render, redraw-interval, paused. Changing one redraws in place; removing the element tears it down, and moving it does not.

  • One script, only the designs used. tabbied-element.js has css-doodle bundled in. Each design is also published on its own as dist/patterns/<slug>.js, with no imports, and the element fetches only the ones the page names, from beside itself. Copy dist/element/ and dist/patterns/ side by side to self-host, or point setPatternsBase() at the designs.
  • The box comes from inline style. Before the script runs the element is an unknown tag, and its style already applies: write display: block, the size and the ground color there, and nothing shifts when the pattern mounts, on a server-rendered page too.
  • In a bundled app, import tabbied/element and register what it uses, so the bundler ships just those: definePatterns({ radius }) with radius from tabbied/patterns. Or set the pattern property to the definition. Importing it on a server is safe.
  • Frameworks: React 19, Svelte, Solid and Angular set properties on it (palette takes an array, options an object). Vue needs compilerOptions.isCustomElement to match the tag, and Angular CUSTOM_ELEMENTS_SCHEMA. In a TypeScript React app, import 'tabbied/element/react-jsx' once types the tag in JSX (the module is empty at runtime).
  • Scripting: redraw(seed?), exportImage(), exportSvg(), refresh() and controller; a ready event after the first render, and error (reason in detail) for a design that cannot be loaded. It is aria-hidden unless it carries an aria-label, which makes it an image.

Svelte and SvelteKit

<script>
  import { tabbied, tabbiedAttributes, patternController } from 'tabbied/svelte';
  import { radius } from 'tabbied/patterns';

  const props = { pattern: radius, seed: 'k9Pz', aspectRatio: '3 / 2' };
  let host = $state();
</script>

<div bind:this={host} {...tabbiedAttributes(props)} use:tabbied={props}></div>
<button onclick={() => patternController(host)?.redraw()}>Redraw</button>

Pass the same props to both parts; they are the React component's props (above), plus style as a string. The action mounts the pattern, forwards prop changes to it and destroys it with the element.

An action runs only in the browser, so on its own it would leave a SvelteKit server render with an empty, unsized <div> and the page would shift when the pattern mounted. tabbiedAttributes() is pure and runs on the server: it gives the element its box, the pattern's background color, the data-* config and the accessibility attributes, and the action then mounts into it. Put inline style in the props as style rather than in a style attribute beside the spread: Svelte keeps whichever comes last, and the spread carries the clipping the pattern needs.

patternController(element) returns the controller (redraw(), exportImage(), exportSvg()), or null before the action runs. Nothing in tabbied/svelte imports Svelte, so it works the same in Svelte 4 and 5.

Vue and Nuxt

<script setup>
import { ref } from 'vue';
import { TabbiedPattern } from 'tabbied/vue';
import { radius } from 'tabbied/patterns';

const pattern = ref(null);
</script>

<template>
  <TabbiedPattern ref="pattern" :pattern="radius" seed="k9Pz" aspect-ratio="3 / 2" />
  <button @click="pattern?.redraw()">Redraw</button>
</template>

The props are the React component's, in kebab case in a template; onReady is the ready event, and a template ref exposes redraw(), exportImage(), exportSvg() and element. A server render, Nuxt's included, is the sized placeholder, and the pattern mounts into it in the browser: no <ClientOnly>, no layout shift. A class or style on the component lands on its element. Vue 3.3 or later.

Core (framework-agnostic)

The full guide is at tabbied.com/docs/javascript.

import { createPattern } from 'tabbied';
import { radius } from 'tabbied/patterns';

const el = document.querySelector('#stage')!;
const controller = createPattern(el, {
  pattern: radius,
  seed: 'k9Pz',
  // Measured fits (grid/cover) mount asynchronously, after the first
  // ResizeObserver tick delivers the host's size - drive the controller from
  // onReady rather than immediately after createPattern().
  onReady: async () => {
    controller.redraw(); // re-randomize the seed
    await controller.exportImage();
    const { svg } = await controller.exportSvg(); // native vector SVG
  },
});

// later: controller.destroy();

The controller accepts the same config the React component takes as props, minus the box props - the host element is yours to size (or run them through resolveBoxStyle, above). That includes the ambient-redraw timer, so the framework-free API animates without reimplementing it:

const controller = createPattern(host, {
  pattern: radius,
  redrawInterval: 5200, // reseed every 5.2s, morphing via the authored transitions
});

// Hold the ticks without losing the redraw phase; resume with `false`.
controller.update({ paused: true });

redrawInterval drops ticks while the tab is hidden or the host is scrolled out of view - a page of animated patterns only pays for the ones somebody is looking at.

Reduced motion

Under prefers-reduced-motion: reduce the controller suppresses every source of movement, with no configuration:

  • the redrawInterval timer never starts,
  • the designs' own cell transitions are muted, so anything that re-renders cuts to the new arrangement instead of morphing into it, and
  • the keyframe animations a few designs declare are paused on their first frame.

The second half matters more than it sounds. Every design carries a ~400ms transition, and a re-render is not always something the reader asked for: a resize re-derives the grid, so turning a phone or dragging a window animates 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 - 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.

Declarative mounting (no build step)

hydratePatterns() reads a pattern's config off plain data-* attributes, so a static HTML page can describe its patterns in the markup and bring them all up with one call:

<div data-pattern="ortho"
     data-palette="transparent, #C9C8C1, #8E8E88"
     data-fit="grid"
     data-cell-size="132"
     data-redraw-interval="5200"
     style="width: 100%; height: 60vh"></div>

<script type="module">
  // Pin a version: a bare `tabbied` on esm.sh is whatever is latest.
  // `?exports=` trims the presets to the ones the page names.
  import { hydratePatterns } from 'https://esm.sh/[email protected]';
  import { ortho } from 'https://esm.sh/[email protected]/patterns?exports=ortho';

  hydratePatterns({ patterns: { ortho } });
</script>

The attributes are readable rather than a JSON blob - the point is that somebody editing a page can change a color or a slug without decoding anything. Values are typed by the pattern's own option metadata, so a ButtonSelectGroup choice that looks numeric stays a string.

| Attribute | Config field | | --- | --- | | data-pattern | The preset slug. Required - everything else is optional. | | data-seed | seed | | data-palette | palette - comma separated, background first. Splits at top level, so rgb(0, 0, 0) survives. | | data-options | options - id: value pairs separated by ; | | data-fit | fit | | data-cell-size / data-density | cellSize / density | | data-width / data-height | fixed canvas size in px | | data-cover-render | coverRender - 800x800 | | data-redraw-interval / data-paused | redrawInterval / paused |

Pass root to scope the search to a subtree, selector to override [data-pattern], and defaults to merge a config into every match. hydratePatterns returns { element, controller } for each pattern it mounted, and is idempotent - calling it twice won't double-mount.

An unknown slug is reported through onError (a console.warn by default) and skipped, and an attribute that doesn't parse falls back to the design's authored default: a hand-edited template degrades to the pattern's defaults rather than to a blank box.

patternConfigToAttributes() is the inverse, for generating such markup. TabbiedPattern uses it on its own placeholder, so a server-rendered React page already emits everything hydratePatterns needs - which is what lets a prerendered page be repackaged as a framework-free template.

Use one or the other on a given element: TabbiedPattern mounts its own controller, so an unscoped hydratePatterns() on a React page would mount a second one on top of it. That's what root is for.

CLI

The package ships a tabbied bin for rendering without writing an app - generate an asset, or a video-ready frame sequence, straight from a shell:

npx tabbied render radius --seed k9Pz --size 1600x900 --out hero.svg
npx tabbied render bight --palette "#0B1020,#3E8BFF" --out banner.png
npx tabbied render radius --frames 120 --reseed-every 30 --size 1920x1080 --out frames/
npx tabbied list --good-for hero-background --density sparse
npx tabbied info radius

Rendering runs css-doodle in a headless browser through Playwright, which this package does not install. The CLI uses whichever of playwright, playwright-core or @playwright/test it finds, from the current directory first, then from its own install:

npm i -D playwright && npx playwright install chromium     # in a project
npx -y -p tabbied -p playwright tabbied render radius --out hero.svg   # nothing installed

The second form needs the browser downloaded once too (npx playwright install chromium). Pass --browser <path> (or set TABBIED_CHROMIUM) to use a specific Chromium binary. --out's extension (or --format svg|png) picks the format, and an SVG is cut to --size in every fit, as the PNG is; frame sequences are PNG, cut deterministically between seeds (frames within a reseed window are identical, so encoders can deduplicate), and named frame-000.png on, so ffmpeg -i frames/frame-%03d.png reads any sequence of up to 1,000 frames. Every flag also takes --flag=value. Run npx tabbied --help for every flag.

For AI agents

AGENTS.md and llms.txt ship in this package - the latter is the complete agent-facing reference (also served at tabbied.com/llms-full.txt). Point your coding assistant at either.

License

MIT © Sy Hong and Ye Joo Park