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).
| Axis | Values |
|---|---|
| variant | — (none) |
| size | — (none; use CorePhoneInputStyle.height, default CoreSpace.space40) |
| style slot | phoneInputStyle: 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),
)
PhoneNumberuses positional parameters:PhoneNumber(country, number). WheninitialValueis provided, itscountrytakes precedence overinitialCountryand itsnumberoverinitialNumber.
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)#
| Parameter | Type | Default | Description |
|---|---|---|---|
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+) →filterZeroCode→filterCountryCode— 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 countryfield on top and the country list below. Search matchesname,dialCode, orcode(case-insensitive). An empty result shows a mutedNo resultshint. -
Form integration: the state mixes in
FormValueSupplier<PhoneNumber, PhoneInput>, soPhoneInputparticipates 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)#
| Member | Type | Description |
|---|---|---|
country | Country | Selected country enum value |
number | String | Phone 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 noCountry.koreamember.
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
idbecomes the root element id and the outside-click scope for closing the dropdown; when omitted, a generatedco-phone-input-Nid 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)#
| Parameter | Type | Default | Description |
|---|---|---|---|
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'scontrollerfield is typedNulland always returnsnull.
Behavior notes (Web)#
- The number field renders as
<input type="tel" autocomplete="tel">. -
The dropdown (
role="listbox") renders only while open, with aSearch countryinput 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", andaria-expanded; the search field carriesaria-label="Search country". -
When the parent swaps
initialCountryto a new non-null value, the selected country follows (didUpdateComponent). -
PhoneInput.flagEmoji(code)(delegating toCoreCountry.flagEmoji) converts an ISO 2-letter code to its flag emoji.
PhoneInputValue (value object, Web)#
| Member | Type | Description |
|---|---|---|
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 '') |
fullNumber | String | dialCode + 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')
| Field | Type | Description |
|---|---|---|
code | String | ISO 2-letter country code |
dialCode |
String |
Dial code with + prefix |
name | String | Localized 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.
| Field | Type | Default |
|---|---|---|
borderRadius |
CoreBorderRadius? |
CoreBorderRadius.all(CoreRadius.radius4) |
flagGapStyle |
CoreGapStyle? |
CoreGapStyle(size: CoreSpace.space8, crossAxisExtent: 0) |
countryGapStyle |
CoreGapStyle? |
CoreGapStyle(size: CoreSpace.space16, crossAxisExtent: 0) |
maxWidth | double? | 200.0 |
padding |
CoreEdgeInsets? |
null (flush row layout) |
height | double? | 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) |
flagSize | double? | 16.0 |
dropdownFlagSize | double? | 16.0 |
disabledOpacity | double? | 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#
| Concept | Flutter | Web (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 noCountry.koreamember, so guessing the naming convention will fail here. -
Web's
controllerfield on the contract is typedNulland always returnsnull—TextEditingControlleris a Flutter-only concept and is not supported on Web. -
The three Flutter
filter*params (filterPlusCode→filterZeroCode→filterCountryCode) 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,
onlyNumberonly toggles theinputmodeattribute ("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 settinginitialCountry+initialNumberseparately. When Flutter'sinitialValueis provided, itscountry/numbersilently take precedence overinitialCountry/initialNumber. -
CoreCountry.flagEmoji(code)returns''(not an error) whencodeis 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 |
|---|---|
❌ CoPhoneInput | PhoneInput |
❌ CoPhoneInput.defaultCountries |
PhoneInput.defaultCountries |
❌ CoPhoneInput.flagEmoji | PhoneInput.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.