Shadcn Phone Input Svelte
An implementation of a Phone Input component built on top of Shadcn UI's input component.
Setup
Install shadcn-svelte via CLI
Run the shadcn-svelte
init command to setup your project:
npx shadcn-svelte@next init
Install necessary components
Run the shadcn-svelte
add command to add the necessary components to your project:
npx shadcn-svelte@next add button
npx shadcn-svelte@next add command
npx shadcn-svelte@next add popover
npx shadcn-svelte@next add scroll-area
Add Component
Automatic
npx jsrepo add github/ieedan/shadcn-phone-input-svelte/ui/phone-input
or
Manual
Install Svelte Tel Input
npm install svelte-tel-input@latest
Copy the code
You can find the code here. Or copy it below.
`src/lib/components/ui/phone-input`
<script lang="ts">
import CountrySelector from './country-selector.svelte';
import { defaultOptions, type Props } from '.';
import { cn } from '$lib/utils';
import { TelInput, normalizedCountries } from 'svelte-tel-input';
import 'svelte-tel-input/styles/flags.css';
const countries = normalizedCountries;
let {
class: className = undefined,
defaultCountry = null,
country = $bindable(defaultCountry),
options = defaultOptions,
placeholder = $bindable(undefined),
readonly = $bindable(false),
disabled = $bindable(false),
value = $bindable(),
valid = $bindable(false),
detailedValue = $bindable(),
order = undefined,
name = undefined,
...rest
}: Props = $props();
let el: HTMLInputElement | undefined = $state();
export const focus = () => {
// sort of an after update kinda thing
setTimeout(() => {
el?.focus();
}, 0);
};
</script>
<div class="flex place-items-center">
<CountrySelector
{order}
{countries}
bind:selected={country}
on:select={focus}
/>
<TelInput
{name}
bind:country
bind:detailedValue
bind:value
bind:valid
bind:readonly
bind:disabled
bind:placeholder
bind:el
{options}
required
class={cn(
'flex h-10 w-[212px] rounded-lg rounded-l-none border border-l-0 border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...rest}
/>
</div>
<script lang="ts">
import * as Popover from '$lib/components/ui/popover';
import { Button } from '$lib/components/ui/button';
import * as Command from '$lib/components/ui/command';
import { ScrollArea } from '$lib/components/ui/scroll-area';
import { Check, ChevronsUpDown } from 'lucide-svelte';
import { cn } from '$lib/utils';
import Flag from './flag.svelte';
import { createEventDispatcher } from 'svelte';
import type { Country, CountryCode } from 'svelte-tel-input/types';
const dispatch = createEventDispatcher();
interface Props {
/** List of countries */
countries: Country[];
disabled?: boolean;
selected?: CountryCode | null;
/** Default ordering is alphabetical by country name supply this function to customize the sorting behavior */
order?: (a: Country, b: Country) => number;
}
let {
countries,
disabled = false,
selected = $bindable(null),
order = (a, b) => {
return a.name.localeCompare(b.name);
},
}: Props = $props();
let selectedCountry = $derived(countries.find((a) => a.iso2 == selected));
let open = $state(false);
const selectCountry = (country: Country) => {
selected = country.iso2;
open = false;
dispatch('select', selected);
};
</script>
<Popover.Root bind:open>
<Popover.Trigger>
{#snippet child({ props })}
<Button
type="button"
variant="outline"
class={cn('flex gap-1 rounded-e-none rounded-s-lg px-3')}
{disabled}
{...props}
>
<Flag country={selectedCountry} />
<ChevronsUpDown
class={cn(
'-mr-2 h-4 w-4 opacity-50',
disabled ? 'hidden' : 'opacity-100'
)}
/>
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-[300px] p-0">
<Command.Root>
<Command.Input placeholder="Search country..." />
<Command.List>
<ScrollArea class="h-72">
<Command.Empty>No country found.</Command.Empty>
<Command.Group>
{#each countries.sort(order) as country}
<Command.Item
class="gap-2"
value={country.name}
onSelect={() => selectCountry(country)}
>
<Flag {country} />
<span class="flex-1 text-sm">{country.name}</span>
<span class="text-sm text-foreground/50">
+{country.dialCode}
</span>
<div class="w-4">
{#if country.iso2 == selected}
<Check class="size-4" />
{/if}
</div>
</Command.Item>
{/each}
</Command.Group>
</ScrollArea>
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>
<script lang="ts">
import type { Country } from 'svelte-tel-input/types';
interface Props {
country?: Country | null;
}
let { country = null }: Props = $props();
</script>
<span class="flex h-4 w-6 overflow-hidden rounded-sm bg-foreground/20">
{#if country}
<span
aria-label="{country.name} flag."
class="flag flag-{country.iso2.toLowerCase()} mr-3 flex-shrink-0"
></span>
{/if}
</span>
import Root from './phone-input.svelte';
import type {
Country,
CountryCode,
DetailedValue,
E164Number,
TelInputOptions,
} from 'svelte-tel-input/types';
export type Props = {
country?: CountryCode | null;
defaultCountry?: CountryCode | null;
el?: HTMLInputElement;
name?: string;
placeholder?: string;
disabled?: boolean;
readonly?: boolean;
class?: string;
value: E164Number | null;
valid?: boolean;
detailedValue?: Partial<DetailedValue> | null;
options?: TelInputOptions;
order?: ((a: Country, b: Country) => number) | undefined;
};
export const defaultOptions: TelInputOptions = {
spaces: true,
autoPlaceholder: false,
format: 'international',
};
export default Root;
Examples
Basic
Default Country
Default Value
Custom Ordering
Detailed Value
National: International: Country Code: Dial Code: