@rikaido/react
The React components
Headless, composable, ready to serve. Every example below runs for real, on this page, with no key.
Install
npm install @rikaido/react @rikaido/sdkCompiled TypeScript, React 18 or later, nothing else. Every component is headless: it exposes data-rikaido="…" attributes and render props, and carries no styles of its own. @rikaido/react/atelier.css is one way to draw them — it is the one these examples wear — and yours is another.
import { RikaidoProvider } from "@rikaido/react";
import "@rikaido/react/atelier.css";
<RikaidoProvider apiKey={import.meta.env.RIKAIDO_KEY} locale="en">
{/* everything below */}
</RikaidoProvider>The verdict
The harness returns a verdict; Verdict shows it as a re-read — eight rows in the validator's order, each clean, noted, failed, refused or not for this dish. Here, a peanut allergy meets a peanut-butter satay.
useVerdict(recipe, constraints) calls the API and caches the answer per request body. Verdict.Rows draws the eight rows; Verdict.Lock says the lock in a word: “Not served.”, “Served.”, “Served, with its notes.” The sentences come from the published spec, in English or French.
Verdict.Root{ verdict, locale?, attribution?, children }children can be a function ({ rows, safe, ok }) => … to draw everything yourself.Verdict.Rows{ showSkipped?, children? }children(row, i) to draw each row; showSkipped={false} hides the ones that did not run.Verdict.Row{ row }Verdict.Issue{ issue }Verdict.Lock{}useVerdict(recipe, constraints?)Async<Verdict>{ status: 'idle' | 'loading' | 'ready' | 'error', data?, error? }.verdictRows(verdict, locale)VerdictRow[]With a render prop you draw all of it yourself, and the structure stays its own:
Exposed attributes: data-rikaido="verdict" (data-safe, data-ok), verdict-rows, verdict-row (data-check, data-state), verdict-mark, verdict-check, verdict-findings, verdict-issue (data-code, data-severity: error, warn, safety), verdict-text, verdict-subject, verdict-code, verdict-lock.
The cast
Two portraits per ingredient: the kawaii drawing, a 64 × 64 SVG rendered inline, and the 3D toy, a WebP served by address and fetched by the component because it travels with your key.
IngredientPortrait{ name, form?, size?, alt?, fallback?, attribution?, children? }form: kawaii (default) or toy. children({ portrait, toyUrl, status }) to draw it all yourself. A name the cast lacks shows fallback, or the initial.useIngredientPortrait(name)Async<Portrait>{ slug, kawaiiSvg, kawaiiUrl, toyUrl }.useToyUrl(name)Async<string>blob: URL, revoked when the component leaves.Exposed attributes: data-rikaido="portrait" (data-form, data-status), portrait-kawaii, portrait-toy, portrait-fallback.
The graph
What the graph knows about an ingredient: what it is made of, what replaces it. AllergenBadges takes the union over a whole list and says when an ingredient is unknown to it — an empty row because the graph never saw the food must never read as “allergen-free”.
AllergenBadges{ names, locale?, attribution?, children? }children({ allergens, unknown, status }) to draw it yourself.Substitutions{ name, locale?, attribution?, children? }useIngredient(name, { neighbours? })Async<GraphIngredient>{ canonical, known, facts, substitutions, art, neighbourhood? }.allergenLabel(code, locale)stringExposed attributes: data-rikaido="allergens" (data-status, data-unknown), allergen (data-allergen), allergen-none, allergen-unknown, substitutions, substitution (data-role), substitution-swap, substitution-ratio, substitution-why, substitution-caveat.
Reading a source
A URL, a markdown or a pasted text; the recipe it holds, in the shape the harness reads.
RecipeReader{ onRead?, locale?, placeholder?, submitLabel?, children? }children(state) to draw the state under the field.useRecipeRead(){ state, read(source, locale?), reset }The small things
Pure, no network, no key — what every cooking app rewrites.
Scaling
“1 ½”, “2-3”, “3/4” read as numbers, multiplied, written back as nice fractions. What cannot be read — “a pinch” — stays as written.
useScaledRecipe(recipe, servings, baseServings, locale?)RecipescaleRecipe(recipe, factor, locale?)Recipescaled (a number or null) and display.parseQuantity(text) · formatQuantity(n, locale?)number | null · stringUnits
Metric and imperial, both ways: g ↔ oz and lb, ml ↔ tsp, tbsp and cups, °C ↔ °F. An unknown unit comes back untouched.
The timer
One reducer, one interval, and the two things a cook wants to know: how long is left, and did it ring. It does not drift: the remaining time is computed off the clock, not off the ticks.
useTimer(totalMs, { onDone?, tickMs? }){ clock, remainingMs, progress, running, done, start, pause, reset, add }clock is “1:30” or “1:00:00”.timerReducer · remainingOf · formatClockpureSteps, one at a time
Cook mode's pager: it holds the index, you draw the step. The keyboard is already wired on props.
useCookSteps(steps, initial?){ step, index, count, isFirst, isLast, next, prev, go, props, tempC }props on the container: role="group", the “2 / 4” label, tabIndex, and onKeyDown for ←, →, Space, Enter, Home, End.The mark
Every component that shows Rikaido's data — the verdict, a portrait, the graph's facts — carries “Powered by Rikaido”, a quiet link with the seal. That is the free tier's other half: the code is Apache-2.0, the mark stays. attribution="mark" keeps only the seal; attribution="none" is for an account with a card on file.
import { PoweredByRikaido, RikaidoSeal } from "@rikaido/react";
<PoweredByRikaido /> // the seal and the words, in the provider's locale
<PoweredByRikaido variant="mark" /> // the seal alone, the words for screen readers
<RikaidoSeal size={24} /> // the seal, for your own linkUnstyled, or styled by you
Nothing here imposes a look. Every piece carries a data-rikaido attribute and, when it has a state, a data-* that names it. One stylesheet targeting those attributes dresses everything at once:
[data-rikaido="verdict-row"][data-state="refused"] { color: crimson; }
[data-rikaido="allergen"] { border-radius: 999px; padding: 2px 10px; background: #fee; }
[data-rikaido="attribution"] { font-size: 12px; opacity: 0.7; }@rikaido/react/atelier.css does exactly that, in the atelier's finish. Read it as a starting point.