# `@alikhalilll/a-tel-input`
> A headless, shadcn-vue style **international telephone input** for Vue 3 / Nuxt 3+.
> Country auto-detect · libphonenumber-js validation · responsive picker (popover ⇆ drawer) ·
> RTL & i18n ready · first-class VeeValidate + Zod integration · server-side validation hook.
Headless picker — popover on desktop, bottom-sheet on mobile.
[](https://www.npmjs.com/package/@alikhalilll/a-tel-input)
[](./LICENSE)
[](https://www.npmjs.com/package/@alikhalilll/a-tel-input)
## Setup
### Nuxt 3 / 4
```bash
pnpm add @alikhalilll/a-tel-input
# npm install @alikhalilll/a-tel-input
# yarn add @alikhalilll/a-tel-input
# bun add @alikhalilll/a-tel-input
```
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@alikhalilll/a-tel-input/nuxt'],
css: ['@alikhalilll/a-tel-input/styles.css'],
});
```
`ATelInput`, `ACountrySelect`, and `ACountryFlag` are auto-imported — no `import` statement needed in your `.vue` files.
### Vue + Vite
```bash
pnpm add @alikhalilll/a-tel-input
```
```ts
// main.ts
import '@alikhalilll/a-tel-input/styles.css';
```
Optional auto-resolve via `unplugin-vue-components`:
```ts
// vite.config.ts
import Components from 'unplugin-vue-components/vite';
import { ATelInputResolver } from '@alikhalilll/a-tel-input/resolver';
export default { plugins: [Components({ resolvers: [ATelInputResolver()] })] };
```
### REST Countries v5 (optional)
The component renders with a synchronous, offline country list built from `libphonenumber-js` + `Intl.DisplayNames` — no network, no auth. Pass `restCountriesApiKey` to opt into the v5 fetch (one request per browser, cached in `localStorage` for 30 days):
```ts
// nuxt.config.ts — applies app-wide
export default defineNuxtConfig({
modules: ['@alikhalilll/a-tel-input/nuxt'],
aTelInput: { apiKey: 'rc_live_...' },
});
```
```vue
```
```ts
// Vue + Vite — install once at bootstrap
import { installTelInputDefaults } from '@alikhalilll/a-tel-input';
installTelInputDefaults(app, { apiKey: 'rc_live_...' });
```
Per-component props win over the injected default. Any failure (CORS, network, non-2xx) silently falls back to the offline baseline — never an empty dropdown. CORS requires you to allowlist your origin's hostnames on the REST Countries dashboard.
---
## Why this component
- **Universal country detection** — debounced parse against the **full libphonenumber
metadata (~250 countries)**. Works with international format (`+201066105963`) AND
local format (`01066105963`), with NANP disambiguation and a hint-priority chain
(env → current → recents → popular → all). No "only the popular countries" caveats.
- **Validates and formats** — error reasons, format hint, E.164 output, every keystroke.
- **Responsive surface** — popover on desktop, bottom-sheet drawer on mobile, sticky-safe
scroll lock on **both**. The page underneath never scrolls; the inner picker list does.
- **Headless slots for every region** — trigger, chevron, flag, item, search, hint,
error. Restyle the field down to the pixel without forking the logic.
- **First-class form-library integration** — controlled `error` prop, `@blur` event,
`useTelField()` composable for VeeValidate, `zPhone()` factory for Zod schemas, and a
`validating` spinner for async server-side checks ("is this number already registered?").
- **Two binding contracts, your pick** — single default `v-model` (E.164 string, drops
into VeeValidate's `` via `v-bind="field"`), or split
`v-model:phone` + `v-model:country`. Both stay in sync.
- **i18n + RTL out of the box** — country names localised via `Intl.DisplayNames`,
alternative numerals (Arabic-Indic, Persian, Devanagari, Bengali) folded to ASCII on
input, RTL inherited from the page or forced via `dir`.
- **Efficient by default** — country list built synchronously from
`libphonenumber-js` metadata + `Intl.DisplayNames` — **zero network requests**.
Lookup indexes are populated at first call, so country detection (`+20`, `+44`,
ambiguous `+1` NANP, etc.) works from first paint. IP geolocation request still
deduped to one call per page across every `` / `` /
`useTelField()` / `zPhone()` instance. LRU-cached matcher.
- **Optional REST Countries v5 upgrade** — pass `restCountriesApiKey` (or
configure it via the Nuxt module / `installTelInputDefaults`) to fire one
`api.restcountries.com/countries/v5` request per browser; the result is cached
in `localStorage` for 30 days. Without a key the picker stays on the offline
baseline — no behaviour change required.
- **SSR-safe** — country detection runs only after mount, hydration is clean.
- **TypeScript-first** — every prop, slot, and event fully typed; web-types ship for
JetBrains IDEs.
---
## Table of contents
- [Setup](#setup)
- [Quick start](#quick-start)
- [Form integration](#form-integration)
- [VeeValidate + Zod](#veevalidate--zod)
- [Server-side validation](#server-side-validation-is-this-phone-already-registered)
- [Native HTML forms](#native-html-forms)
- [API reference](#api-reference)
- [Props](#props)
- [Events](#events)
- [Slots](#slots)
- [Exposed methods](#exposed-methods)
- [Composables](#composables)
- [Theming](#theming)
- [Accessibility](#accessibility)
- [SSR](#ssr)
- [TypeScript](#typescript)
- [Browser support](#browser-support)
- [Troubleshooting](#troubleshooting)
- [License](#license)
---
## Quick start
The component supports **two binding contracts** — pick whichever fits your form code:
### Single v-model (E.164 string)
The friendliest with VeeValidate's ``, native HTML `