Lucky Wheel
Lucky Wheel / Spin to Win#
Interactive HTML5 canvas prize wheel with server-side outcome determination. Works in two modes: product-tied (quick setup on any raffle product) and standalone (dedicated admin page, usable anywhere in your store — cart, checkout, thank-you, popup, or shortcode).
Two Ways to Create a Wheel#
| Product Wheel | Standalone Wheel | |
|---|---|---|
| Created via | Product edit screen → PRO Features | WooCommerce → Lucky Wheels → Add New |
| Tied to a product | Yes — one raffle product | No — independent |
| Display on product page | Automatic (before form, after form, or after summary) | Via Display Rules or shortcode |
| Cart / Checkout / Thank-you | No | Yes |
| Popup / Modal | No | Yes |
| Email gate | No | Yes |
| Coupon scope | Auto-restricted to that product | Configurable — any product, category, or store-wide |
| Shortcode | [raffle_wheel id="PRODUCT_ID"] | [raffle_wheel id="WHEEL_ID"] |
Product Wheel (Quick Setup)#
Best for raffle owners who want a wheel on their product page with minimal configuration.
- Edit a raffle product.
- In the PRO Features section, check Lucky Wheel.
- Choose Quick setup (configure slices inline) or Use existing wheel (link a standalone wheel).
Standalone Wheel#
Best for store-wide marketing: cart abandonment recovery, checkout incentives, lead generation, or post-purchase re-engagement.
- Go to WooCommerce → Lucky Wheels → Add New.
- Give it a title (e.g. “Summer Sale Spinner”).
- Configure slices, display rules, coupon settings, and spin limits.
- Publish.
Configuring Slices#
Each slice of the wheel represents one possible outcome. A wheel needs at least 2 slices.
| Field | Description |
|---|---|
| Colour | Visual colour on the wheel (colour picker) |
| Label | Text displayed on the slice (e.g., “10% Off”, “Free Ticket!”) |
| Weight | Probability weight. Higher number = more likely. Weights are relative to all slices. |
| Prize type | No prize, Discount %, Discount (fixed), Free ticket, Custom message |
| Prize value | The value: percentage, currency amount, or custom text |
Free Ticket (standalone wheels)#
When using a standalone wheel, the Free Ticket prize type shows an additional Target Product dropdown. This lets you select which product the 100% discount coupon applies to. If left empty, the coupon has no product restriction.
Default slices#
When enabling a wheel for the first time, these defaults are pre-populated:
| Label | Type | Weight | Effect |
|---|---|---|---|
| 10% Off | Discount % | 20 | 10% coupon |
| Try Again | No prize | 35 | Better luck next time |
| 25% Off | Discount % | 10 | 25% coupon |
| Better Luck | No prize | 30 | Better luck next time |
| Free Ticket! | Free ticket | 5 | 100% discount coupon |
| 5% Off | Discount % | 25 | 5% coupon |
Display Rules (Standalone Wheels)#
Configure where standalone wheels appear automatically, without shortcodes.
Product pages#
| Option | Behaviour |
|---|---|
| Specific products | Select one or more products. The wheel renders on those product pages. |
| All products in categories | Select categories. The wheel renders on every product in those categories. |
| Do not show | The wheel does not auto-display on any product page. |
When shown on product pages, the wheel renders before the add-to-cart form by default.
WooCommerce pages#
| Option | Behaviour |
|---|---|
| Cart page | Renders above the cart. Works with both classic (shortcode) and block-based cart. |
| Checkout page | Renders above the checkout form. Works with both classic and block-based checkout. |
| Thank-you page | Renders on the order confirmation page. Supports a minimum order total threshold — the wheel only shows if the order meets the minimum. |
Block & Classic compatibility#
All placements detect whether WooCommerce is using the block-based cart/checkout (default since WooCommerce 8.3) or the legacy shortcode pages and hook in correctly for both.
Popup / Modal Mode#
Show the wheel as a popup triggered by user behaviour.
Settings#
| Setting | Description |
|---|---|
| Enable popup | Toggle on/off |
| Trigger | Delay (seconds after page load), Scroll (% scrolled), Exit intent (mouse leaves viewport) |
| Delay | Seconds to wait (for delay trigger) |
| Scroll % | Percentage of page scrolled (for scroll trigger) |
| Page scope | All pages, WooCommerce pages only, or Shop & product pages only |
| Cooldown | Days before the popup shows again to the same visitor (stored in localStorage) |
How it works#
- The popup renders as a native HTML
<dialog>element in the page footer. - The selected trigger fires on the appropriate event.
- The dialog opens with a backdrop overlay and entrance animation.
- The close button (×) or clicking outside dismisses it.
- After closing, the cooldown prevents it from showing again for the configured number of days.
Coupon Settings#
All auto-generated coupons share these characteristics:
- Single-use (1 redemption)
- Configurable expiry (default: 7 days, overridable per wheel or via global settings)
- Optional minimum order amount
- Described as “Lucky Wheel prize — [wheel name]“
Coupon scope (standalone wheels)#
| Scope | Behaviour |
|---|---|
| Store-wide | No product/category restriction |
| Specific product | Coupon only valid for the selected product |
| Specific category | Coupon only valid for products in the selected category |
For product-tied wheels, coupons are automatically scoped to that raffle product.
Auto-cleanup#
Expired wheel-generated coupons are automatically trashed by a daily WP-Cron job. This can be toggled on/off in the global Pro settings.
Email Gate & Lead Generation#
Require visitors to enter their email address before they can spin. Turns the wheel into a lead generation tool.
Enabling#
- Edit a standalone wheel.
- In the Spin Limits meta box, check Require email before spinning.
- Optionally enable the consent checkbox and customise the consent text (e.g. “I agree to receive marketing emails”).
How it works#
- The wheel renders with an email form overlay instead of the Spin button.
- The visitor enters their email (and ticks consent if required).
- The email is validated and checked for duplicates (same email cannot spin the same wheel twice).
- On success, the overlay disappears and the Spin button becomes active.
- If the visitor wins a coupon, the coupon code is emailed to them using WooCommerce’s branded email templates.
GDPR consent#
When enabled, a checkbox with custom text appears below the email input. The form cannot be submitted without ticking it. Consent status is recorded in the database.
Integration hooks#
After an email is collected, the action rfwc_pro_wheel_email_collected fires with four parameters:
do_action( 'rfwc_pro_wheel_email_collected', $email, $wheel_id, $row_id, $consent );
Use this to integrate with Mailchimp, Mailpoet, ConvertKit, or any other email service.
Admin: Collected Emails#
Go to WooCommerce → Lucky Wheels → Collected Emails to view all collected emails.
| Feature | Description |
|---|---|
| Filter by wheel | Dropdown to show emails for a specific wheel |
| Pagination | 30 emails per page |
| CSV export | One-click download of all emails (or filtered by wheel) |
CSV columns: Email, Wheel, Coupon Code, Consent (Yes/No), IP, Date.
Thank-You Page / Post-Purchase#
Show the wheel on the order confirmation page as a re-engagement tool: “You just ordered — spin for a reward on your next purchase!”
Order-aware features#
| Feature | Description |
|---|---|
| Purchase threshold | Only show the wheel if the order total meets a configurable minimum |
| One spin per order | Prevents refresh abuse — each order can only spin each wheel once (tracked in order meta) |
| Customer-tied coupons | Won coupons are email-restricted to the customer’s billing email |
| Raffle-aware mode | If the order contains raffle products with their own wheel, that product-specific wheel shows first |
Sound Effects#
When enabled, the wheel plays sounds using the Web Audio API (no external files):
| Event | Sound |
|---|---|
| Spinning | Tick-tick as the pointer passes each slice |
| Win | Ascending fanfare |
| Lose | Descending tone |
Sound can be toggled per wheel or set as a global default.
Spin Tracking#
| User type | Storage |
|---|---|
| Logged-in users | User meta (_rfwc_pro_wheel_spins) |
| Guests | WooCommerce session |
| Thank-you page | Order meta (_rfwc_pro_order_spun_wheels) |
The server enforces the max spins limit — even if the frontend button is visible, exceeding the limit returns an error.
Shortcode#
[raffle_wheel id="123"]
| Attribute | Values | Default | Description |
|---|---|---|---|
id | Post ID | Required | Product ID (for product wheels) or Wheel ID (for standalone wheels) |
The shortcode handler automatically detects whether the ID belongs to a product or a standalone wheel and renders accordingly.
Global Pro Settings#
Go to WooCommerce → Settings → Raffle → Pro to configure store-wide defaults.
| Setting | Default | Description |
|---|---|---|
| Default Max Spins | 1 | Max spins per user for new wheels (0 = unlimited) |
| Default Coupon Expiry | 7 days | How long wheel coupons last |
| Default Sound Effects | On | Enable sound by default |
| Auto-delete Expired Coupons | On | Daily cron to trash expired wheel coupons |
| Default Email Gate | Off | Require email by default |
Per-wheel settings always override global defaults. Empty/unset per-wheel values fall back to the global value.
How Spinning Works (Technical)#
- Customer clicks Spin! (or submits email first if email gate is active).
- An AJAX request is sent to the server with the wheel/product ID (and order ID if on thank-you page).
- The server validates: wheel exists, user hasn’t exceeded max spins, email gate passed, order not already spun.
- A winning slice is picked using weighted random selection (
random_int— cryptographically secure). - If the prize requires a coupon, a WooCommerce coupon is created with the configured settings.
- The server returns the winning slice index and a rotation angle.
- The frontend animates the wheel (4.5 second easing animation) to land on the winning slice.
- A result card appears showing the outcome — with a copy-to-clipboard button for coupon codes.
Security: The outcome is always determined server-side. The frontend animation is purely visual — the wheel always lands on the server-determined slice.
Prize Types#
| Type | What happens |
|---|---|
| No prize | ”Better luck next time” message. No action taken. |
| Discount % | A single-use WooCommerce coupon with the specified percentage off. |
| Discount (fixed) | A single-use fixed-amount coupon. |
| Free ticket | A 100% discount coupon for 1 unit (product wheels: that product; standalone: configurable target product). |
| Custom message | Displays the configured text. No coupon generated. Useful for “Come back tomorrow” or “Follow us on Instagram” type prizes. |
CSS Classes#
Wheel#
| Element | CSS Class |
|---|---|
| Container | .rfwc-pro-lucky-wheel |
| Title | .rfwc-pro-lucky-wheel__title |
| Canvas container | .rfwc-pro-lucky-wheel__container |
| Canvas | .rfwc-pro-lucky-wheel__canvas |
| Pointer | .rfwc-pro-lucky-wheel__pointer |
| Spin button | .rfwc-pro-lucky-wheel__spin-btn |
| Result container | .rfwc-pro-lucky-wheel__result |
| Win card | .rfwc-pro-lucky-wheel__result-card--win |
| Lose card | .rfwc-pro-lucky-wheel__result-card--lose |
| Coupon code | .rfwc-pro-lucky-wheel__coupon-code |
Email Gate#
| Element | CSS Class |
|---|---|
| Container | .rfwc-pro-wheel-email-gate |
| Prompt text | .rfwc-pro-wheel-email-gate__prompt |
| Form wrapper | .rfwc-pro-wheel-email-gate__form |
| Email input | .rfwc-pro-wheel-email-gate__input |
| Consent label | .rfwc-pro-wheel-email-gate__consent |
| Submit button | .rfwc-pro-wheel-email-gate__submit |
| Error message | .rfwc-pro-wheel-email-gate__error |
Popup#
| Element | CSS Class |
|---|---|
| Dialog | .rfwc-pro-wheel-popup |
| Inner wrapper | .rfwc-pro-wheel-popup-inner |
| Close button | .rfwc-pro-wheel-popup-close |
Files Reference#
| File | Purpose |
|---|---|
class-rfwc-pro-lucky-wheel.php | Core wheel class — product mode, rendering, AJAX spin, prize generation |
class-rfwc-pro-lucky-wheel-cpt.php | Standalone wheel CPT registration, admin columns |
class-rfwc-pro-lucky-wheel-metaboxes.php | Edit screen meta boxes (slices, display rules, coupon, limits, shortcode) |
class-rfwc-pro-lucky-wheel-placements.php | Auto-display on cart/checkout/thank-you (block + classic), popup rendering |
class-rfwc-pro-lucky-wheel-email-gate.php | Email collection, custom table, AJAX, coupon emailing, admin page |
class-rfwc-pro-settings.php | Pro global settings (WooCommerce → Settings → Raffle → Pro) |
lucky-wheel.js | Frontend canvas drawing, animation, spin AJAX, email gate JS |
admin-lucky-wheel.js | Admin slice repeater, source toggle, prize type toggle |
lucky-wheel-popup.js | Popup trigger logic (delay, scroll, exit intent), localStorage cooldown |
lucky-wheel.css | Frontend wheel + email gate styles |
lucky-wheel-popup.css | Popup dialog styles |
admin.css | Admin meta box styles |
Action & Filter Hooks#
| Hook | Type | Parameters | Description |
|---|---|---|---|
rfwc_pro_wheel_email_collected | Action | $email, $wheel_id, $row_id, $consent | Fires after email is collected (email gate) |
rfwc_pro_wheel_spin_completed | Action | $wheel_id, $prize_data, $session_email | Fires after a standalone wheel spin completes |