Moon Blocks Toss
Throw two crescent divination blocks with a 3D tumbling animation; the landing resolves to sheng-bei / xiao-bei / yin-bei and fires onResult for playful yes/no decisions
A playful nod to temple culture: two red crescent-shaped moon blocks (pure CSS, one face flat and one face rounded) rest on a gradient altar table and breathe gently while idle. Click the table or press Enter / Space and each block flies along its own random arc, tumbles several turns, lands with a small bounce and kicks up dust. Once settled, the pair resolves as "one flat, one rounded = sheng-bei (yes)", "both flat = xiao-bei (laughing, no answer)", "both rounded = yin-bei (no)" and the outcome is shown as a large word plus a short hint. You can require three sheng-bei in a row, inject a seeded random source, adjust the weights of the three outcomes, and read the full history through onResult — a good fit for giveaways, decision toys and festival landing pages.
npx shadcn@latest add https://webberui.com/r/moon-blocks-toss.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<MoonBlocksToss />
Installation
npx shadcn@latest add https://webberui.com/r/moon-blocks-toss.jsonOr, once registries are configured in components.json, install it as @webberui/moon-blocks-toss.
Usage
import { MoonBlocksToss } from "@/components/ui/moon-blocks-toss";
<MoonBlocksToss
questionText="今天要不要吃鹹酥雞?"
buttonText="擲筊問問看"
requireThree
onResult={(result, history) => {
console.log(`Toss #${result.index}: ${result.outcome}`, result.faces);
if (result.confirmed) console.log("Three sheng-bei — settled!", history);
}}
/>To make results reproducible (tests, or a fixed "fortune of the day"), swap rng for a seeded generator; weights adjusts the relative odds of the three outcomes (for entertainment only — the default is already the theoretical 2:1:1 ratio of two fair blocks):
import {
MoonBlocksToss,
pickMoonBlocksOutcome,
resolveMoonBlocks,
} from "@/components/ui/moon-blocks-toss";
// A tiny linear congruential generator: the same seed always yields the same sequence
function seeded(seed: number) {
let s = seed >>> 0;
return () => {
s = (s * 1664525 + 1013904223) >>> 0;
return s / 4294967296;
};
}
<MoonBlocksToss rng={seeded(20260818)} weights={{ sheng: 3, xiao: 1, yin: 1 }} />;
// The resolver and the sampler can be used on their own
resolveMoonBlocks("flat", "round"); // "sheng"
pickMoonBlocksOutcome(Math.random); // "sheng" | "xiao" | "yin"Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | MoonBlocksResult | null | — | Controlled: the result currently shown, null when nothing has been tossed yet; when omitted the component is uncontrolled |
defaultValue | MoonBlocksResult | null | null | Initial result in uncontrolled mode |
onChange | (result: MoonBlocksResult | null) => void | — | Callback when the result changes: the new result on landing, null when "reset" is pressed |
onResult | (result: MoonBlocksResult, history: MoonBlocksResult[]) => void | — | Callback on every landing, with the full history including this toss (oldest first) |
labels | Partial<MoonBlocksLabels> | see table below | Custom outcome names and hints (fields you leave out keep their defaults) |
requireThree | boolean | false | Require three sheng-bei in a row: shows 3 progress dots, and result.confirmed becomes true when the third sheng-bei lands |
questionText | string | — | Question text shown above the altar table |
buttonText | string | "擲筊" | Text of the toss button under the table |
size | number | 72 | Width of a single block (px); table height and throw height scale with it |
weights | MoonBlocksWeights | { sheng: 2, xiao: 1, yin: 1 } | Relative weights of the three outcomes (entertainment only; the default is the theoretical 2:1:1 ratio of two fair blocks) |
rng | () => number | Math.random | Random source returning a number in [0, 1); inject a seeded generator to make results reproducible |
disabled | boolean | false | Disable tossing |
className | string | — | Forwarded to the outermost container |
MoonBlocksResult
| Field | Type | Default | Description |
|---|---|---|---|
outcome | "sheng" | "xiao" | "yin" | — | Resolved outcome: sheng-bei (聖杯, yes) / xiao-bei (笑杯, laughing) / yin-bei (陰杯, no) |
faces | ["flat" | "round", "flat" | "round"] | — | Which face is up on each block (left, right); flat is the flat face, round the rounded face |
index | number | — | Ordinal of this toss (starts at 1, resets to zero after "reset") |
streak | number | — | In requireThree mode, consecutive sheng-bei count up to and including this toss (0–3; always 0 otherwise) |
confirmed | boolean | — | In requireThree mode, whether this toss completed the run of three sheng-bei |
MoonBlocksLabels
| Field | Type | Default | Description |
|---|---|---|---|
sheng | string | "聖杯" (sheng-bei) | Name of the sheng-bei outcome |
xiao | string | "笑杯" (xiao-bei) | Name of the xiao-bei outcome |
yin | string | "陰杯" (yin-bei) | Name of the yin-bei outcome |
shengHint | string | "允杯——同意,可以放心去做" (approved — go ahead) | Hint shown under sheng-bei |
xiaoHint | string | "笑而不答——問法再想想,再擲一次" (a smile, no answer — rephrase and toss again) | Hint shown under xiao-bei |
yinHint | string | "不允——這次先別,換個方式問" (not approved — hold off, ask differently) | Hint shown under yin-bei |
idle | string | "點桌面或按下方按鈕擲出" (tap the table or press the button to toss) | Prompt before the first toss |
tossing | string | "筊杯落地中……" (blocks are landing…) | Prompt while the blocks are in the air |
streak | string | "連續聖杯" (consecutive sheng-bei) | Caption next to the progress dots in requireThree mode |
confirmed | string | "三聖杯!定案" (three sheng-bei — settled) | Hint shown when three sheng-bei in a row are reached |
reset | string | "重來" (reset) | Text of the reset button |
MoonBlocksWeights
| Field | Type | Default | Description |
|---|---|---|---|
sheng | number | 2 | Relative weight of sheng-bei |
xiao | number | 1 | Relative weight of xiao-bei |
yin | number | 1 | Relative weight of yin-bei |
Only the ratio matters; if every weight is 0 or negative the component falls back to the default ratio. resolveMoonBlocks(left, right) (resolve an outcome from two faces), pickMoonBlocksOutcome(rng, weights?) (weighted sampling), DEFAULT_MOON_BLOCKS_LABELS, and the MoonBlocksOutcome / MoonBlockFace types are also named exports, so the same rules can be reused outside the component.
How it works
- Resolution rule: each block has one flat face (the "yang" side, brighter) and one rounded face (the "yin" side, darker with a raised shadow). One flat plus one rounded is sheng-bei (yes), both flat is xiao-bei (laughing, no answer), both rounded is yin-bei (no); two fair blocks give a theoretical 2:1:1 ratio, which is the default
weights - Pick the outcome first, then the faces: randomness is only consumed inside the click handler (never
Math.randomduring render, to avoid hydration mismatches). The outcome is sampled by weight, then the two faces are derived from it — for sheng-bei the flat block is chosen at random so the picture is not identical every time - Rotation only ever increases: every toss adds another 2–4 turns of
rotateXand 1–2 turns ofrotateY(random direction) on top of the previous resting angle, and the face-up side is encoded as a 0° / 180° half-turn offset, so motion continues from the current angle instead of unwinding to zero. The two faces sit back to back withbackface-visibility: hidden, so which one shows is decided purely by the angle - Arc and landing:
yruns through five keyframes — launch, apex, landing, small bounce, rest — and the tumbling stops at the landing instant; the table shadow shrinks and fades in sync, and the dust specks are delayed until touchdown before scattering sideways. The result is committed andonResultfires only after both blocks report their animation complete, so there is no timer and nothing to clean up on unmount - The table is the button: the whole altar surface is a native
button, so clicking the table, the visual button or using the keyboard all toss; repeated triggers mid-flight are ignored - Three in a row: with
requireThreeon, each sheng-bei incrementsstreakand any other outcome resets it to zero; the third sheng-bei setsconfirmedtotrueand swaps in the "settled" hint. The next toss starts counting from zero again - Controlled mode: once
valueis passed, the picture follows the parent's result (including both faces), and pressing "reset" callsonChangewithnull
Accessibility
- The whole altar table is a single native
button(type="button"), so Enter / Space tosses; itsaria-labelcarries the button text and the previous outcome,aria-busymarks a toss in flight, andaria-describedbypoints at the result region. Blocks, shadows and dust are decorative and hidden witharia-hidden - The result region is
role="status"witharia-live="polite", so screen readers announce the outcome name and hint once the blocks land; therequireThreeprogress dots userole="img"with anaria-labelreading "consecutive sheng-bei n / 3" - Both the table and the reset button have
focus-visiblefocus rings;disabledlowers opacity and blocks interaction - When the user has "reduce motion" enabled at the system level, the tumbling, arc, dust and idle breathing are all disabled — pressing the button settles the blocks and shows the result immediately
Invoice Lottery Checker (React)
A React component for Taiwan's uniform-invoice lottery: type the last 3 or all 8 digits of an invoice number and match it against the prize numbers you pass in — confetti, a rolling prize amount and a prize badge on a win, a gentle head-shake and a one-tap check-the-next-one button on a miss.
Solar Terms Timeline (React)
A horizontal timeline of the 24 solar terms for React: locates the current term for any date, counts down to the next one, with seasonal colour bands and phenology / seasonal-food cards; term dates come from the standard 21st-century approximation, getSolarTerms and currentSolarTerm are exported, and almanac values can override the formula.