# vue-tel-input: the country dropdown Vue forms keep rewriting

> vue-tel-input is a Vue 3 phone number input that binds a country picker and a formatted number to a single v-model. It ships as an npm plugin, a locally importable component, or a script tag, but it also asks the host app to supply Vue and libphonenumber-js itself.

**iamstevendao/vue-tel-input** — International Telephone Input with Vue

- Repository: https://github.com/iamstevendao/vue-tel-input
- Website: https://iamstevendao.com/vue-tel-input/
- Stars: 910 · Forks: 363
- Language: CSS
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/iamstevendao-vue-tel-input

## The form problem: one field, two pieces of data

A phone number in a signup form is never a plain string. It is a country plus a national number, and the two parts follow different rules: dialing prefixes, national trunk codes and national digit lengths all change from one country to the next. Teams that build forms from scratch usually end up with a country dropdown bolted next to a text input, plus a pile of validation code that nobody wants to own. vue-tel-input exists to collapse that arrangement into a single control. It is described as International Telephone Input with Vue, and the repository carries the topic tags input, international-telephone, phone-input and vue-tel. The audience is therefore Vue developers assembling a contact or checkout form, not teams looking for a general validation library. What the component does not do, at least as far as the repository layout shows, is replace a form library or a schema validator. It is a widget for one field, with the country context attached to it. If the rest of your form is a bespoke mix of input handlers, the widget removes a piece of work but does not set a convention for the rest of the form.

## npm install, register the plugin, import the stylesheet

The documented path starts with the npm package. After that you register the plugin on the Vue app and you import the stylesheet separately, which is worth noticing because a missing CSS import produces an unstyled dropdown rather than an error. The installation section shows a global options object passed as the second argument to app.use, and that object carries a mode of 'auto'.

```bash
  npm install vue-tel-input
```

The full registration snippet below shows the import order, the global options object, and the mount call. A comment in the code notes that the global options are optional.

```javascript
  import { createApp } from 'vue';
  import App from './App.vue';
  import VueTelInput from 'vue-tel-input';
  import 'vue-tel-input/vue-tel-input.css';

  const globalOptions = {
    mode: 'auto',
  };

  const app = createApp(App);
  app.use(VueTelInput, globalOptions); // Define default global options here (optional)
  app.mount('#app');
```

With the plugin registered, the component itself is a single element bound to a model. The value lives in your component, the control lives in the library.

```html
  <template>
    <vue-tel-input v-model="phone"></vue-tel-input>
  </template>
```

## Three registration paths, and they do not behave identically

There are three documented ways to get the component into a page, and the differences matter when you are debugging a blank dropdown. The npm plugin path registers the component globally, so any template in the app can use it. The direct import path registers it locally through the components option, which keeps the dependency explicit and is the route that plays well with tree shaking. The browser path avoids a build step entirely by loading two files from unpkg. That last path has a conditional behaviour that is easy to miss: if Vue is detected in the page, the plugin is installed automatically, and otherwise it has to be installed by hand with a call on the window object. Auto-detection means a script tag build can fail silently when the page loads Vue after the library, since the detection then finds nothing.

```html
<script src="https://unpkg.com/vue-tel-input"></script>
<link rel="stylesheet" href="https://unpkg.com/vue-tel-input/dist/vue-tel-input.css" />
```

The manual fallback for the script tag route is a single line, and it is worth keeping in your notes if you ever move away from a bundler.

```js
  app.use(window['vue-tel-input']);
```

## The v-model contract, and a documentation bug worth knowing about

The component is driven by v-model, so the parent's ref receives the number the user assembled. The mode attribute changes how the number is presented, and the global options object sets a default of 'auto' for every instance in the app, while a per component mode attribute overrides it for one field. The direct import example contains a mistake. Its setup function declares a ref called phone and returns an object containing a key called value. The template in the same example binds v-model to phone. A reader copying that snippet verbatim ends up with a template reference the component does not return, and a returned key that no template consumes. The binding in the shorter global registration example is consistent, so the fix is obvious once you see it, but the mismatch is a reminder to read the returned object carefully rather than paste it.

```javascript
      setup() {
        const phone = ref(null);

        return {
          value,
        };
      },
```

## Peer dependencies put the parser and the framework in your hands

The package manifest declares two peer dependencies and no regular dependencies, and both entries tell you where responsibility sits. libphonenumber-js is pinned to a caret range on the 1.12 line, and Vue itself is required at ^3.5.4. A bare install of the package therefore does not leave a working number library in your node_modules unless something else in the project already pulls it in. The phone parsing and formatting behaviour you see in the component comes from a library that your application is responsible for keeping present and in range. In practice that is good news for bundle size, because the parser becomes something you can share across fields, and it is bad news for reproducibility if the version that lands on your machine differs from the one the author tested against. The manifest also declares a type of module, an exports map with separate import, require and types conditions, and a files entry limited to dist, so only the built output is published.

```json
  "peerDependencies": {
    "libphonenumber-js": "^1.12.12",
    "vue": "^3.5.4"
  },
```

## Two hundred kilobytes of JavaScript and one hundred of CSS

The project is unusually candid about weight. The library is described as about 200kb of JavaScript and 100kb of CSS, and the recommendation is to split it out of the main bundle so that it only loads when the user reaches the page that needs a phone field. The recipe uses a dynamic import inside a Promise.all and relies on a webpackChunkName comment to name the chunk, and the project says the technique works in modern bundlers such as Webpack and Rollup. There is a second detail in that recipe that shows the author has thought about bundler internals: the component CSS is imported inside the same Promise.all rather than through a Vue single file component style block, because the style block would pull the stylesheet into the main or vendors bundle and defeat the point. This is a real trade-off in the design, not a free win. Lazy loading a form control costs you a loading state and a chunk boundary, and on a single page checkout where the phone field is always visible, the split buys little while the complexity stays.

## The Vue 2 line lives under a separate package tag

Projects that support several major versions of a framework often do it in a way that quietly doubles the support surface. Here the split is explicit and cheap to read: Vue 2 users are pointed to a package tagged vue-tel-input@legacy and to a separate legacy guide on the project site. Nothing about the mainline package targets Vue 2, and the peer dependency on Vue 3.5.4 makes that clear. If you are still on Vue 2, this is not a drop-in for the same component. You are choosing a different distribution with its own guide and its own release cadence, and the repository does not say how closely the two lines track each other. Teams migrating from the legacy tag to the mainline also get a different import style, since the mainline package exposes a named VueTelInput export alongside the default plugin export.

## What the repository layout says about the build, and what it leaves undocumented

The top level of the repository holds src, docs, a VitePress setup, a Vite config, TypeScript configuration and an eslint config. The manifest shows how the pieces fit together: the dev and docs scripts both point at vitepress dev docs, the test script runs vitest with a UI, and the build script runs vue-tsc before vite build, with a postbuild step that copies a hand written type declaration over the generated one. A 200kb library with a type check in the build and a test runner in the toolchain is a maintained project, and the release history is consistent with that, with v9.9.0 published on 2026-09-25 and earlier entries for v9.8.0 and v9.7.1. The last push was on 2026-10-21. The gaps are on the documentation side. The prop list is not in the repository, which points to a props page on the project site, and the same site hosts the demo, the guide and the legacy notes. The README does not document a validation error event, does not describe how a country list can be restricted, and says nothing about server side rendering. A project whose public contract lives on a separately deployed site is a project whose API you should read before you depend on it. If that site is unreachable, you are left with a component and a v-model. The licence is MIT, with copyright held by Steven Dao, so there is no commercial restriction on use in a proprietary application.

## Conclusion

Choose vue-tel-input if your form needs a country-aware number field inside a Vue 3 app and you can live with peer dependencies you install yourself. Skip it if you need SSR-safe rendering, a documented validation error contract, or country rules that the site rather than the repository describes, because those details are not settled here. Before you adopt it, verify that libphonenumber-js and Vue 3.5.4 are already in your dependency tree, and read the props page referenced from the repository, since the prop surface is not described there.

## FAQ

### Does vue-tel-input work with Vue 3 only?

The mainline package lists vue ^3.5.4 as a peer dependency, so it targets Vue 3. Vue 2 users are pointed to a separate package tag, vue-tel-input@legacy, with its own guide on the project site.

### Do I need to install libphonenumber-js myself to use vue-tel-input?

It is declared as a peer dependency at ^1.12.12 rather than a regular dependency, so your application is the one that has to provide a compatible version. The documented install steps do not mention adding it separately.

### How can I use vue-tel-input without installing it as a Vue plugin?

Import the component into the components option of a local component and import 'vue-tel-input/vue-tel-input.css' alongside it, then bind it with v-model. You can also load both files from unpkg with a script tag and a stylesheet link.

### Is vue-tel-input too heavy to include on every page?

The library is described as about 200kb of JavaScript and 100kb of CSS, and there is a lazy load example that imports the component and its stylesheet dynamically so they are downloaded only when needed.

## Sources

- [iamstevendao/vue-tel-input on GitHub](https://github.com/iamstevendao/vue-tel-input)
- [License: MIT](https://github.com/iamstevendao/vue-tel-input/blob/main/LICENSE)
- [Project website](https://iamstevendao.com/vue-tel-input/)
- [README](https://github.com/iamstevendao/vue-tel-input/blob/main/README.md)
- [Releases](https://github.com/iamstevendao/vue-tel-input/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/iamstevendao-vue-tel-input
