AstroBaaS

Storefront & headless

Quantity Selector

Free — GPL coresize Splanned, not built

Generated from docs/plan/core/quantity-selector/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.

A React input component for selecting product quantity at checkout. Enforces min/max, respects stock levels, validates integers, and provides clear error states. Accessible, touch-friendly, dark-mode aware, and keyboard-navigable. Prevents invalid orders before submission.

The problem

Customers type invalid quantities (negative, decimals, zero) or exceed available stock; invalid orders reach the backend, wasting time on cleanup. Merchants want a foolproof quantity widget that prevents bad orders before checkout, reducing support overhead.

What it does

  • Numeric input field (integers only; rejects decimals and negative numbers)
  • Increment/decrement buttons (+ and - buttons or arrow buttons, configurable)
  • Min validation (default 1, configurable per product; enforces minimum)
  • Max validation (default 999, but stock_available overrides to actual available stock)
  • Clear error message on violation (e.g., ‘Only 5 left in stock’ or ‘Minimum quantity is 1’)
  • Disabled state when stock is 0 (max = 0, cannot increment, shows ‘Out of stock’)
  • Keyboard support (arrow-up/arrow-down increment/decrement; Enter confirms; Tab navigates to buttons)
  • Touch-friendly buttons (44px minimum tap target per WCAG, works on all touch devices)
  • Dark mode support (CSS variables —theme-text, —theme-bg, etc.)
  • Full accessibility (role=‘spinbutton’, aria-label, aria-valuenow, aria-valuemin, aria-valuemax)
  • Loading state (grayed out while order submits)
  • Initial quantity prop (default 1, configurable)
  • onChange callback (fires on every change; debounced if needed)

What it deliberately does NOT do

Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.

  • Bulk pricing (quantity discounts, e.g., ‘10+ get 10% off’) — pricing logic is a paid feature; selector is input-only
  • Pre-filled quantity from URL params — merchant’s routing responsibility; app parses URL and sets initial prop
  • Direct cart sync (component doesn’t update cart; just fires onChange callback; app owns state management)
  • Persistent quantity history (‘Last time you ordered 5, suggest 5?’) — personalization is paid; selector is stateless
  • Barcode scanner input — hardware integration is merchant’s concern; selector is keyboard/touch input only
  • Per-variant stock limits (e.g., red size-S has different stock than blue size-S) — each variant gets own selector instance

Data model

none

API

  • No new routes. Component uses Product.stock_available from existing GET /api/products/:id endpoint

Every route added here must also appear in src/pages/openapi.json.ts — a test fails the build if it does not.

Admin

Component docs in admin with props reference (min, max, disabled, onChange callback examples). No merchant configuration needed; all customization via React props.

The seam — why this is core

Core owns the quantity input component, integer validation, stock-based max enforcement, accessibility, and dark mode. Paid pack owns nothing; input widgets are developer infrastructure.

Core owns the interface + honest quantity widget; checkout UX is infrastructure, not a per-country obligation or credential.

Dependencies

  • React 18+ (peer dependency)
  • Product type from API (for stock_available field)
  • CSS for dark mode (inline or CSS module)

Acceptance checks

Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.

  • Component starts at 1; user clicks + button 9 times; quantity becomes 10 (each click adds 1)
  • User types ‘0’; component displays error ‘Quantity must be at least 1’ and resets to 1
  • User types ‘-5’; component shows error and resets to 1
  • User types ‘2.5’; component strips decimal, sets to 2, shows error ‘Enter whole numbers only’
  • Product has 3 in stock; user clicks +, button is disabled after reaching 3 (max equals stock)
  • Product has 0 stock; component is disabled and shows ‘Out of stock’
  • User presses arrow-up on keyboard; quantity increments by 1
  • User presses Enter; onChange callback fires with new quantity value
  • Dark mode: CSS variable —theme-mode = ‘dark’; buttons invert colors and text is readable
  • Component is fully keyboard-navigable: Tab moves to +/- buttons, arrow keys in input, screen readers announce quantity changes

Risks

Race condition: stock changes between component load and purchase; backend must re-validate at checkout. Input blur: if user tabs out with invalid value, component must reset or show error (test blur handling). Mobile numeric keypad: iOS may use comma as decimal separator; validate and strip non-digits. Stock cache: if stock changes server-side, component doesn’t refresh (app must poll or subscribe via webhook). Integer overflow: if max is set to 999,999,999, JavaScript number is safe (2^53-1 limit), but enforce sensible server-side limits.

Commercial context

Suggested pricefree (core)
Rival anchorShopify: included (theme components); Magento: included

The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.


Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full catalogue and ../../AI-GUIDE.md for how to work on this repository.