Skip to content

Use it in your project ​

Korokoro is an API and a tray, each usable without the other: an API of plain functions (roll, read notation, work out odds, keep a history), and a tray you mount into any element, which also comes as a React component, a Vue component and a web component.

1. The API alone ​

ts
import { checkNotation, distributionOf, roll, seededSource } from "@johnmorrisdotca/korokoro";

const read = checkNotation("4d6dl1");            // { ok: true, spec } or the part refused and why
if (read.ok) {
  const thrown = roll(read.spec, seededSource("table-7"));
  thrown.faces;                                  // [6, 4, 2, 1]: every die, in the order thrown
  thrown.kept;                                   // [true, true, true, false]: the 1 was dropped
  thrown.total;                                  // 12
  distributionOf(read.spec).probabilities;       // the exact chance of every total from 3 to 18
}

2. The tray, in plain HTML ​

html
<div id="dice"></div>
<script type="module">
  import { mountRoller } from "@johnmorrisdotca/korokoro";

  mountRoller(document.getElementById("dice"), {
    spec: { count: 1, sides: 20, modifier: 5 },
    onRoll: (roll) => console.log(roll.total),
  });
</script>

3. React ​

tsx
import { DiceRoller } from "@johnmorrisdotca/korokoro/react";

export function Table() {
  return <DiceRoller wide notation="2d20kh1+5" onRoll={(roll) => save(roll)} />;
}

The component takes the tray's options as props, notation as a shorter way to give the dice (the tray follows it when it changes), and any attribute for its <div>. The tray mounts in the browser after the first render, so server rendering draws an empty box and nothing needs a provider. In Next.js, use it from a client component ("use client").

4. Vue ​

vue
<script setup>
import { DiceRoller } from "@johnmorrisdotca/korokoro/vue";
</script>

<template>
  <DiceRoller notation="2d20kh1+5" wide @roll="(roll) => save(roll)" />
</template>

The props are the tray's options, with notation as a shorter way to give the dice, and each roll is a roll event. The dice and locale are followed as they change; roll(), history(), setSpec() and setLocale() are there on a template ref. It renders an empty box on the server (Nuxt included) and mounts the tray in the browser. Vue 3.3 or later.

5. A web component ​

html
<korokoro-roller notation="2d20kh1+5" wide></korokoro-roller>

<script type="module">
  import { defineRoller } from "@johnmorrisdotca/korokoro/element";

  defineRoller();
  document.addEventListener("korokoro-roll", (event) => console.log(event.detail.total));
</script>

A custom element, for any page and any framework that renders HTML. Call defineRoller() once; each roll is a korokoro-roll event that bubbles, with the roll as its detail.

Or with no call at all: importing @johnmorrisdotca/korokoro/element/define registers the element by being imported, so one script tag is the whole of it, from a CDN or from your own bundle:

html
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/korokoro@1/dist/element-define.js"></script>
<korokoro-roller notation="2d20kh1+5"></korokoro-roller>
AttributeWhat it does
notationThe dice showing at first, and again whenever it changes
langja for Japanese, anything else English; the page's own language when left out
wideTray and panels side by side on a wide screen
sizesmall is the felt and the result alone, medium adds the choice of dice, large (or left out) is everything
sound="off"No sound and no mute button
hold="off"Dice are not held
placeholder="off"The opening dice are the user's own roll
keyboard="off"Space rolls only when the focus is inside the tray
language-chooserThe tray's own choice of English or 日本語
storage="none", storage-keyKeep no history; or the key it is kept under
animation-ms, share-base, queryAs the options of the same names
clothThe felt's cloth: green (unless said), blue, red, black or wood, the family's five; changed in place, keeping the dice and the rolls
one-pipThe colour of a d6's one pip: red (unless said) or black; changed in place

What an attribute cannot carry (a theme, your own words, your own sound) goes on the element's options property. roll(), setSpec() and history are on the element. The tray is drawn in the element's own light DOM, so the page's --kk-… variables theme it as they do a mounted tray.

One die on its own ​

Rolls: tap it
Shows a face, never rolls

A single die with nothing round it: no felt, no total, no panels. For a page that wants a die to look at, or one to tap. There are two kinds, chosen by one option.

  • A die that rolls (rollable, the default) is a button. A tap, Enter or Space throws it; it tumbles, lands on the face the generator chose before anything moved, and says so to a screen reader.
  • A die that does not roll (rollable: false) is a picture of one face, the one you give it, changed from code with show().
ts
import { mountDie } from "@johnmorrisdotca/korokoro";

const die = mountDie(document.getElementById("die"), { sides: 20, size: "large", onRoll: (face) => console.log(face) });
die.roll();                                            // as a tap does

mountDie(document.getElementById("shown"), { sides: 6, face: 5, rollable: false, onePip: "black" });

It keeps one steady square (small 48 pixels, medium 96, large 150, or width): the tumble moves only the picture inside it, so nothing on the page moves. Nothing on it can be selected, and a device that asks for reduced motion gets no tumble. It is silent unless you say sound: true, because a die on somebody's page has not been asked to make noise.

OptionDefaultWhat it does
sides62 to 1000, or "F" for a Fate die
facesnoneA die of your own, as in notation's d[Yes,No]: [{ label: "Yes" }, { label: "No" }]
facethe top faceThe face showing at first
rollabletruefalse is a die that only shows a face
size, width"medium""small", "medium" or "large"; or a width in pixels, which wins
onePip"red"The colour of a d6's one pip: "red" or "black"
sourcecryptographicseededSource("table") throws the same faces for everyone
animationMs600The tumble; reduced motion always skips it
sound, playSoundfalseWhether a throw makes the sound of dice, and a sound of your own
onRollnoneCalled with the face once it has landed
cloth, theme, locale, stringsAs on the tray

The handle has roll(), show(face), setSides(sides, face?, faces?), setOnePip(colour), setLocale(locale), setRollable(on), destroy(), and face, rolling and element.

As a tag, from the same script as <korokoro-roller>:

html
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/korokoro@1/dist/element-define.js"></script>
<korokoro-die sides="20" size="large"></korokoro-die>
<korokoro-die sides="6" face="5" rollable="off"></korokoro-die>
AttributeWhat it does
sides, faceThe kind of die, and the face showing
rollable="off"A die that shows and does not roll; rollable="on" makes it roll again
size, widthsmall, medium, large, or pixels
one-pipred (unless said) or black
lang, seed, sound, animation-ms, clothAs the options of the same names; sound="on" for dice sounds

Each throw is a korokoro-die-roll event with the face as its detail; changing face shows that face at once. The demo has one of each.

Embed it on any site ​

A tray on a page you do not build: a blog, a wiki, a forum. Choose how much of it you want.

SizeWhat is shownRoom at 360px wide
smallThe felt and the result, for the dice you name. Tap to rollabout 540px tall
mediumThose, and the choice of dice and bonusabout 1320px
largeEverything: history, stats and odds too. Side by side from 900px wideabout 1600px

An iframe, where the page allows no scripts:

html
<iframe src="https://johnmorrisdotca.github.io/korokoro/embed/?dice=2d6%2B3&size=small" title="Korokoro" width="360" height="540" style="border:0;max-width:100%" loading="lazy"></iframe>

The address takes dice (any notation), size, lang (en or ja), sound=off, seed, and the colours felt and ink as #rrggbb. The page tracks nothing and loads nothing from anywhere else; a large tray keeps its history on the visitor's own device. Each roll is sent to the page that frames it: { korokoro: "roll", notation, total, dice } by postMessage.

One tag, where the page may run a script, with nothing to install:

html
<script type="module" src="https://cdn.jsdelivr.net/npm/@johnmorrisdotca/korokoro@1/dist/element-define.js"></script>
<korokoro-roller notation="2d6+3" size="small"></korokoro-roller>

The demo writes both for the dice you last rolled there, with a look at each size.

6. Svelte and Angular ​

The same one call in the framework's mount hook, and destroy() on the way out:

svelte
<script>
  import { onMount } from "svelte";
  import { mountRoller } from "@johnmorrisdotca/korokoro";
  let box;
  onMount(() => {
    const roller = mountRoller(box);
    return () => roller.destroy();
  });
</script>

<div bind:this={box}></div>
ts
// Angular: in a standalone component with <div #box></div> in its template
private box = viewChild.required<ElementRef<HTMLElement>>("box");
constructor() {
  afterNextRender(() => (this.roller = mountRoller(this.box().nativeElement)));
}
ngOnDestroy() {
  this.roller?.destroy();
}

Each of the six (Vue, Svelte, Angular, React, the web component and a plain page) is built from the packed tarball and rolled in Chromium and WebKit by scripts/check-frameworks.mjs before a release names it.

What a developer gets ​

  • Typed results. TypeScript types for everything, with a doc comment on every export.
  • A random source you can replace. The default is the platform's cryptographic generator; seededSource("any text") is reproducible; and anything with a next() that returns a 32-bit number will do.
  • No dependencies, ES modules, a default export condition for tools that resolve from CommonJS, and sideEffects: false, so a bundler drops what you do not import.
  • Sizes. Rolling, notation and odds alone are about 16 kB minified (6 kB gzipped) once a bundler has shaken the rest out. With the tray it is about 98 kB (35 kB gzipped). The recorded sounds are another 36 kB (23 kB gzipped), fetched only when a roll first needs them.
  • Where it runs. Browsers from Chrome and Edge 111, Firefox 113 and Safari 16.2. The core runs in Node 20 and later, Deno and Bun.

MIT © John Morris