Commerce & Checkout

Convenience Store Pickup

Three-step convenience-store pickup picker: chain → city/district → store list with search, hours and a selected-store summary card; brand-neutral, data supplied by props

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

The "pick up at a convenience store" flow that shows up in almost every Taiwanese e-commerce checkout, packaged as a self-contained stepper panel: step one picks the chain with color-swatch chips, step two derives city and district dropdowns from the store data, and step three lets you choose from a searchable list — each row shows the address, opening hours, a 24-hour badge and (optionally) a distance label. Once a store is chosen the panel collapses into a summary card (chain swatch + store name + address + a "Change" button) and calls onSelect(store). Steps slide horizontally, list items stagger in and the selected item gets a check mark; it supports controlled value and uncontrolled defaultValue, an isLoading skeleton and fully overridable copy. The component ships no real brand logos or names — chains and stores always come in through props.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/convenience-store-pickup.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

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

Active edited usageDefault props
<ConvenienceStorePickup/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/convenience-store-pickup.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/convenience-store-pickup.

Usage

import {
  ConvenienceStorePickup,
  type PickupChain,
  type PickupStore,
} from "@/components/ui/convenience-store-pickup";

// Chains and stores come from your API; the values below are fictional
const chains: PickupChain[] = [
  { id: "sudah", name: "Express Delivery Convenience Store", color: "#e8541e" },
  { id: "neighbor", name: "Good Neighbor Convenience Store", color: "#2e7d32" },
];

const stores: PickupStore[] = [
  {
    id: "sd-001",
    chainId: "sudah",
    name: "Da'an LOHAS Store",
    address: "No. 12, LOHAS Rd., Da'an District, Taipei City",
    city: "Taipei City",
    district: "Da'an District",
    open24h: true,
    distanceText: "350 m",
  },
  {
    id: "nb-001",
    chainId: "neighbor",
    name: "Da'an Kapok Store",
    address: "No. 6, Kapok Lane, Da'an District, Taipei City",
    city: "Taipei City",
    district: "Da'an District",
    hours: "07:00–22:30",
  },
];

// Controlled: keep the store id in your checkout form state
<ConvenienceStorePickup
  chains={chains}
  stores={stores}
  value={form.pickupStoreId}
  onChange={(id) => setForm({ ...form, pickupStoreId: id })}
  onSelect={(store) => console.log("pickup store", store.name, store.address)}
/>

// Uncontrolled: you only care about the final pick
<ConvenienceStorePickup
  chains={chains}
  stores={stores}
  defaultValue="sd-001"
  onSelect={(store) => console.log(store.id)}
  compact
/>

Props

PropTypeDefaultDescription
chainsPickupChain[]—Chain list; sets the content and order of the step-one chips
storesPickupStore[]—Store list; the city/district options and the store list are all derived from it
valuestring | null—Controlled value: the currently selected store id (null means nothing selected); when omitted the component is uncontrolled
defaultValuestring | nullnullInitial store id in uncontrolled mode
onSelect(store: PickupStore) => void—Called when the user picks a store, with the full store object
onChange(storeId: string) => void—Called when the selected store id changes; update value here in controlled mode
titlestring—Panel title; a shortcut for labels.title — when both are given, title wins
labelsPartial<ConvenienceStorePickupLabels>built-in Traditional ChineseOverride the UI copy (partial overrides are fine; the rest falls back to the defaults)
isLoadingbooleanfalseShow the loading skeleton (while store data is not ready yet)
showDistancebooleantrueWhether to show each store's distanceText
compactbooleanfalseCompact mode: tighter spacing; hints, step sub-labels and the opening hours in the list are hidden — suits a checkout sidebar
classNamestring—Forwarded to the outermost container

PickupChain

FieldTypeDefaultDescription
idstring—Unique chain id, matched by PickupStore.chainId
namestring—Display name of the chain
colorstring—Brand color (any valid CSS color string) used for the swatch on chips and the summary card; neutral gray when omitted

PickupStore

FieldTypeDefaultDescription
idstring—Unique store id — this is what value holds
chainIdstring—Id of the chain the store belongs to
namestring—Store name
addressstring—Full address (shown in the list and the summary card)
citystring—City, used to derive the step-two city dropdown
districtstring—District, used to derive the step-two district dropdown
hoursstring—Opening-hours text, e.g. "07:00–23:00"
open24hboolean—Open 24 hours; when true a badge is shown and it takes precedence over hours
distanceTextstring—Distance label (e.g. "350 m" / "350 m") computed by the caller; hidden when showDistance is false

ConvenienceStorePickupLabels

FieldTypeDefaultDescription
titlestring"Convenience Store Pickup" (Convenience store pickup)Panel title
stepChainstring"Pickup Location" (Chain)Step one name
stepAreastring"Pickup Area" (Area)Step two name
stepStorestring"Pickup Store" (Store)Step three name
chainHintstring"Please select the convenience store for pickup" (Choose the chain to pick up from)Step one hint
areaHintstring"Please select county/city or township/district" (Choose the city and district)Step two hint
citystring"County/City" (City)Label of the city dropdown
districtstring"Township/District" (District)Label of the district dropdown
cityPlaceholderstring"Select County/City" (Select a city)Placeholder of the city dropdown
districtPlaceholderstring"Select Township/District" (Select a district)Placeholder of the district dropdown
nextstring"View Stores" (View stores)Button that goes to the store list once an area is chosen on step two
searchPlaceholderstring"Search store name or address" (Search by store name or address)Placeholder of the store search input
storeUnitstring"Stores" (stores)Unit appended to the store count ("N Stores")
open24hstring"24 Hours" (24 hours)Text of the 24-hour badge
emptyChainsstring"No available channels currently" (No chains available)Empty state when there are no chains
emptyAreasstring"No stores currently offering pickup for this channel" (This chain has no pickup stores)Empty state when the chain has no stores
emptyStoresstring"No matching stores found" (No matching stores)Empty state when the search/filter finds nothing
selectedstring"Selected Stores" (Selected store)Summary card heading
changestring"Change" (Change)Text of the re-select button on the summary card
loadingstring"Store data loading…" (Loading stores…)Screen-reader text for the loading skeleton

filterPickupStores(stores, { chainId, city, district, query }) is also a named export — the same filter the component uses internally (the query matches store name and address, case-insensitively), handy for counting stores at the form layer or pre-filtering on the server.

How it works

  • Three-step navigation: the step bar lets you jump back to any completed step; steps whose prerequisites are not met yet (e.g. picking a store before a chain) are disabled. Switching chains clears the chosen city/district, staying on the same chain keeps them. Choosing a district advances to the store list automatically; when you return to step two a "View stores" button takes you forward again.
  • Options derived from data: cities and districts are de-duplicated from that chain's stores, preserving the order you passed in (north to south, say) — no separate administrative-region table to maintain.
  • Collapses into a summary card: after a store is picked the stepper collapses and only the summary card remains; "Change" re-opens it straight at that store's list with the store pre-set as the active row. In controlled mode, setting value back to null from outside re-opens the panel too.
  • Distance text is up to you: the component does no geolocation or distance math, it only renders distanceText; showDistance={false} hides it entirely.
  • Motion: direction-aware horizontal slide between steps (forward slides left, back slides right), the first 8 list rows stagger in, the check mark uses a spring scale and the summary card expands by height.

Accessibility

  • The step bar is an ol list; the current step carries aria-current="step" and steps whose prerequisites are unmet are disabled
  • Chain chips are aria-pressed buttons inside a role="group"; city/district use native select elements (with label), so mobile devices open the system picker
  • The store list is a focusable role="listbox" with role="option" rows and aria-selected, pointing at the current row via aria-activedescendant; ↑/↓/Home/End move, Enter/Space selects, ↓ in the search input jumps straight into the list and Enter selects the current row (never misfires while a CJK IME is composing)
  • After a store is chosen focus moves to the summary card's "Change" button; after "Change" focus returns to the search input, so the keyboard flow never breaks; the store count is announced via aria-live="polite"
  • The isLoading skeleton announces itself with role="status", aria-busy and sr-only text
  • When the user has "reduce motion" enabled at the system level, step changes become a plain cross-fade, list rows no longer stagger, and the check mark and summary card drop their scale/height animations, leaving instant switches only