WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/moon-blocks-toss.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

72
<MoonBlocksToss />

Installation

npx shadcn@latest add https://webberui.com/r/moon-blocks-toss.json

Or, 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

PropTypeDefaultDescription
valueMoonBlocksResult | nullControlled: the result currently shown, null when nothing has been tossed yet; when omitted the component is uncontrolled
defaultValueMoonBlocksResult | nullnullInitial result in uncontrolled mode
onChange(result: MoonBlocksResult | null) => voidCallback when the result changes: the new result on landing, null when "reset" is pressed
onResult(result: MoonBlocksResult, history: MoonBlocksResult[]) => voidCallback on every landing, with the full history including this toss (oldest first)
labelsPartial<MoonBlocksLabels>see table belowCustom outcome names and hints (fields you leave out keep their defaults)
requireThreebooleanfalseRequire three sheng-bei in a row: shows 3 progress dots, and result.confirmed becomes true when the third sheng-bei lands
questionTextstringQuestion text shown above the altar table
buttonTextstring"擲筊"Text of the toss button under the table
sizenumber72Width of a single block (px); table height and throw height scale with it
weightsMoonBlocksWeights{ 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() => numberMath.randomRandom source returning a number in [0, 1); inject a seeded generator to make results reproducible
disabledbooleanfalseDisable tossing
classNamestringForwarded to the outermost container

MoonBlocksResult

FieldTypeDefaultDescription
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
indexnumberOrdinal of this toss (starts at 1, resets to zero after "reset")
streaknumberIn requireThree mode, consecutive sheng-bei count up to and including this toss (0–3; always 0 otherwise)
confirmedbooleanIn requireThree mode, whether this toss completed the run of three sheng-bei

MoonBlocksLabels

FieldTypeDefaultDescription
shengstring"聖杯" (sheng-bei)Name of the sheng-bei outcome
xiaostring"笑杯" (xiao-bei)Name of the xiao-bei outcome
yinstring"陰杯" (yin-bei)Name of the yin-bei outcome
shengHintstring"允杯——同意,可以放心去做" (approved — go ahead)Hint shown under sheng-bei
xiaoHintstring"笑而不答——問法再想想,再擲一次" (a smile, no answer — rephrase and toss again)Hint shown under xiao-bei
yinHintstring"不允——這次先別,換個方式問" (not approved — hold off, ask differently)Hint shown under yin-bei
idlestring"點桌面或按下方按鈕擲出" (tap the table or press the button to toss)Prompt before the first toss
tossingstring"筊杯落地中……" (blocks are landing…)Prompt while the blocks are in the air
streakstring"連續聖杯" (consecutive sheng-bei)Caption next to the progress dots in requireThree mode
confirmedstring"三聖杯!定案" (three sheng-bei — settled)Hint shown when three sheng-bei in a row are reached
resetstring"重來" (reset)Text of the reset button

MoonBlocksWeights

FieldTypeDefaultDescription
shengnumber2Relative weight of sheng-bei
xiaonumber1Relative weight of xiao-bei
yinnumber1Relative 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.random during 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 rotateX and 1–2 turns of rotateY (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 with backface-visibility: hidden, so which one shows is decided purely by the angle
  • Arc and landing: y runs 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 and onResult fires 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 requireThree on, each sheng-bei increments streak and any other outcome resets it to zero; the third sheng-bei sets confirmed to true and swaps in the "settled" hint. The next toss starts counting from zero again
  • Controlled mode: once value is passed, the picture follows the parent's result (including both faces), and pressing "reset" calls onChange with null

Accessibility

  • The whole altar table is a single native button (type="button"), so Enter / Space tosses; its aria-label carries the button text and the previous outcome, aria-busy marks a toss in flight, and aria-describedby points at the result region. Blocks, shadows and dust are decorative and hidden with aria-hidden
  • The result region is role="status" with aria-live="polite", so screen readers announce the outcome name and hint once the blocks land; the requireThree progress dots use role="img" with an aria-label reading "consecutive sheng-bei n / 3"
  • Both the table and the reset button have focus-visible focus rings; disabled lowers 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

On this page