@desource/phone-mask-svelte
v1.0.0
Published
π Svelte 5 component and composable for international phone number masking. Powered by @desource/phone-mask with Google libphonenumber sync.
Maintainers
Readme
@desource/phone-mask-svelte
Svelte 5 phone input component with Google's libphonenumber data
Beautiful, accessible, extreme small & tree-shakeable Svelte 5 phone input with auto-formatting, country selector, and validation.
β¨ Features
- π¨ Beautiful UI β Modern design with light/dark themes
- π Smart Country Search β Fuzzy matching with keyboard navigation
- π Auto-formatting β As-you-type formatting with smart cursor
- β Validation β Built-in validation with visual feedback
- π Copy Button β One-click copy to clipboard
- π Auto-detection β GeoIP and locale-based detection
- βΏ Accessible β ARIA labels, keyboard navigation
- π± Mobile-friendly β Optimized for touch devices
- π― TypeScript β Full type safety
- π§© Two modes β Component or composable
- β‘ Optimized β Tree-shaking and code splitting
π¦ Installation
npm install @desource/phone-mask-svelte
# or
yarn add @desource/phone-mask-svelte
# or
pnpm add @desource/phone-mask-svelteπ Quick Start
Importing
Component mode:
import { PhoneInput } from '@desource/phone-mask-svelte';
import '@desource/phone-mask-svelte/assets/lib.css'; // Import stylesComposable mode:
import { usePhoneMask } from '@desource/phone-mask-svelte';Component Mode
<script lang="ts">
import { PhoneInput } from '@desource/phone-mask-svelte';
import '@desource/phone-mask-svelte/assets/lib.css';
let phone = $state('');
let isValid = $state(false);
</script>
<PhoneInput bind:value={phone} country="US" onvalidationchange={(v) => (isValid = v)} />
{#if isValid}
<p>β Valid phone number</p>
{/if}Composable Mode
For custom input implementations:
<script lang="ts">
import { usePhoneMask } from '@desource/phone-mask-svelte';
let value = $state('');
const phoneMask = usePhoneMask({
value: () => value,
onChange: (digits) => (value = digits),
country: () => 'US',
detect: () => false
});
</script>
<div>
<input bind:this={phoneMask.inputRef} type="tel" placeholder="Phone number" />
<p>Formatted: {phoneMask.fullFormatted}</p>
<p>Valid: {phoneMask.isComplete ? 'Yes' : 'No'}</p>
<p>Country: {phoneMask.country.name}</p>
<button onclick={() => phoneMask.setCountry('GB')}>Use UK</button>
</div>π Component API
Props
Note: The component supports both controlled and bindable modes. Use
bind:valuefor two-way binding orvalue+onchangefor controlled mode.
interface PhoneInputProps {
// Bindable value (digits only, without country code)
value?: string;
// Preselected country (ISO 3166-1 alpha-2)
country?: CountryKey;
// Auto-detect country from IP/locale
detect?: boolean; // Default: true
// Locale for country names
locale?: string; // Default: browser language
// Size variant
size?: 'compact' | 'normal' | 'large'; // Default: 'normal'
// Visual theme ("auto" | "light" | "dark")
theme?: 'auto' | 'light' | 'dark'; // Default: 'auto'
// Disabled state
disabled?: boolean; // Default: false
// Readonly state
readonly?: boolean; // Default: false
// Show copy button
showCopy?: boolean; // Default: true
// Show clear button
showClear?: boolean; // Default: false
// Show validation state (borders & outline)
withValidity?: boolean; // Default: true
// Custom search placeholder
searchPlaceholder?: string; // Default: 'Search country or code...'
// Custom no results text
noResultsText?: string; // Default: 'No countries found'
// Custom clear button label
clearButtonLabel?: string; // Default: 'Clear phone number'
// Dropdown menu custom CSS class
dropdownClass?: string;
// Disable default styles
disableDefaultStyles?: boolean; // Default: false
// Extra CSS class merged onto root element
class?: string;
// Callback when the phone number changes.
// Provides an object with:
// - full: Full phone number with country code (e.g. +1234567890)
// - fullFormatted: Full phone number formatted according to country rules (e.g. +1 234-567-890)
// - digits: Only the digits of the phone number without country code (e.g. 234567890)
onchange?: (value: PhoneNumber) => void;
// Callback when the selected country changes
oncountrychange?: (country: MaskFull) => void;
// Callback when the validation state changes
onvalidationchange?: (isValid: boolean) => void;
// Callback when the input is focused
onfocus?: (event: FocusEvent) => void;
// Callback when the input is blurred
onblur?: (event: FocusEvent) => void;
// Callback when phone number is copied
oncopy?: (value: string) => void;
// Callback when input is cleared
onclear?: () => void;
}Exposed Methods
Access component methods via bind:this:
<script lang="ts">
import { PhoneInput } from '@desource/phone-mask-svelte';
import type { PhoneInputExposed } from '@desource/phone-mask-svelte';
let phoneInput = $state<PhoneInputExposed | null>(null);
</script>
<PhoneInput bind:this={phoneInput} />
<button onclick={() => phoneInput?.focus()}>Focus</button>interface PhoneInputExposed {
focus: () => void; // Focus the input
blur: () => void; // Blur the input
clear: () => void; // Clear the input value
selectCountry: (code: string) => void; // Programmatically select a country by ISO code
getFullNumber: () => string; // Returns full phone number with country code (e.g. +1234567890)
getFullFormattedNumber: () => string; // Returns formatted number with country code (e.g. +1 234-567-890)
getDigits: () => string; // Returns only digits without country code (e.g. 234567890)
isValid: () => boolean; // Checks if the current phone number is valid
isComplete: () => boolean; // Alias for isValid()
}Snippets
<PhoneInput bind:value={phone}>
{#snippet flag(country)}
<img src="/flags/{country.code.toLowerCase()}.svg" alt={country.name} />
{/snippet}
{#snippet copysvg(copied)}
{copied ? 'β' : 'π'}
{/snippet}
{#snippet clearsvg()}
β
{/snippet}
{#snippet actionsbefore()}
<button onclick={handleCustomAction}>Custom</button>
{/snippet}
</PhoneInput>| Snippet | Props | Description |
| --------------- | ------------------------ | ------------------------------------------------- |
| flag | MaskFull | Custom flag icon in the country list and selector |
| copysvg | boolean (copied state) | Custom copy button icon |
| clearsvg | β | Custom clear button icon |
| actionsbefore | β | Content rendered before default action buttons |
π§© Composable API
Options
Note: The composable uses getter functions for reactive options. Do NOT pass values directly.
interface UsePhoneMaskOptions {
// Getter returning current digit value (controlled) - REQUIRED
value: () => string;
// Callback when the digits value changes - REQUIRED
onChange: (digits: string) => void;
// Getter for ISO country code (e.g., 'US', 'DE', 'GB')
country?: () => string | undefined;
// Getter for locale string (default: navigator.language)
locale?: () => string | undefined;
// Getter for auto-detect flag (default: false)
detect?: () => boolean | undefined;
// Callback when the phone changes (full, fullFormatted, digits)
onPhoneChange?: (phone: PhoneNumber) => void;
// Country change callback
onCountryChange?: (country: MaskFull) => void;
}Return Value
Important: Do NOT destructure the returned object β all properties are reactive getters and destructuring breaks reactivity.
interface UsePhoneMaskReturn {
inputRef: HTMLInputElement | null; // Bind to <input> with bind:this
digits: string;
full: string;
fullFormatted: string;
isComplete: boolean;
isEmpty: boolean;
shouldShowWarn: boolean;
country: MaskFull;
setCountry: (countryCode?: string | null) => boolean;
clear: () => void;
}<script lang="ts">
// β
CORRECT β access as properties
const phoneMask = usePhoneMask(options);
phoneMask.digits;
// β WRONG β loses reactivity
const { digits } = usePhoneMask(options);
</script>π¨ Component Styling
CSS Custom Properties
Customize colors via CSS variables:
.phone-input,
.phone-dropdown {
/* Colors */
--pi-bg: #ffffff;
--pi-fg: #111827;
--pi-muted: #6b7280;
--pi-border: #e5e7eb;
--pi-border-hover: #d1d5db;
--pi-border-focus: #3b82f6;
--pi-focus-ring: 3px solid rgb(59 130 246 / 0.15);
--pi-disabled-bg: #f9fafb;
--pi-disabled-fg: #9ca3af;
/* Sizes */
--pi-font-size: 16px;
--pi-height: 44px;
/* Spacing */
--pi-padding: 12px;
/* Border radius */
--pi-radius: 8px;
/* Shadows */
--pi-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--pi-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05);
/* Validation */
--pi-warning: #f59e0b;
--pi-warning-light: #fbbf24;
--pi-success: #10b981;
--pi-focus-ring-warning: 3px solid rgb(245 158 11 / 0.15);
--pi-focus-ring-success: 3px solid rgb(16 185 129 / 0.15);
}Dark Theme
<PhoneInput bind:value={phone} theme="dark" />Or with CSS:
.phone-input[data-theme='dark'] {
--pi-bg: #1f2937;
--pi-fg: #f9fafb;
--pi-border: #374151;
}π Examples
With Validation
<script lang="ts">
import { PhoneInput } from '@desource/phone-mask-svelte';
let phone = $state('');
let isValid = $state(false);
const errorMessage = $derived(!phone ? '' : isValid ? '' : 'Please enter a valid phone number');
</script>
<div>
<PhoneInput bind:value={phone} country="US" onvalidationchange={(v) => (isValid = v)} />
{#if errorMessage}
<span class="error">{errorMessage}</span>
{/if}
</div>Auto-detect Country
<script lang="ts">
import { PhoneInput } from '@desource/phone-mask-svelte';
import type { PMaskFull } from '@desource/phone-mask-svelte';
let phone = $state('');
let detectedCountry = $state('');
function handleCountryChange(country: PMaskFull) {
detectedCountry = country.name;
}
</script>
<PhoneInput bind:value={phone} detect oncountrychange={handleCountryChange} />
{#if detectedCountry}
<p>Detected: {detectedCountry}</p>
{/if}Programmatic Control
<script lang="ts">
import { PhoneInput } from '@desource/phone-mask-svelte';
import type { PhoneInputExposed } from '@desource/phone-mask-svelte';
let phone = $state('');
let phoneInput = $state<PhoneInputExposed | null>(null);
</script>
<PhoneInput bind:this={phoneInput} bind:value={phone} />
<div>
<button onclick={() => phoneInput?.focus()}>Focus</button>
<button onclick={() => phoneInput?.clear()}>Clear</button>
<button onclick={() => phoneInput?.selectCountry('GB')}>Switch to UK</button>
<p>Full: {phoneInput?.getFullFormattedNumber()}</p>
<p>Valid: {phoneInput?.isValid()}</p>
</div>Multiple Inputs
<script lang="ts">
import { PhoneInput } from '@desource/phone-mask-svelte';
let form = $state({ mobile: '', home: '', work: '' });
</script>
<div class="form">
<label>
Mobile
<PhoneInput bind:value={form.mobile} country="US" />
</label>
<label>
Home
<PhoneInput bind:value={form.home} country="US" />
</label>
<label>
Work
<PhoneInput bind:value={form.work} country="US" />
</label>
</div>Custom Composable Implementation
<script lang="ts">
import { usePhoneMask } from '@desource/phone-mask-svelte';
import type { PMaskPhoneNumber } from '@desource/phone-mask-svelte';
let inputValue = $state('');
let selectedCountry = $state('US');
const phoneMask = usePhoneMask({
value: () => inputValue,
country: () => selectedCountry,
detect: () => false,
onChange: (digits) => {
inputValue = digits;
},
onPhoneChange: (data: PMaskPhoneNumber) => {
console.log('Phone:', data.fullFormatted);
}
});
</script>
<div class="custom-phone">
<select bind:value={selectedCountry}>
<option value="US">πΊπΈ +1</option>
<option value="GB">π¬π§ +44</option>
<option value="DE">π©πͺ +49</option>
</select>
<input bind:this={phoneMask.inputRef} type="tel" placeholder="Phone number" />
</div>
<p>Formatted: {phoneMask.fullFormatted}</p>
<p>Valid: {phoneMask.isComplete ? 'Yes' : 'No'}</p>
<p>Country: {phoneMask.country.name}</p>π― Browser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- iOS Safari 14+
- Chrome Mobile
π¦ What's Included
@desource/phone-mask-svelte/
βββ dist/
β βββ types/ # TypeScript declaration files
β βββ index.mjs # ES module bundle
β βββ index.cjs # CommonJS bundle
β βββ phone-mask-svelte.css # Component styles
βββ README.md # This file
βββ package.json # Package manifestπ Related
- @desource/phone-mask β Core library
- @desource/phone-mask-nuxt β Nuxt module
- @desource/phone-mask-vue β Vue 3 bindings
- @desource/phone-mask-react β React bindings
π License
MIT Β© 2026 DeSource Labs
