Skip to content
Devix Open Source

Customize

Styling and theming

Phone Input is built to disappear into your design system. There are four levels, and you can mix them:

  1. CSS variables. Colours, sizes, radii and weights, a handful of lines per theme.
  2. classNames. Your own classes on every part, for Tailwind, Bootstrap or utility CSS.
  3. Plain CSS. Every part has a stable dxp-* class and uses ARIA state attributes.
  4. Your own UI. The DOM-free core (@devix-labs/phone-input/core) gives you parsing, formatting, validation and country search without any markup.

Stylesheets

File Use it when
styles.css / phone-input.min.css The default: plain CSS that works on any page, even with old global resets
phone-input.layer.css Your app uses cascade layers (Tailwind v4, modern design systems). Everything sits in @layer devix, so any rule you write wins without !important
/* Tailwind v4 */
@import 'tailwindcss';
@import '@devix-labs/phone-input/styles.layer.css';

Variables

Set them on .dxp (the field) and .dxp-panel (the dropdown). The panel is rendered in the browser's top layer, so it doesn't inherit variables from your wrapper; target both.

.dxp,
.dxp-panel {
  --dxp-accent: #635bff;
  --dxp-radius: 6px;
  --dxp-height: 40px;
}

Size and shape

Variable Default
--dxp-height 36px Field height
--dxp-radius 8px Field corners
--dxp-font-size 14px Field text
--dxp-flag-width 20px Flag on the country button and in the list
--dxp-panel-font-size 13px Dropdown text
--dxp-panel-radius 10px Dropdown corners; inner corners follow
--dxp-panel-max-height 340px Tallest the dropdown gets before scrolling
--dxp-option-height 32px Row height in the country list
--dxp-font-weight-selected 500 Selected country

Colour (light defaults, then dark)

Variable Light Dark
--dxp-bg #fff #111114
--dxp-text #18181b #ededf0
--dxp-muted #71717a #9b9ba6
--dxp-border #e4e4e7 #2c2c33
--dxp-border-hover #c9c9d0 #3d3d46
--dxp-accent #ea4b71 same
--dxp-ring accent at 28% same
--dxp-danger #d92d45 same
--dxp-panel-bg #fff #18181c
--dxp-panel-shadow soft shadow deeper shadow
--dxp-option-active #f4f4f5 #232329
--dxp-option-selected accent at 10% same
--dxp-divider #ececf1 #26262c

Dark mode

With theme: 'auto' (the default), the widget looks at the page it sits in, in this order:

  1. The nearest theme marker on an ancestor: .dark / .light (Tailwind, shadcn/ui), data-theme="dark", data-bs-theme="dark" (Bootstrap 5.3), data-mode or data-color-scheme.
  2. Otherwise, the real background colour behind the field, including oklch() backgrounds.

It re-checks when those attributes change on <html> or <body>, when the OS setting changes, and each time the dropdown opens, so a theme toggle updates open and closed widgets alike.

Force a theme with theme: 'light' or theme: 'dark', and give dark mode your own colours:

.dxp[data-theme='dark'],
.dxp-panel[data-theme='dark'] {
  --dxp-bg: #0b1220;
  --dxp-panel-bg: #0f172a;
  --dxp-border: #1e293b;
}

Matching popular design systems

shadcn/ui, whose tokens already hold complete colours:

.dxp, .dxp-panel {
  --dxp-bg: var(--background);
  --dxp-panel-bg: var(--popover);
  --dxp-text: var(--foreground);
  --dxp-muted: var(--muted-foreground);
  --dxp-border: var(--input);
  --dxp-accent: var(--ring);
  --dxp-option-active: var(--accent);
  --dxp-option-selected: var(--accent);
  --dxp-radius: var(--radius);
}

Bootstrap 5.3:

.dxp, .dxp-panel {
  --dxp-bg: var(--bs-body-bg);
  --dxp-panel-bg: var(--bs-body-bg);
  --dxp-text: var(--bs-body-color);
  --dxp-muted: var(--bs-secondary-color);
  --dxp-border: var(--bs-border-color);
  --dxp-accent: var(--bs-primary);
  --dxp-ring: rgba(var(--bs-primary-rgb), 0.25);
  --dxp-radius: var(--bs-border-radius);
  --dxp-height: calc(1.5em + 0.75rem + 2px);
  --dxp-font-size: 1rem;
}

Material 3:

.dxp, .dxp-panel {
  --dxp-accent: var(--md-sys-color-primary);
  --dxp-bg: var(--md-sys-color-surface);
  --dxp-panel-bg: var(--md-sys-color-surface-container);
  --dxp-text: var(--md-sys-color-on-surface);
  --dxp-border: var(--md-sys-color-outline);
  --dxp-radius: 4px;
  --dxp-height: 56px;
}

Tailwind classes on every part

createPhoneInput(input, {
  classNames: {
    root: 'rounded-lg border-zinc-300 shadow-sm dark:border-zinc-700',
    button: 'hover:bg-zinc-50 dark:hover:bg-zinc-800',
    input: 'placeholder:text-zinc-400',
    panel: 'rounded-xl shadow-xl ring-1 ring-black/5',
    search: 'rounded-md',
    option: 'rounded-md',
  },
});

Parts you can target: root, button, flag, dial, input, panel, search, list, option and empty.

Classes and states

Selector What it is
.dxp The field: country button + input
.dxp--open, .dxp--inline Dropdown open; dial code kept inside the text
.dxp-country, .dxp-flag, .dxp-dial, .dxp-chevron The country button and its parts
.dxp-input Your input
.dxp-panel, .dxp-panel--sheet The dropdown; the phone bottom sheet
.dxp-search, .dxp-list, .dxp-option, .dxp-name, .dxp-code, .dxp-divider, .dxp-empty Inside the dropdown
.dxp-option.is-active Keyboard or pointer highlight
.dxp-option[aria-selected='true'] The current country
.dxp-input[aria-invalid='true'] Invalid number (after validateOn)
[data-theme='light' | 'dark'] The resolved theme, on .dxp and .dxp-panel

Icons, flags and text

  • icons: { chevron: '<svg>…</svg>' } swaps the dropdown arrow.
  • flags: 'svg' | 'emoji' | 'none'. The SVG flags take the Flag Icons variables too.
  • strings and messages change every word, including the validation messages.
  • Fonts are inherited from your page. Nothing is loaded.

Density

The defaults are compact, sized for dashboards and forms. For a roomier consumer layout:

.dxp, .dxp-panel {
  --dxp-height: 44px;
  --dxp-font-size: 16px;
  --dxp-panel-font-size: 14px;
  --dxp-option-height: 40px;
  --dxp-flag-width: 22px;
}

On touch phones the dropdown becomes a bottom sheet with 44px rows. Use sheet: false to keep the dropdown, or sheet: true to always use the sheet.

Keeping your own input (overlay layout)

Sometimes the field is not yours to restyle: a WooCommerce checkout, a Contact Form 7 form, a theme with its own inputs, or a design system where every field is already correct. layout: 'overlay' leaves the input exactly where it is — same parent, same classes, same borders, same focus ring — and floats the country button inside it:

createPhoneInput(document.querySelector('#billing_phone'), { layout: 'overlay' });

What changes and what does not:

  • The input is never moved in the DOM, and its inline styles are restored on destroy().
  • It gains one class, dxp-overlay-input, as a hook for your own CSS. It does not get dxp-input, so none of the widget's field styling applies to it.
  • Its padding-inline-start is grown to clear the button, measured from the button's real width, and the widget re-measures on resize, focus and pointer entry.
  • The button's position is written inline, so a host stylesheet that resets every element inside a form cannot unstick it.

Style the button like the rest of your field:

.dxp--overlay .dxp-country {
    padding-inline: 12px 8px;
    border-inline-end: 1px solid var(--my-field-border);
}

Two things to check in your own layout. A floating label (<label> over the input) needs to start after the button — the widget sets no label rules, so move it yourself. And if the field's wrapper is position: static with other absolutely positioned children, give it position: relative so the button anchors to the field and not to the page.

Your own UI (headless)

import { inspect, format, searchCountries, listCountries } from '@devix-labs/phone-input/core';

The core has no DOM and no styles: the same parsing, validation and country search, for a combobox you build with your own components. See Server and core.

Updated 15 Sep 2026