react-raffle-picker
v0.2.2
Published
Headless React raffle picker for giveaways, winner draws, countdowns, and slot-machine UIs.
Maintainers
Readme
A headless, composable React component for giveaways, raffles, and slot-machine UIs. Cycles numbers or names with smooth animations and freezes on a winner. Performant on slow devices — high-frequency tick updates bypass React.
npm install react-raffle-pickerPreview
Idea
react-raffle-picker ships as a headless compound component, not a monolithic widget. The root owns the engine — cycling, phase machine, freeze logic — and exposes it through React context. Sub-components (Value, Button, Countdown, Slots) are dumb consumers that you compose anywhere in your tree.
This means:
- No layout opinions. Wrap pieces in your own card, modal, sidebar, paragraph.
- No style props soup. Style each piece directly via
className/style. - One source of truth. All state lives in the root. Sub-components read context.
- Performance preserved. Tick updates write to DOM imperatively via refs — no React re-render per tick. Context only re-renders on phase boundaries (start, settle, freeze).
import { RafflePick } from 'react-raffle-picker'
;<RafflePick min={1} max={100} interval={100} inertia onSelect={(v) => console.log(v)}>
<RafflePick.Value animation="roll" className="my-value" />
<RafflePick.Button startLabel="Pick" stopLabel="Stop" />
</RafflePick>Styles
The library is headless — no global stylesheet is required for Value, Button, or Countdown. You bring your own CSS.
For the slot reel (<RafflePick.Slots>), a minimal stylesheet is required to make the column animate. Two ways to load it:
Auto-injected at runtime —
<RafflePick.Slots>injects a<style data-rrp-slot-base>tag intodocument.headon first mount. No action needed in CSR apps.Static import (recommended for SSR / strict CSP / full control):
import 'react-raffle-picker/styles.css'This file also includes opt-in keyframes for
<RafflePick.Value animation="roll|fade|blur|reel" />. Override or replace any selector in your own CSS.
Components
<RafflePick> (root)
Provides context. Renders an optional wrapper element (as prop, default 'div').
| Prop | Type | Default | Notes |
| -------------- | --------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| min, max | number | 1, 100 | Numeric range. Ignored if items provided. |
| items | string[] | — | Switches to item mode (cycles through names). |
| interval | number (ms, clamped ≥ 50) | 100 | Tick speed. |
| random | boolean | true | Random pick vs sequential. |
| inertia | boolean | false | Soft start / soft stop ramp. |
| autoStart | boolean | true | Begin cycling on mount. |
| initialValue | number \| string | — | Starting display before first run. Number for min/max mode, string for items mode. For <Slots>, each character seeds the corresponding reel. |
| finalValue | number \| string | — | Forces settle to land on this value. Cycle still appears random; only final freeze is rigged. For <Slots>, each character is the final char of the corresponding reel. |
| onSelect | (value) => void | — | Fires once per round on freeze. |
| as | ElementType | 'div' | Wrapper tag. |
| className | string | — | Wrapper class. |
| style | CSSProperties | — | Wrapper style. |
| children | ReactNode | — | Sub-components. |
<RafflePick.Value>
Renders the cycling value. Updates textContent imperatively each tick (no React re-render).
| Prop | Type | Default |
| ----------- | -------------------------------------- | -------- |
| animation | 'roll' \| 'fade' \| 'blur' \| 'reel' | 'roll' |
| as | ElementType | 'span' |
| className | string | — |
| style | CSSProperties | — |
Multiple Value instances inside one root are supported — all subscribe to the same tick.
<RafflePick.Button>
Toggles start / freeze based on phase. Disabled during settling.
| Prop | Type | Notes |
| ------------ | --------------- | ------------------------------------------------ |
| startLabel | ReactNode | Shown in idle / frozen (click starts). |
| stopLabel | ReactNode | Shown in running / starting (click stops). |
| waitLabel | ReactNode | Shown in settling (button disabled). |
| children | ReactNode | Fallback label when state-specific label absent. |
| className | string | — |
| style | CSSProperties | — |
<RafflePick.Countdown>
Schedules auto-freeze after seconds. Renders an SVG ring + numeric label by default. Optional render-prop for custom output.
| Prop | Type | Notes |
| ----------- | ---------------------------------- | ---------------------------------------------- |
| seconds | number (required) | Auto-freeze delay. Renders only while running. |
| className | string | — |
| style | CSSProperties | — |
| children | (remaining: number) => ReactNode | Render-prop for custom UI. |
<RafflePick.Slots>
Independent multi-reel slot machine. Each reel ticks on its own and stops with a stagger.
initialValue (root prop) seeds reels char-by-char before the first run. finalValue rigs the freeze so each reel lands on the corresponding character — useful for predetermined winners or scripted demos.
| Prop | Type | Default | Notes |
| -------------------------------------------------- | -------------------------- | -------------- | -------------------------------------------- |
| length | number | 3 | Number of reels. |
| chars | string | '0123456789' | Charset pool. Emoji-safe (code-point split). |
| spinInterval | number (ms, ≥ 50) | 80 | Tick rate per reel. |
| staggerMs | number | 220 | Delay between consecutive reel stops. |
| onResult | (joined: string) => void | — | Fires when the last reel lands. |
| className, slotClassName, style, slotStyle | various | — | Style hooks. |
Recipes
Inline chip in a sentence
<RafflePick min={1} max={36} as="p" autoStart={false}>
Roulette landed on <RafflePick.Value animation="roll" className="chip" /> —{' '}
<RafflePick.Button startLabel="spin again" stopLabel="stop" className="link-btn" />
</RafflePick>Hero with countdown ring
<RafflePick min={1} max={999} inertia autoStart={false} className="hero">
<RafflePick.Countdown seconds={5} className="hero__ring" />
<RafflePick.Value animation="blur" className="hero__value" />
<RafflePick.Button startLabel="Start Draw" stopLabel="Stop" className="hero__btn" />
</RafflePick>Rigged draw with predetermined winner
finalValue lands the freeze on a specific value while the cycle still looks random — useful for staged demos, scripted reveals, or showing a known winner.
<RafflePick
items={['Alice', 'Bob', 'Carol']}
initialValue="Alice"
finalValue="Bob"
autoStart={false}
onSelect={(winner) => console.log(winner)} // always 'Bob'
>
<RafflePick.Value animation="reel" />
<RafflePick.Button startLabel="Draw" stopLabel="Reveal" />
</RafflePick>Slot machine with custom result handler
<RafflePick inertia autoStart={false}>
<RafflePick.Slots
length={5}
chars="0123456789"
staggerMs={260}
onResult={(code) => console.log('winning code:', code)}
/>
<RafflePick.Button startLabel="Spin" stopLabel="Stop" />
</RafflePick>Roadmap
- Winner position — emit row/index alongside value (
onSelectreceives{ value, index }) and a<RafflePick.WinnerPosition>consumer that reflects the landed position. bounceanimation — vertical hop on each tick, easing back to baseline.glitchanimation — offset color-channel pulse for a digital noise feel during running.- Headless
useRafflePick()hook for users who want zero rendering from the lib. - Render-prop variant of
<Value>for fully custom DOM.
Performance notes
- Cycle ticks (every
intervalms) writetextContentdirectly via refs — no React render. - Phase machine re-renders only on transitions (start, inertia step, settle, freeze).
- CSS animation duration is bound to
--rrp-tickso each cycle aligns with one tick — no cross-frame tearing. will-changeis scoped to running / inertia phases only, so idle / frozen text renders with crisp subpixel anti-aliasing.
License
MIT
