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

recipe-table

v0.2.0

Published

Render recipes as tabular recipes (ingredients are rows, steps are columns) in React. Plain-CSS theming, JSON Schema, agent-friendly.

Readme

recipe-table

Render recipes as tabular recipes in React or as a Web Component: ingredients are rows, steps are columns, and each step spans exactly the ingredients it combines. Reading left to right, the table narrows until one cell is left: the dish.

+-----------------------+-------+-------------------------------------+-----------------------------------------+
| 2 cups / 250 g flour  |       |                                     |                                         |
+-----------------------+       |                                     |                                         |
| 2 tbsp sugar          | whisk |                                     |                                         |
+-----------------------+       |                                     |                                         |
| 2 tsp baking powder   |       |                                     |                                         |
+-----------------------+       |                                     |                                         |
| ½ tsp salt            |       | fold together, a few lumps are fine | cook, 2 min, per side, on a hot griddle |
+-----------------------+-------+                                     |                                         |
| 2 eggs                |       |                                     |                                         |
+-----------------------+       |                                     |                                         |
| 1½ cups / 360 ml milk | whisk |                                     |                                         |
+-----------------------+       |                                     |                                         |
| 3 tbsp butter, melted |       |                                     |                                         |
+-----------------------+-------+-------------------------------------+-----------------------------------------+

The notation was invented by Michael Chu at Cooking for Engineers. This package is an independent implementation.

  • One component, one required prop, zero runtime dependencies (React 18 or 19 as a peer).
  • A <recipe-table> custom element for pages without React: same markup, same CSS.
  • Styling is plain, optional CSS in a cascade layer, driven by --rt-* custom properties. Your CSS always wins.
  • Recipe data is plain JSON with a JSON Schema and a validator whose errors explain how to fix them.
  • Accessible: a real <table> with row headers and headers associations, plus an ordered step list for screen readers and narrow screens.

Status: 0.2. The API may change before 1.0.

Install

npm install recipe-table

Use

import { RecipeTable } from "recipe-table";
import "recipe-table/styles.css"; // optional

<RecipeTable recipe={recipe} />;

| Prop | Type | Default | | | --- | --- | --- | --- | | recipe | Recipe | required | Validated on every change. Invalid data renders an error block, never a wrong table. | | scale | number | 1 | Multiplies quantities and yield. Durations and temperatures never scale. | | view | "auto" \| "table" \| "list" | "auto" | auto switches to the step list when the component is narrower than 36rem. | | locale | string | runtime | Number formatting, e.g. "de-DE". | | labels | Partial<RecipeTableLabels> | English | UI strings and unit names. Recipe text is never translated. | | headingLevel | 2–6 | 2 | Level of the title heading. | | className, style | | | On the root. style accepts tokens: { "--rt-radius": 0 }. | | classNames | Partial<Record<RecipeTablePart, string>> | | Extra classes per part, e.g. for Tailwind. | | onError | (errors) => void | | Called once per invalid recipe. |

Recipe format

A recipe is a flat list of ingredients and steps. Steps name their inputs by id. Every ingredient and every step except the last is used by exactly one step; the last step is the finished dish.

{
  "schemaVersion": 1,
  "title": "Pancakes",
  "yield": { "amount": 4, "unit": "servings" },
  "ingredients": [
    { "id": "flour", "name": "flour", "quantity": { "value": 2, "unit": "cup", "alt": [{ "value": 250, "unit": "g" }] } },
    { "id": "eggs", "name": "eggs", "quantity": { "value": 2 } },
    { "id": "milk", "name": "milk", "quantity": { "value": 1.5, "unit": "cup" } }
  ],
  "steps": [
    { "id": "whisk", "action": "whisk", "inputs": ["eggs", "milk"] },
    { "id": "fold", "action": "fold together", "inputs": ["flour", "whisk"] },
    { "id": "cook", "action": "cook", "inputs": ["fold"], "duration": "PT2M", "note": "per side" }
  ]
}
  • Quantities are numbers (0.5, not "1/2") with a canonical unit code; ranges are [min, max].
  • alt holds the source's second unit. It is never computed.
  • "scalable": false keeps an amount fixed when scaling (a pinch of salt).
  • Steps with no ingredients ("Preheat the oven") go in prep.
  • An ingredient used in two places ("1 cup sugar, divided") is listed twice with different ids.
  • Durations are ISO 8601: "PT1H15M".

Full reference: recipe.schema.json. Examples: examples/.

import { validateRecipe } from "recipe-table";

const result = validateRecipe(json);
if (!result.ok) console.error(result.errors);
// [{ path: "/steps/0/inputs/1", code: "unknown_input",
//    message: "Step \"mix\" uses \"c\", which does not exist.", hint: "Known ingredient and step ids: a, b, mix." }]

Styling

Four levels, cheapest first:

  1. Tokens. Set custom properties on any ancestor:
    .my-recipes {
      --rt-font-family: "Inter", sans-serif;
      --rt-color-step-bg: oklch(95% 0.03 80);
      --rt-radius: 0;
    }
  2. Parts. Every element has a stable class (rt-ingredient, rt-step, rt-quantity, ...) and state attributes (data-depth, data-root, data-optional). Style them with ordinary CSS; no !important needed.
  3. classNames prop for utility classes: classNames={{ step: "bg-amber-100" }}.
  4. Unstyled. Skip the CSS import for bare semantic HTML, or use useRecipeTable() and render your own markup.

Light and dark defaults follow prefers-color-scheme. Force one with data-theme="light" or data-theme="dark" on any ancestor. All tokens and parts: tokens.json.

Without React

<recipe-table> element

For plain HTML, Vue, Svelte, Angular or a CMS. Without a build step, two tags in <head> load it, and the recipe JSON goes straight into the page:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/styles.css" />
<script type="module" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/define.js"></script>

<recipe-table scale="2" locale="en-US">
  <script type="application/json">{ "schemaVersion": 1, "title": "Pancakes", ... }</script>
</recipe-table>

With a bundler:

import { defineRecipeTable } from "recipe-table/element";
import "recipe-table/styles.css";

defineRecipeTable(); // once
document.querySelector("recipe-table").recipe = recipe; // or set it from code

| | | | --- | --- | | Attributes | scale, view, locale, heading-level (also properties; headingLevel) | | Properties only | recipe, labels, classNames. There is no recipe attribute. | | Event | recipe-error, bubbles, event.detail.errors |

When a server writes untrusted recipe data into the page, serialize it with recipeToInlineJson(recipe) from recipe-table/core, not JSON.stringify: a </script> in any recipe string would otherwise end the script and inject HTML.

It renders in the light DOM, so styling works exactly as above: tokens on any ancestor or on the element, part classes, data-theme. Vue needs isCustomElement for the tag; Angular needs CUSTOM_ELEMENTS_SCHEMA. Full description: custom-elements.json.

Core and CLI

recipe-table/core has everything except the components: validateRecipe, layoutRecipe, scaleRecipe, formatQuantity, renderRecipeText.

npx recipe-table validate my-recipe.json
npx recipe-table preview my-recipe.json --scale 2

Develop

npm install
npm test          # vitest
npm run dev       # visual test bench: React at /, the element (inline JSON) at /element.html
npm run build

Licence

MIT. Recipe-table notation by Michael Chu.