react-thaizip
v0.3.9
Published
CLI scaffold tool for adding thaizip React address autocomplete components.
Maintainers
Readme
react-thaizip
A CLI that scaffolds ready-to-use Thai address React components — powered by thaizip — directly into your project, shadcn/ui-style.
📚 Documentation and live component demos
Using a coding agent? Start with the AI usage guide or provide it the site's llms.txt.
| Thai input | English input (romanization alias) |
|---|---|
|
|
|
Quick start
npx react-thaizip init
npx react-thaizip add autocompleteinitdetects your project structure, package manager, and Tailwind version, then writes athaizip.config.jsonused byadd.add autocompletescaffolds the component plus its shared dependencies (lib/utils.tsandhooks/use-thai-address-index.ts). It installs missing packages:thaizip,clsx, andtailwind-merge, plus@base-ui/reactfor the vanilla style. In supported shadcn/ui projects it uses the project's Base, Radix, or React Aria primitives instead.add address-form-fieldadditionally installsreact-hook-form.
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'
<ThaiAddressAutocomplete
name="address"
onValueChange={(address) => console.log(address)}
/>Prerequisite: your project needs Tailwind CSS (v3 or v4) already installed.
initdetects it but doesn't install it for you — if none is found, it prints a pointer to the Tailwind install docs and exits.
Components
autocomplete(ThaiAddressAutocomplete) — free-text address search. The vanilla variant uses Base UI'sCombobox.cascade-select(ThaiAddressCascadeSelect) — province > district > sub-district cascade. The vanilla variant uses Base UI'sSelect(×3).address-form(ThaiAddressForm) — house number + optional moo/soi/street free text, layered directly on top ofThaiAddressCascadeSelect(embedded by file, not by npm import).address-display(ThaiAddressDisplay) — read-only address renderer; noonValueChange/name/hidden inputs, just formats aThaiAddressDisplayValueyou hand it.address-form-field(ThaiAddressFormField) — react-hook-formControllerwrapper aroundThaiAddressCascadeSelect.
npx react-thaizip add # interactive multiselect
npx react-thaizip add autocomplete # or: ThaiAddressAutocomplete
npx react-thaizip add cascade-select # or: ThaiAddressCascadeSelect
npx react-thaizip add address-form # or: ThaiAddressForm
npx react-thaizip add address-display # or: ThaiAddressDisplay
npx react-thaizip add address-form-field # or: ThaiAddressFormField
npx react-thaizip add autocomplete cascade-select # multiple at onceEach add also requires thaizip >= 0.7.5 (which includes the cascade/enumeration, bilingual-label, and synchronous index-cache APIs used by the templates) — an older installed version makes add exit without writing files.
init selects a component style from the project's components.json: base-*, radix-*, and aria-* choose the corresponding shadcn/ui variant; legacy default and new-york choose Radix. Other or absent styles use the vanilla Base UI variant. The choice is recorded in thaizip.config.json v4. See the configuration reference for all fields and migration behavior.
autocomplete, cascade-select, and address-form share the same shape:
- Controlled or uncontrolled
value/defaultValue/onValueChange(ResolvedThaiAddress | null—FullThaiAddress | nullforaddress-form) name— renders 4 hidden inputs for plain<form>submission:${name}-subdistrict,-district,-province,-zipcode(address-formrenders 4 more of its own on top of the cascade's 4)locale('th'default or'en') and atextsprop to override any label/status messagedisabled/required/onBlur/onError, plus per-partclassNameslots- A forwarded
ref
address-display is read-only: it takes value / locale / mode, with no onValueChange, name, or hidden inputs.
address-form-field takes react-hook-form's own control / name / rules instead of value / onValueChange, and renders no hidden inputs — submission goes through react-hook-form's own handleSubmit, which reads the form state directly.
Interactive components load the address index on mount by default. Set indexLoad="visible" on autocomplete, cascade select, address form, or address form field when the field starts below the viewport; loading begins when it enters view. Browsers without IntersectionObserver use mount loading.
ThaiAddressAutocomplete
| Prop | Purpose |
|---|---|
| value / defaultValue / onValueChange | Controlled or uncontrolled ResolvedThaiAddress \| null selection |
| name | When set, renders 4 hidden inputs: ${name}-subdistrict, -district, -province, -zipcode |
| locale | 'th' (default) or 'en' — drives suggestion labels and default texts |
| texts | Partial<Texts> — override any subset of the default labels/status messages |
| indexLoad | 'mount' (default) or 'visible' — when to begin loading the address index |
| limit / debounce / threshold | Passed through to the underlying search hook |
| disabled / required / onBlur / onError | Standard field wiring; onError fires if the bundled address index fails to load |
| className / inputClassName / popupClassName / itemClassName | Class-name slots for the wrapper, input, popup, and each suggestion item |
| ref | Forwarded to the underlying <input> |
ThaiAddressCascadeSelect
| Prop | Purpose |
|---|---|
| value / defaultValue / onValueChange | Controlled or uncontrolled ResolvedThaiAddress \| null selection. Changing a parent select in a way that invalidates a full selection fires onValueChange(null) and resets the downstream selects |
| name | When set, renders 4 hidden inputs: ${name}-subdistrict, -district, -province, -zipcode |
| locale | 'th' (default) or 'en' — drives option labels and default texts |
| texts | Partial<Texts> — override any subset of the default labels/status messages |
| indexLoad | 'mount' (default) or 'visible' — when to begin loading the address index |
| disabled / required / onBlur / onError / aria-invalid | Standard field wiring; onError fires if the bundled address index fails to load |
| className / labelClassName / triggerClassName / popupClassName / itemClassName | Class-name slots for the wrapper, labels, each select trigger, popup, and each option item |
| ref | Forwarded to the province select's trigger button |
ThaiAddressForm
| Prop | Purpose |
|---|---|
| value / defaultValue / onValueChange | Controlled or uncontrolled FullThaiAddress \| null — a ResolvedThaiAddress plus houseNo (required) and optional moo/soi/street. Fires only when houseNo is non-blank and the embedded cascade has a full resolution; otherwise the value is null |
| name | When set, forwarded to the embedded ThaiAddressCascadeSelect (its own 4 hidden inputs) plus renders 4 more of its own: ${name}-houseno, -moo, -soi, -street |
| locale | 'th' (default) or 'en' — drives this component's own labels and the embedded cascade's |
| texts | Partial<ThaiAddressFormTexts> — override the house-number/moo/soi/street labels and placeholders |
| cascadeTexts | Partial<Texts> forwarded to the embedded ThaiAddressCascadeSelect's own texts prop |
| indexLoad | Forwarded to the embedded cascade ('mount' or 'visible') |
| disabled / required / onBlur / onError / aria-invalid | Standard field wiring; onBlur fires on the house-number input specifically; onError fires if the bundled address index fails to load (delegated to the embedded cascade); aria-invalid marks the 4 text inputs and the embedded cascade's triggers invalid |
| className / labelClassName / inputClassName / triggerClassName / popupClassName / itemClassName | Class-name slots for the wrapper, labels, the 4 text inputs, and the embedded cascade's own trigger/popup/item slots |
| ref | Forwarded to the house-number <input> |
ThaiAddressDisplay
| Prop | Purpose |
|---|---|
| value | ThaiAddressDisplayValue \| null — a ResolvedThaiAddress plus optional houseNo/moo/soi/street. null renders emptyText |
| locale | 'th' (default) or 'en' — drives the locality label and the default emptyText |
| mode | 'single-line' (default, street + locality joined with ', ') or 'multi-line' (two <span> rows) |
| emptyText | Overrides the default "no address" text shown when value is null |
| className | Class name for the root <address> element |
| lineClassName | Class name for each row's <span> in 'multi-line' mode |
| ref | Forwarded to the root <address> element |
ThaiAddressFormField
| Prop | Purpose |
|---|---|
| control | react-hook-form's Control object, from useForm() |
| name | The field path in your form values, e.g. "address" |
| rules | react-hook-form validation rules, e.g. { required: 'Please select an address' } — forwarded to Controller |
| locale | 'th' (default) or 'en' — drives the embedded cascade's option labels and default texts |
| texts | Partial<Texts> forwarded to the embedded ThaiAddressCascadeSelect's own texts prop |
| indexLoad | Forwarded to the embedded cascade ('mount' or 'visible') |
| disabled | Forwarded to the embedded cascade's triggers |
| className / labelClassName / triggerClassName / popupClassName / itemClassName | Class-name slots for the wrapper, and the embedded cascade's own label/trigger/popup/item slots |
| errorClassName | Class name for the role="alert" validation message shown when rules fails |
Customizing labels (texts prop)
Every generated component ships with Thai default labels (locale defaults to 'th'); pass locale="en" to switch to the built-in English set. The optional texts prop overrides any subset of the active set:
<ThaiAddressCascadeSelect
locale="en"
texts={{
provinceLabel: 'Province',
districtLabel: 'District',
subdistrictLabel: 'Sub-district',
zipLabel: 'Postal code',
}}
/>texts is Partial<Texts>, so you only need to supply the keys you want to change — anything you omit falls back to the locale's default. Each component exports its own Texts type covering its labels, placeholders, and status messages (e.g. loadingText, errorText).
CLI flags
npx react-thaizip init [--yes]
npx react-thaizip add [component...] [--yes] [--overwrite]
npx react-thaizip --help
npx react-thaizip --version--yes,-y— skip confirmation prompts (initandadd)--overwrite— overwrite existing files without prompting (addonly), including<libDir>/utils.tsand<hooksDir>/use-thai-address-index.ts, which are otherwise never touched once they exist--help,-h— print usage and the list of available components--version,-v— print the CLI's own versioninit --help/add --help— print command-scoped usage (add --helpincludes the component list)
Contributing: deploying the documentation site
Deploy apps/docs as a separate Vercel project. Set its Root Directory to apps/docs and use Node.js 22 or newer. The docs app imports the component templates from templates/react/ts/ directly, and those imports (thaizip, clsx, tailwind-merge, @base-ui/react) resolve against this repo's own node_modules, not just apps/docs/node_modules — apps/docs/vercel.json overrides the Install Command to install both. Keep Vercel's default npm run build command. The docs app owns its own package-lock.json and its build uses Webpack for compatibility with the Fumadocs MDX pipeline.
