WebberUI

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

Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.

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.

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

Playground

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

<ConvenienceStorePickup />

Installation

npx shadcn@latest add "https://webberui.com/r/convenience-store-pickup.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: "速達超商", color: "#e8541e" },
  { id: "neighbor", name: "好鄰居便利店", color: "#2e7d32" },
];

const stores: PickupStore[] = [
  {
    id: "sd-001",
    chainId: "sudah",
    name: "大安樂活門市",
    address: "臺北市大安區樂活路 12 號",
    city: "臺北市",
    district: "大安區",
    open24h: true,
    distanceText: "350 公尺",
  },
  {
    id: "nb-001",
    chainId: "neighbor",
    name: "大安木棉店",
    address: "臺北市大安區木棉巷 6 號",
    city: "臺北市",
    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 | nullControlled 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) => voidCalled when the user picks a store, with the full store object
onChange(storeId: string) => voidCalled when the selected store id changes; update value here in controlled mode
titlestringPanel 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
classNamestringForwarded to the outermost container

PickupChain

FieldTypeDefaultDescription
idstringUnique chain id, matched by PickupStore.chainId
namestringDisplay name of the chain
colorstringBrand color (any valid CSS color string) used for the swatch on chips and the summary card; neutral gray when omitted

PickupStore

FieldTypeDefaultDescription
idstringUnique store id — this is what value holds
chainIdstringId of the chain the store belongs to
namestringStore name
addressstringFull address (shown in the list and the summary card)
citystringCity, used to derive the step-two city dropdown
districtstringDistrict, used to derive the step-two district dropdown
hoursstringOpening-hours text, e.g. "07:00–23:00"
open24hbooleanOpen 24 hours; when true a badge is shown and it takes precedence over hours
distanceTextstringDistance label (e.g. "350 公尺" / "350 m") computed by the caller; hidden when showDistance is false

ConvenienceStorePickupLabels

FieldTypeDefaultDescription
titlestring"超商取貨" (Convenience store pickup)Panel title
stepChainstring"取貨通路" (Chain)Step one name
stepAreastring"取貨地區" (Area)Step two name
stepStorestring"取貨門市" (Store)Step three name
chainHintstring"請選擇要取貨的超商通路" (Choose the chain to pick up from)Step one hint
areaHintstring"請選擇縣市與鄉鎮市區" (Choose the city and district)Step two hint
citystring"縣市" (City)Label of the city dropdown
districtstring"鄉鎮市區" (District)Label of the district dropdown
cityPlaceholderstring"選擇縣市" (Select a city)Placeholder of the city dropdown
districtPlaceholderstring"選擇鄉鎮市區" (Select a district)Placeholder of the district dropdown
nextstring"查看門市" (View stores)Button that goes to the store list once an area is chosen on step two
searchPlaceholderstring"搜尋門市名稱或地址" (Search by store name or address)Placeholder of the store search input
storeUnitstring"間門市" (stores)Unit appended to the store count ("N 間門市")
open24hstring"24 小時" (24 hours)Text of the 24-hour badge
emptyChainsstring"目前沒有可選的通路" (No chains available)Empty state when there are no chains
emptyAreasstring"此通路目前沒有可取貨的門市" (This chain has no pickup stores)Empty state when the chain has no stores
emptyStoresstring"找不到符合的門市" (No matching stores)Empty state when the search/filter finds nothing
selectedstring"已選門市" (Selected store)Summary card heading
changestring"更換" (Change)Text of the re-select button on the summary card
loadingstring"門市資料載入中…" (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

On this page