NumberInput
버튼으로 올리고 내리는 한 줄 숫자 입력 — 인원 · 수량처럼 작은 수
목차
NumberInput 은 [ − ] 값 [ + ] 한 줄로 숫자를 올리고 내리는 입력입니다. 인원 · 수량처럼 작은 수를 고를 때 씁니다. 값은 직접 입력할 수도 있고, 두 버튼과 위 · 아래 화살표 키가
step 만큼 움직입니다. 값은 늘 min ~ max 안에 머물고, 끝에 닿으면 그 방향 버튼이 비활성이 됩니다.
필드는 TextField 를, 양 끝은 Button 을 합성해 그립니다. 테두리 · 바탕 · hover · 포커스 · 오류 · 라벨 · 설명은
TextField 와 같은 값이라 나란히 놓아도 같은 말을 합니다.
기본 사용법 (Basic Usage)#
NumberInput(
label: '인원',
description: '최대 8명까지 예약할 수 있어요',
initialValue: 2,
min: 1,
max: 8,
size: CoreNumberInputSize.lg,
onChanged: (value) => print(value),
)
NumberInput(
label: '인원',
description: '최대 8명까지 예약할 수 있어요',
initialValue: 2,
min: 1,
max: 8,
size: CoreNumberInputSize.lg,
onChanged: (value) => print(value),
)
Live Preview#
최대 8명까지 예약할 수 있어요
코드 보기
class NumberInputDefaultExample extends StatelessComponent {
const NumberInputDefaultExample({super.key});
@override
Component build(BuildContext context) {
return NumberInput(
label: '인원',
description: '최대 8명까지 예약할 수 있어요',
initialValue: 2,
min: 1,
max: 8,
size: CoreNumberInputSize.lg,
onChanged: (value) => print('Guests: $value'),
);
}
}
class NumberInputDefaultExample extends StatelessWidget {
const NumberInputDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return NumberInput(
label: '인원',
description: '최대 8명까지 예약할 수 있어요',
initialValue: 2,
min: 1,
max: 8,
size: CoreNumberInputSize.lg,
onChanged: (value) => debugPrint('Guests: $value'),
);
}
}
구조 (Anatomy)#
label // 선택
┌────────────────────────────────┐
│ [ − ] value [ + ] │ // TextField(centred) · Button(plain · square) × 2
└────────────────────────────────┘
description // 선택
| 크기 | 필드 | 버튼 | 모서리 |
|---|---|---|---|
sm | 90 × 32 | 24 | 8 |
md | 114 × 40 | 32 | 10 |
lg | 138 × 48 | 40 | 12 |
Figma: CoUI 라이브러리
❖ NumberInput 페이지.
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 인원 · 수량 · 개수처럼 작은 범위의 수를 한두 번 눌러 바꿀 때
- 값의 범위(
min~max)가 정해져 있어 넘어가지 않게 막아야 할 때
대신 다른 컴포넌트를 사용하세요:
- TextField: 자릿수가 많은 숫자나 단위가 붙는 값을 자유롭게 입력할 때
- Slider: 범위 안에서 대략적인 값을 끌어서 고를 때
- Stepper: 숫자 입력이 아니라 단계 이동을 보여 줄 때
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value |
double? |
null |
현재 값(제어). null 이면 initialValue 에서 시작합니다 |
initialValue |
double? |
null |
시작 값. null 이면 min, 그것도 없으면 0 |
onChanged |
ValueChanged<double>? |
null |
값이 바뀔 때 — 버튼 · 키 · 직접 입력 |
min |
double? |
null |
가장 작은 값. 닿으면 − 버튼이 비활성 |
max |
double? |
null |
가장 큰 값. 닿으면 + 버튼이 비활성 |
step |
double |
1 |
버튼 한 번 · 화살표 한 번에 움직이는 양. 양수 |
variant |
CoreNumberInputVariant |
defaultVariant |
defaultVariant · filled · error |
size |
CoreNumberInputSize |
md |
sm · md · lg |
enabled |
bool? |
null |
null 이면 켜짐 |
label | String? | null | 필드 위 이름 |
description |
String? |
null |
필드 아래 도움말 |
numberInputStyle |
CoreNumberInputStyle? |
null |
인스턴스 스타일 |
CoreNumberInputStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
width |
double? |
The whole field's width (logical px), border to border. null → the size row's width. |
buttonStyle |
CoreButtonStyle? |
Chrome of the two step buttons, forwarded raw to each Button. |
fieldStyle |
CoreTextFieldStyle? |
Chrome of the composed TextField, forwarded raw. |
스타일 시스템 (Style System)#
CoreNumberInputStyle 은 두 가지 크기(width · buttonSize)와 두 개의 중첩 슬롯(fieldStyle
· buttonStyle)만 가집니다. 필드의 테두리 · 바탕 · 포커스는 TextField 의 스타일이 정하고, fieldStyle
이 그대로 넘겨집니다.
Resolve chain#
design system default (defaultsBySize[size])
→ CoreNumberInputTheme.style
→ CoreNumberInputTheme.variantStyles[variant]
→ widget.numberInputStyle
테마로 덮어쓰기#
CoreNumberInputTheme(
variantStyles: {
CoreNumberInputVariant.error: CoreNumberInputStyle(
fieldStyle: CoreTextFieldStyle(errorBorderColor: ...),
),
},
)
변형 (Variants)#
variant: defaultVariant#
표면 바탕에 outline-variant 테두리. hover 는 outline, 포커스는 primary 1.5.
variant: filled#
surface-container 바탕. 테두리가 바탕과 같은 색이라 면으로 읽힙니다.
variant: error#
테두리가 오류 색입니다. 굵기는 쉴 때 1, 포커스에서만 1.5 입니다.
동작 스펙 (Behavior)#
인터랙션#
- − · + 버튼은
step만큼 값을 움직이고onChanged를 부릅니다. - 위 · 아래 화살표는
step, Page Up · Page Down 은step × 10만큼 움직입니다. -
값 칸에 직접 입력할 수 있습니다. 범위 안의 숫자는 입력하는 즉시 반영하고, 범위 밖이거나 아직 숫자가 아닌 입력(
-)은 포커스가 빠지거나 Enter 를 누를 때 확정합니다 — 범위 밖은 가장 가까운 끝으로, 숫자가 아니면 이전 값으로 돌아갑니다. - 값은
min~max를 넘지 않습니다.
상태 전환#
- 끝에 닿으면 그 방향 버튼이 비활성이 되고 반대쪽 버튼은 그대로 눌립니다.
enabled: false이면 필드와 두 버튼이 모두 비활성이고 키도 반응하지 않습니다.
소수 값#
step 이 소수여도 0.1 + 0.2 가 0.3 으로 보이도록 step 과 현재 값의 소수 자릿수에서 반올림합니다.
사용 가이드라인 (Usage Guidelines)#
Do#
- 범위가 정해진 값에는
min·max를 함께 줍니다 — 끝의 버튼이 자동으로 비활성이 됩니다. - 라벨을 붙여 무엇의 수인지 말합니다. 단위는
description에 적습니다.
Don't#
- 자릿수가 많은 값(전화번호 · 금액)에 쓰지 않습니다 —
TextField를 씁니다. step에 0 이나 음수를 주지 않습니다.
접근성 (Accessibility)#
역할#
-
Web 은 값 칸이
type="number"입력이라 브라우저가spinbutton으로 알리고min·max· 현재 값을 읽습니다. - Flutter 는 값과 함께 늘리기 · 줄이기 동작을 시맨틱에 싣습니다.
-
두 버튼의 접근성 이름은 로케일의 "줄이기" · "늘리기" 입니다(
numberInputDecrease·numberInputIncrease).
키보드#
- 위 · 아래 화살표:
step만큼 증감. - Page Up · Page Down:
step × 10만큼 증감. - Enter: 입력한 값을 확정.
- Tab: 값 칸과 두 버튼을 차례로 지납니다.
스크린 리더#
- 값 칸은 현재 값과 범위를, 두 버튼은 "줄이기" · "늘리기" 이름을 읽어 줍니다.
- Flutter 는 늘리기 · 줄이기 동작을 시맨틱에 실어, 보조 기술의 스와이프로 값을 움직일 수 있습니다.
스크린 리더#
- 값 칸은 현재 값과 범위를, 두 버튼은 "줄이기" · "늘리기" 이름을 읽어 줍니다.
- Flutter 는 늘리기 · 줄이기 동작을 시맨틱에 실어, 보조 기술의 스와이프로 값을 움직일 수 있습니다.
스크린 리더#
- 값 칸은 현재 값과 범위를, 두 버튼은 "줄이기" · "늘리기" 이름을 읽어 줍니다.
- Flutter 는 늘리기 · 줄이기 동작을 시맨틱에 실어, 보조 기술의 스와이프로 값을 움직일 수 있습니다.
포커스 관리#
바깥 포커스 링은 키보드 포커스일 때만 그립니다. 마우스 · 터치로 값 칸을 누르면 테두리가 primary 1.5 로만 바뀝니다.
알려진 제약#
- 비활성 버튼은 포커스를 받지 않습니다. 끝에 닿아 버튼이 비활성이 되는 순간 포커스가 그 버튼에 있었다면 값 칸으로 돌아오지 않습니다.
전역 축(동작 줄이기 · 고대비 · 색 강제 모드 · 시맨틱 역할 · 최소 터치 타겟)은 접근성 페이지가 한 곳에서 다룹니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 값 칸 | 글자 입력만 받는 TextField, 0-9 . + - 만 통과 |
type="number" 입력 — 브라우저가 문자를 거릅니다 |
| 키 처리 | 포커스 부모에서 KeyEvent 를 받음 |
래퍼 <div> 의 keydown |
| 확정 시점 | 포커스가 빠지거나 onSubmitted | focusout 이나 Enter |
| 접근성 역할 | Semantics 의 늘리기 · 줄이기 동작 | 입력의 spinbutton 역할 |
| 캐럿 |
합성한
TextField
의 기본값 그대로 — 굵기 2 · 모서리 2 ·
primary
, 값 바로 뒤(간격 0), 높이는 값 줄 상자(
bodyMedium
14 × 1.5 = 21). iOS · macOS 는 프레임워크가 줄 상자보다 2 높게 그립니다
|
브라우저가 그리는 기본 캐럿 — CSS 로 정할 수 있는 것은 색(caret-color: primary)뿐이라 굵기 · 모서리 · 높이는 브라우저 몫입니다 |
이름 · 기본값 · 순서는 두 플랫폼이 같습니다.
관련 컴포넌트 (Related Components)#
조합 예제#
NumberInput(
label: '수량',
variant: CoreNumberInputVariant.filled,
size: CoreNumberInputSize.sm,
initialValue: 1,
min: 1,
)