Open source @drobinetm/countries-flags

Implementation

Vue 3
vue-countries-flags

Vue 3 component for <script setup> with v-model and @change.

Try it live

Open the Vue demo page to try the component.

Open Vue demo

Styling examples

Go to the Vue styling section for Tailwind, Bootstrap, Vuetify, inline styles, and the slot map.

Open Vue styling

Installation

pnpm add @drobinetm/vue-countries-flags flag-icons
Copied

Import the CSS in your entry point:

// main.ts
import 'flag-icons/css/flag-icons.min.css'
import '@drobinetm/vue-countries-flags/styles'
Copied

Basic usage

Use native v-model to keep the selected country code reactive.

<script setup lang="ts">
import { ref } from 'vue'
import { CountriesFlags } from '@drobinetm/vue-countries-flags'

const selectedCode = ref<string | null>(null)
</script>

<template>
  <CountriesFlags
    v-model="selectedCode"
    placeholder="Select a country"
  />
  <p>Selected: {{ selectedCode }}</p>
</template>
Copied

Filter & limit

Use filter and max to narrow the list to the markets you need.

<CountriesFlags
  v-model="selectedCode"
  :filter="['es', 'us', 'br', 'ar', 'mx']"
  :max="10"
/>
Copied

@change event

The change event gives you the resolved country object in addition to the code.

import type { CountryChangeEvent } from '@drobinetm/vue-countries-flags'

function onSelect(event: CountryChangeEvent) {
  console.log(event.code)    // 'es'
  console.log(event.country) // { code: 'es', name: 'Spain' }
}

<CountriesFlags v-model="code" @change="onSelect" />
Copied

CSS integrations

In Vue, you can theme the component with utility classes, design-system helpers, or raw inline styles. The examples below are valid for Vue apps only.

Tailwind works well here, and the same slot mapping also fits a shadcn-style design system.

<CountriesFlags
  v-model="selectedCode"
  :unstyled="true"
  :class-names="{
    root: 'w-full',
    trigger:
      'flex h-11 w-full items-center justify-between rounded-xl border border-slate-300 bg-white px-3 text-left text-slate-900 shadow-sm transition focus:outline-none focus:ring-2 focus:ring-emerald-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100',
    triggerContent: 'flex min-w-0 items-center gap-2',
    placeholder: 'text-slate-400 dark:text-slate-500',
    label: 'truncate',
    list:
      'mt-2 max-h-64 overflow-auto rounded-xl border border-slate-200 bg-white p-1 shadow-2xl dark:border-slate-800 dark:bg-slate-950',
    option: 'flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm',
    optionActive: 'bg-slate-100 dark:bg-slate-900',
    optionSelected: 'bg-emerald-600 text-white',
    optionName: 'truncate',
  }"
  :styles="{
    flag: { borderRadius: '9999px' },
  }"
/>
Copied

You can apply Bootstrap utilities through classNames without wrapping the component.

<CountriesFlags
  v-model="selectedCode"
  :unstyled="true"
  :class-names="{
    root: 'w-100',
    trigger: 'btn btn-outline-secondary w-100 d-flex align-items-center justify-content-between rounded-3 px-3 py-2',
    triggerContent: 'd-flex align-items-center gap-2 text-truncate',
    placeholder: 'text-secondary',
    list: 'mt-2 list-unstyled border rounded-3 bg-body shadow-lg p-1',
    option: 'd-flex align-items-center gap-2 rounded-3 px-3 py-2',
    optionActive: 'bg-light',
    optionSelected: 'bg-primary text-white',
    optionName: 'text-truncate',
  }"
  :styles="{
    root: { minWidth: '280px' },
    flag: { borderRadius: '50%' },
  }"
/>
Copied

The component is not a native Vuetify field, but you can align it with Vuetify spacing and surfaces through slot classes.

<CountriesFlags
  v-model="selectedCode"
  :unstyled="true"
  :class-names="{
    root: 'w-100',
    trigger: 'd-flex align-center justify-space-between w-100 px-4 py-3 rounded-lg border',
    triggerContent: 'd-flex align-center ga-2 min-w-0',
    placeholder: 'text-medium-emphasis',
    list: 'mt-2 pa-2 rounded-lg border bg-surface elevation-8',
    option: 'd-flex align-center ga-2 rounded-lg px-3 py-2',
    optionActive: 'bg-grey-lighten-4',
    optionSelected: 'bg-primary text-white',
  }"
  :styles="{
    flag: { borderRadius: '9999px' },
  }"
/>
Copied

If your styles come from runtime values, bind them with the styles prop.

<CountriesFlags
  v-model="selectedCode"
  :unstyled="true"
  :styles="{
    root: { width: '100%', minWidth: '280px' },
    trigger: {
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'space-between',
      width: '100%',
      minHeight: '44px',
      padding: '0 14px',
      border: '1px solid #d4d4d8',
      borderRadius: '16px',
      backgroundColor: '#ffffff',
      color: '#111827',
    },
    triggerContent: { display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 },
    placeholder: { color: '#6b7280' },
    list: {
      marginTop: '8px',
      padding: '6px',
      listStyle: 'none',
      border: '1px solid #e5e7eb',
      borderRadius: '16px',
      backgroundColor: '#ffffff',
      boxShadow: '0 24px 48px rgba(15, 23, 42, 0.18)',
    },
    option: { display: 'flex', alignItems: 'center', gap: '8px', padding: '10px 12px', borderRadius: '12px' },
    optionActive: { backgroundColor: '#f3f4f6' },
    optionSelected: { backgroundColor: '#059669', color: '#ffffff' },
    flag: { borderRadius: '9999px' },
  }"
/>
Copied

Props

PropTypeDefaultDescription
modelValue (v-model)string | nullnullSelected country code.
maxnumber0Max countries shown.
filterstring[][]ISO alpha-2 whitelist.
placeholderstring"Select a country"Placeholder text.
disabledbooleanfalseDisables the component.
unstyledbooleanfalseTurns off the bundled UI styles.
classNamesCountriesFlagsClassNames{}Classes by component slot.
stylesCountriesFlagsStyles{}Inline styles by component slot.

Emits

EventPayloadDescription
update:modelValuestring | nullv-model binding update.
changeCountryChangeEventFull { country, code } event.