LogoSkills

coui-phone-input

전화번호 입력, 국가 코드 선택기, 국제 전화 필드, 국가번호(dial code) 입력을 CoUI Flutter(coui_flutter) 또는 CoUI Web(coui_web/Jaspr)에서 PhoneInput과 PhoneNumber, Country, CoreCountry, PhoneInputValue, CorePhoneInputStyle, CoreP...

CoUI PhoneInput#

Quick Reference#

  • Widget: PhoneInput
  • Variant: 없음 (variant 파라미터 없음) — CoreComponentVariant 축이 없는 단일 폼 컴포넌트
  • Size: 없음 — CoreComponentSize 파라미터 없음; 높이는 CorePhoneInputStyle.height로 조절 (기본값 CoreSpace.space40)
  • Style slot: phoneInputStyle: CorePhoneInputStyle(...)
  • Canonical snippet:
PhoneInput(
  initialCountry: Country.unitedStates,
  onChanged: (phoneNumber) {
    // phoneNumber.fullNumber  → '+11234567890'
    // phoneNumber.number      → '1234567890'  (without dial code)
    // phoneNumber.value       → null when empty, fullNumber otherwise
    print(phoneNumber.fullNumber);
  },
)

한마디로#

이 스킬은 화면에 "전화번호 입력칸"을 만들 때 쓰는 사용법 설명서입니다. 국가 깃발과 국가번호(예: 한국 +82)를 고르는 버튼과 번호 입력칸이 한 칸 안에 합쳐진 형태예요. 마치 해외 배송 주문서에서 나라를 먼저 고르면 앞자리 번호가 자동으로 채워지는 것과 같습니다. 개발자나 AI가 이 입력칸을 일관된 모양으로 빠르게 만들 수 있게 도와줍니다.

무엇을·언제#

  • 무엇을 해주나요: 국가 선택 버튼과 전화번호 입력칸을 하나로 묶은 컴포넌트(PhoneInput)를 만드는 방법을 알려줍니다. 나라를 고르면 국가번호 앞자리가 자동으로 바뀌고, 기본적으로 숫자만 입력되도록 막아줍니다. 국가 선택창 안에는 검색칸이 있어 나라 이름·국가번호·국가코드로 바로 찾을 수 있습니다.
  • 언제 작동하나요: 회원가입, 본인 인증, 연락처 입력처럼 "전화번호를 받는 화면"을 만들 때 이 스킬이 참고됩니다.
  • 어디에서 쓰나요: 모바일 앱(Flutter)과 웹사이트(Web) 두 곳 모두에서 같은 방식으로 쓸 수 있습니다.
  • 무엇을 조절할 수 있나요: 보여줄 나라 목록 제한, 안내 문구(placeholder), 입력칸 모양(둥근 정도·높이·간격·색상 등), 비활성화 상태 등을 자유롭게 바꿀 수 있습니다.

핵심 용어#

용어쉬운 설명
dial code (국가번호)나라마다 정해진 전화 앞자리 번호. 예: 한국 +82, 미국 +1
country / CoreCountry선택한 나라 정보(국가코드·국가번호·이름)를 담는 단위
placeholder입력칸이 비어 있을 때 흐리게 보이는 안내 문구. 예: "전화번호"
onChanged나라나 번호가 바뀔 때마다 알려주는 신호(연결 동작)
fullNumber국가번호와 번호를 합친 전체 번호. 예: +821012345678
enabled / disabled입력칸을 켜고(사용 가능) 끄는(흐리게 비활성화) 상태
style / theme입력칸의 모양·색·크기 같은 디자인 설정
Flutter / Web(Jaspr)각각 모바일 앱과 웹사이트를 만드는 기술

Overview#

PhoneInput is a form component that combines a country selector trigger and a phone number text field inside a single bordered container. Tapping the selector opens a country picker (search input + country list) that both platforms share as the same UX: search matches country name, dial code, or ISO code (case-insensitive). Selecting a country updates the dial code prefix; the number field accepts only digits by default. The Flutter and Web implementations share the same CorePhoneInputContract interface and CorePhoneInputStyle design tokens.

Resolve priority: widget.phoneInputStyle > CorePhoneInputTheme.style (project theme) > design system defaults.

Variants & sizes#

PhoneInput has no variant or size enum (no CoreComponentSize axis). It is a single-form component; every visual knob flows through the single style slot phoneInputStyle: CorePhoneInputStyle (full field set below).

AxisValues
variant— (none)
size — (none; use CorePhoneInputStyle.height, default CoreSpace.space40)
style slotphoneInputStyle: CorePhoneInputStyle

Flutter (coui_flutter)#

Import#

import 'package:coui_flutter/coui_flutter.dart';

The Country enum (and Countries helper) is re-exported from the phonecodes package through the same barrel — no extra import needed.

Basic usage#

PhoneInput(
  initialCountry: Country.unitedStates,
  onChanged: (phoneNumber) {
    // phoneNumber.fullNumber  → '+11234567890'
    // phoneNumber.number      → '1234567890'  (without dial code)
    // phoneNumber.value       → null when empty, fullNumber otherwise
    print(phoneNumber.fullNumber);
  },
)

With initial value#

PhoneInput(
  initialValue: PhoneNumber(Country.southKorea, '1012345678'),
  onChanged: (phoneNumber) => print(phoneNumber.fullNumber),
)

PhoneNumber uses positional parameters: PhoneNumber(country, number). When initialValue is provided, its country takes precedence over initialCountry and its number over initialNumber.

With placeholder and restricted country list#

PhoneInput(
  initialCountry: Country.southKorea,
  placeholder: 'Phone number',
  countries: const [Country.southKorea, Country.unitedStates, Country.japan],
  onChanged: (phoneNumber) => print(phoneNumber.fullNumber),
)

With external controller#

final _controller = TextEditingController();

PhoneInput(
  controller: _controller,
  initialCountry: Country.unitedStates,
  onChanged: (phoneNumber) => print(phoneNumber.fullNumber),
)

An internal controller is created (and disposed) automatically when controller is null.

Disabled state#

PhoneInput(
  initialCountry: Country.unitedStates,
  enabled: false,
)

Disabled rendering wraps the component in IgnorePointer + Opacity (default opacity 0.5), matching Web's container-level opacity.

Style override via phoneInputStyle#

PhoneInput(
  initialCountry: Country.unitedStates,
  phoneInputStyle: const CorePhoneInputStyle(
    borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
    height: CoreSpace.space48,
    selectorPadding: CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16),
    inputFieldPadding: CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16),
    flagSize: 20.0,
    dropdownFlagSize: 20.0,
    hoverColor: CoreColor.token(CoreColors.tertiary),
  ),
  onChanged: (phoneNumber) => print(phoneNumber.fullNumber),
)

Project-level theme (CorePhoneInputTheme)#

// Inside your CoUI theme setup:
CoreComponentTheme(
  phoneInput: CorePhoneInputTheme(
    style: CorePhoneInputStyle(
      borderRadius: CoreBorderRadius.all(CoreRadius.radius4),
      height: CoreSpace.space40,
    ),
  ),
)

Parameters — PhoneInput (Flutter)#

ParameterTypeDefaultDescription
initialCountry Country? null (falls back to Country.unitedStates) Country shown when no initialValue is provided
initialNumber String? null Phone number text (without dial code) used as initial field content
initialValue PhoneNumber? null Combined initial value; country overrides initialCountry , number overrides initialNumber
onChanged ValueChanged<PhoneNumber>? null Called on every country or number change
controller TextEditingController? null External text controller; an internal one is created when null
placeholder String? null Hint text rendered inside the number field when empty
countries List<Country>? null (→ Country.values) Allowed countries shown in the picker; null means all
enabled bool true Disables interaction and applies disabled opacity when false
onlyNumber bool true keyboardType: phone + FilteringTextInputFormatter.digitsOnly
filterPlusCode bool true Strips leading dial code (e.g. +1) or bare + from typed input
filterZeroCode bool true Strips a leading 0 from typed input
filterCountryCode bool true Strips the numeric country-code prefix (without +) from typed input
phoneInputStyle CorePhoneInputStyle? null Per-instance style override

The three filter* checks run as an if/else-if chain in this order — filterPlusCode (dial code with +, then bare +) → filterZeroCodefilterCountryCode — so at most one filter applies per value read.

Behavior notes (Flutter)#

  • Country picker popup: opens anchored below the selector trigger (popover, no vertical invert) with a Search country field on top and the country list below. Search matches name, dialCode, or code (case-insensitive). An empty result shows a muted No results hint.
  • Form integration: the state mixes in FormValueSupplier<PhoneNumber, PhoneInput>, so PhoneInput participates in the CoUI form value system automatically; a replaced form value is written back into the text controller.
  • Autofill: the number field sets autofillHints: [AutofillHints.telephoneNumber].

PhoneNumber (value object, Flutter)#

MemberTypeDescription
countryCountrySelected country enum value
numberStringPhone number without dial code
fullNumber String dialCode + number (e.g. +821012345678)
value String? null when number is empty, fullNumber otherwise
toString() String '' when empty, fullNumber otherwise

Country enum (from phonecodes, re-exported)#

Use Country.<camelCaseName> enum values — e.g. Country.unitedStates, Country.southKorea, Country.japan, Country.unitedKingdom. Use Country.values for the full list. Each value exposes name, code, dialCode, and flag (emoji string).

Note: Korea is Country.southKorea — there is no Country.korea member.


Web / Jaspr (coui_web)#

Import#

import 'package:coui_web/coui_web.dart';

Basic usage#

PhoneInput(
  initialCountry: const CoreCountry(code: 'US', dialCode: '+1', name: 'United States'),
  placeholder: 'Phone number',
  onChanged: (value) {
    // value.fullNumber  → '+11234567890'
    // value.number      → '1234567890'
    // value.countryCode → 'US'
    // value.dialCode    → '+1'
    print(value.fullNumber);
  },
)

With predefined default countries#

PhoneInput.defaultCountries contains 10 common countries (US, KR, JP, GB, DE, FR, CN, IN, CA, AU). When countries is null, this list is used.

PhoneInput(
  initialCountry: const CoreCountry(code: 'KR', dialCode: '+82', name: 'South Korea'),
  countries: PhoneInput.defaultCountries,
  onChanged: (value) => print(value.fullNumber),
)

With custom country list#

PhoneInput(
  initialCountry: const CoreCountry(code: 'KR', dialCode: '+82', name: 'South Korea'),
  countries: const [
    CoreCountry(code: 'KR', dialCode: '+82', name: 'South Korea'),
    CoreCountry(code: 'US', dialCode: '+1', name: 'United States'),
    CoreCountry(code: 'JP', dialCode: '+81', name: 'Japan'),
  ],
  onChanged: (value) => print(value.fullNumber),
)

With HTML attributes and CSS#

PhoneInput(
  id: 'signup-phone',
  classes: 'my-phone-input',
  initialCountry: const CoreCountry(code: 'US', dialCode: '+1', name: 'United States'),
  attributes: const {'data-testid': 'phone-input'},
  onChanged: (value) => print(value.fullNumber),
)

A caller-supplied id becomes the root element id and the outside-click scope for closing the dropdown; when omitted, a generated co-phone-input-N id is used.

Style override via phoneInputStyle#

PhoneInput(
  initialCountry: const CoreCountry(code: 'US', dialCode: '+1', name: 'United States'),
  phoneInputStyle: const CorePhoneInputStyle(
    borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
    height: CoreSpace.space48,
    popupMaxWidth: CoreSpace.space248,
    popupMaxHeight: CoreSpace.space248,
  ),
  onChanged: (value) => print(value.fullNumber),
)

Parameters — PhoneInput (Web)#

ParameterTypeDefaultDescription
initialCountry CoreCountry? null (falls back to CoreCountry(code: 'US', dialCode: '+1', name: 'United States') ) Country shown initially
initialNumber String? null Phone number text (without dial code) used as initial field value
onChanged CoreValueChanged<PhoneInputValue>? null Called on every country or number change
onlyNumber bool true Sets inputmode="numeric" (otherwise inputmode="tel") on the number field
placeholder String? null HTML placeholder attribute on the number field
countries List<CoreCountry>? null (→ PhoneInput.defaultCountries) Countries available in the picker
enabled bool true Adds disabled attribute and applies disabled opacity on the root when false
phoneInputStyle CorePhoneInputStyle? null Per-instance style override
id String? null HTML id for the root <div> (also the outside-click scope)
classes String? null Additional CSS classes for the root element
css Styles? null Inline CSS styles merged onto the root element
attributes Map<String, String>? null Extra HTML attributes for the root element
eventHandlers Map<String, List<UiEventHandler>> {} Custom DOM event handlers for the root element
onClick / onMouseEnter / onMouseLeave UiMouseEventHandler? null Root-level mouse event passthrough
onKeyDown / onKeyUp UiKeyboardEventHandler? null Root-level keyboard event passthrough
onInput / onChange UiInputEventHandler? null Root-level input event passthrough

Web does not use TextEditingController — the contract's controller field is typed Null and always returns null.

Behavior notes (Web)#

  • The number field renders as <input type="tel" autocomplete="tel">.
  • The dropdown (role="listbox") renders only while open, with a Search country input on top; clicking outside the component closes it. Each country row is a <button role="option">.
  • Accessibility: the selector trigger carries aria-label="Select country", aria-haspopup="listbox", and aria-expanded; the search field carries aria-label="Search country".
  • When the parent swaps initialCountry to a new non-null value, the selected country follows (didUpdateComponent).
  • PhoneInput.flagEmoji(code) (delegating to CoreCountry.flagEmoji) converts an ISO 2-letter code to its flag emoji.

PhoneInputValue (value object, Web)#

MemberTypeDescription
countryCode String ISO 2-letter code (e.g. 'KR')
dialCode String Dial code with prefix (e.g. '+82')
number String Phone number without dial code (default '')
fullNumberStringdialCode + number
value String? null when number is empty, fullNumber otherwise

Constructor uses named parameters: PhoneInputValue(countryCode: 'KR', dialCode: '+82', number: '1012345678').

CoreCountry (value object, from coui_core)#

const CoreCountry(code: 'KR', dialCode: '+82', name: 'South Korea')
FieldTypeDescription
codeStringISO 2-letter country code
dialCode String Dial code with + prefix
nameStringLocalized country name

Use CoreCountry.flagEmoji(code) to convert a code string to its flag emoji (returns '' when code is not exactly 2 characters).


CorePhoneInputStyle fields (single style slot)#

All fields are optional overrides merged over the chain defaults → CorePhoneInputTheme.style → phoneInputStyle. Defaults are static fields on CorePhoneInputStyle (static const, except defaultDialCodeTextStyle which is static final) — the single source of truth shared by both platforms.

FieldTypeDefault
borderRadius CoreBorderRadius? CoreBorderRadius.all(CoreRadius.radius4)
flagGapStyle CoreGapStyle? CoreGapStyle(size: CoreSpace.space8, crossAxisExtent: 0)
countryGapStyle CoreGapStyle? CoreGapStyle(size: CoreSpace.space16, crossAxisExtent: 0)
maxWidthdouble?200.0
padding CoreEdgeInsets? null (flush row layout)
heightdouble?CoreSpace.space40
borderWidth double? CoreStrokeWidth.stroke1
inputFieldPadding CoreEdgeInsets? CoreEdgeInsets.symmetric(horizontal: CoreSpace.space12)
selectorPadding CoreEdgeInsets? CoreEdgeInsets.symmetric(horizontal: CoreSpace.space12)
dropdownItemPadding CoreEdgeInsets? CoreEdgeInsets.symmetric(horizontal: CoreSpace.space12, vertical: CoreSpace.space8)
dropdownItemGapStyle CoreGapStyle? CoreGapStyle(size: CoreSpace.space8, crossAxisExtent: 0)
popupMaxWidth double? CoreSpace.space248
popupMaxHeight double? CoreSpace.space248
popoverTopOffset double? CoreSpace.space4
chevronIconStyle CoreIconStyle? CoreIconStyle(size: 12.0)
flagSizedouble?16.0
dropdownFlagSizedouble?16.0
disabledOpacitydouble?0.5
popupBorderRadius CoreBorderRadius? CoreBorderRadius.all(CoreRadius.radius8)
dropdownDialCodeTextStyle CoreTextStyle? CoreTextStyle.token(CoreTextStyles.bodySmall, color: CoreColor.token(CoreColors.onSurfaceVariant))
backgroundColor CoreColor? CoreColor.token(CoreColors.surface)
borderColor CoreColor? CoreColor.token(CoreColors.outlineVariant)
hoverColor CoreColor? CoreColor.token(CoreColors.tertiary)
separatorColor CoreColor? CoreColor.token(CoreColors.outlineVariant)
placeholderColor CoreColor? CoreColor.token(CoreColors.onSurfaceVariant)
chevronColor CoreColor? CoreColor.token(CoreColors.onSurface)
disabledChevronColor CoreColor? CoreColor.token(CoreColors.onSurfaceVariant)
dialCodeTextStyle CoreTextStyle? labelLarge role + medium weight + onSurface color
disabledDialCodeColor CoreColor? CoreColor.token(CoreColors.onSurfaceVariant)
flagTextStyle CoreTextStyle? CoreTextStyle.token(CoreTextStyles.labelLarge)
bodyTextStyle CoreTextStyle? CoreTextStyle.token(CoreTextStyles.bodySmall)

CorePhoneInputStyle also provides merge(other) and copyWith(...).


Flutter ↔ Web differences#

ConceptFlutterWeb (Jaspr)
Widget class PhoneInput (extends StatefulWidget) PhoneInput (extends UiStatefulComponent)
Contract CorePhoneInputContract<TextEditingController?, PhoneNumber, Country> CorePhoneInputContract<Null, PhoneInputValue, CoreCountry>
Country type Country (enum, from phonecodes) CoreCountry (value object, from coui_core)
Value type in onChanged PhoneNumber (positional ctor) PhoneInputValue (named ctor)
Callback type ValueChanged<PhoneNumber>? CoreValueChanged<PhoneInputValue>?
Controller TextEditingController? Null (always null, not supported)
Default country fallback Country.unitedStates CoreCountry(code: 'US', dialCode: '+1', name: 'United States')
Country list default Country.values (all countries) PhoneInput.defaultCountries (10 common countries)
Input filters filterPlusCode / filterZeroCode / filterCountryCode params — (no filter params)
initialValue (combined) supported (PhoneNumber?) — (use initialCountry + initialNumber)
onlyNumber effect phone keyboard + digits-only formatter inputmode="numeric" vs "tel"
Disabled rendering IgnorePointer + Opacity(disabledOpacity) disabled attribute + opacity inline CSS on root
Picker presentation popover overlay below the trigger absolutely-positioned <div role="listbox"> sibling
HTML extras id , classes , css , attributes , eventHandlers , onClick , onInput , onChange , onKeyDown , onKeyUp , onMouseEnter , onMouseLeave
Flag rendering country.flag emoji string PhoneInput.flagEmoji(code) / CoreCountry.flagEmoji(code)
Form integration FormValueSupplier<PhoneNumber, PhoneInput>

Shared across both platforms: CorePhoneInputStyle, CorePhoneInputTheme, CorePhoneInputContract, search-driven country picker UX, and the resolve chain defaults → theme.style → phoneInputStyle.


Pitfalls#

  • Korea is Country.southKorea — there is no Country.korea member, so guessing the naming convention will fail here.
  • Web's controller field on the contract is typed Null and always returns nullTextEditingController is a Flutter-only concept and is not supported on Web.
  • The three Flutter filter* params (filterPlusCodefilterZeroCodefilterCountryCode) run as an if/else-if chain, so at most one filter applies per value read; none of these params exist on Web at all.
  • On Web, onlyNumber only toggles the inputmode attribute ("numeric" vs "tel") as a soft keyboard hint — unlike Flutter, it does not attach a digits-only input formatter that actually blocks non-digit characters.
  • The combined initialValue (PhoneNumber?) parameter is Flutter-only; Web has no equivalent and requires setting initialCountry + initialNumber separately. When Flutter's initialValue is provided, its country/number silently take precedence over initialCountry/initialNumber.
  • CoreCountry.flagEmoji(code) returns '' (not an error) when code is not exactly 2 characters.

v0.127 Migration (Legacy)#

v0.127 removed the Co prefix from widget classes (hard break — no aliases). Core* style/theme/contract names and the value objects are unchanged.

Legacy (❌ removed)v0.127 Canonical
CoPhoneInputPhoneInput
CoPhoneInput.defaultCountries PhoneInput.defaultCountries
CoPhoneInput.flagEmojiPhoneInput.flagEmoji
CoLucideIcons (internal chevron icon set)LucideIcons
❌ per-component src/ imports single barrel: package:coui_flutter/coui_flutter.dart / package:coui_web/coui_web.dart

Unchanged names: CorePhoneInputStyle, CorePhoneInputTheme, CorePhoneInputContract, PhoneNumber, PhoneInputValue, CoreCountry, Country.