Button#
사용자 인터랙션을 위한 기본 버튼 컴포넌트입니다. Primary, secondary, ghost, outline 변형을 지원합니다.
Live Preview#
class ButtonPrimaryExample extends StatefulComponent {
const ButtonPrimaryExample({super.key});
@override
State<ButtonPrimaryExample> createState() => _ButtonPrimaryExampleState();
}
class _ButtonPrimaryExampleState extends State<ButtonPrimaryExample> {
@override
Component build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: text('Primary'),
);
}
}
class ButtonPrimaryExample extends StatefulWidget {
const ButtonPrimaryExample({super.key});
@override
State<ButtonPrimaryExample> createState() => _ButtonPrimaryExampleState();
}
class _ButtonPrimaryExampleState extends State<ButtonPrimaryExample> {
@override
Widget build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.primary,
onPressed: () {},
child: const Text('Primary'),
);
}
}
class ButtonSecondaryExample extends StatefulComponent {
const ButtonSecondaryExample({super.key});
@override
State<ButtonSecondaryExample> createState() =>
_ButtonSecondaryExampleState();
}
class _ButtonSecondaryExampleState extends State<ButtonSecondaryExample> {
@override
Component build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.secondary,
onPressed: () {},
child: text('Secondary'),
);
}
}
class ButtonSecondaryExample extends StatefulWidget {
const ButtonSecondaryExample({super.key});
@override
State<ButtonSecondaryExample> createState() => _ButtonSecondaryExampleState();
}
class _ButtonSecondaryExampleState extends State<ButtonSecondaryExample> {
@override
Widget build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.secondary,
onPressed: () {},
child: const Text('Secondary'),
);
}
}
class ButtonOutlineExample extends StatefulComponent {
const ButtonOutlineExample({super.key});
@override
State<ButtonOutlineExample> createState() => _ButtonOutlineExampleState();
}
class _ButtonOutlineExampleState extends State<ButtonOutlineExample> {
@override
Component build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.outline,
onPressed: () {},
child: text('Outline'),
);
}
}
class ButtonOutlineExample extends StatefulWidget {
const ButtonOutlineExample({super.key});
@override
State<ButtonOutlineExample> createState() => _ButtonOutlineExampleState();
}
class _ButtonOutlineExampleState extends State<ButtonOutlineExample> {
@override
Widget build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.outline,
onPressed: () {},
child: const Text('Outline'),
);
}
}
class ButtonGhostExample extends StatefulComponent {
const ButtonGhostExample({super.key});
@override
State<ButtonGhostExample> createState() => _ButtonGhostExampleState();
}
class _ButtonGhostExampleState extends State<ButtonGhostExample> {
@override
Component build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.ghost,
onPressed: () {},
child: text('Ghost'),
);
}
}
class ButtonGhostExample extends StatefulWidget {
const ButtonGhostExample({super.key});
@override
State<ButtonGhostExample> createState() => _ButtonGhostExampleState();
}
class _ButtonGhostExampleState extends State<ButtonGhostExample> {
@override
Widget build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.ghost,
onPressed: () {},
child: const Text('Ghost'),
);
}
}
class ButtonLinkExample extends StatefulComponent {
const ButtonLinkExample({super.key});
@override
State<ButtonLinkExample> createState() => _ButtonLinkExampleState();
}
class _ButtonLinkExampleState extends State<ButtonLinkExample> {
@override
Component build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.link,
onPressed: () {},
child: text('Link'),
);
}
}
class ButtonLinkExample extends StatefulWidget {
const ButtonLinkExample({super.key});
@override
State<ButtonLinkExample> createState() => _ButtonLinkExampleState();
}
class _ButtonLinkExampleState extends State<ButtonLinkExample> {
@override
Widget build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.link,
onPressed: () {},
child: const Text('Link'),
);
}
}
class ButtonDestructiveExample extends StatefulComponent {
const ButtonDestructiveExample({super.key});
@override
State<ButtonDestructiveExample> createState() =>
_ButtonDestructiveExampleState();
}
class _ButtonDestructiveExampleState extends State<ButtonDestructiveExample> {
@override
Component build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.destructive,
onPressed: () {},
child: text('Destructive'),
);
}
}
class ButtonDestructiveExample extends StatefulWidget {
const ButtonDestructiveExample({super.key});
@override
State<ButtonDestructiveExample> createState() =>
_ButtonDestructiveExampleState();
}
class _ButtonDestructiveExampleState extends State<ButtonDestructiveExample> {
@override
Widget build(BuildContext context) {
return CoButton(
variant: CoreButtonVariant.destructive,
onPressed: () {},
child: const Text('Destructive'),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 폼 제출, 저장, 삭제 등 명확한 액션을 트리거할 때
- 다이얼로그의 확인/취소 버튼이 필요할 때
- 네비게이션이 아닌 사용자 액션을 수행할 때
대신 다른 컴포넌트를 사용하세요:
Toggle: 켜기/끄기 상태를 전환할 때Link (Text): 페이지 이동이 목적일 때FAB: 화면의 주요 플로팅 액션일 때Menu: 여러 옵션 중 선택이 필요할 때
기본 사용법 (Basic Usage)#
// 기본 버튼 — variant 는 위젯 파라미터
CoButton(
variant: CoreButtonVariant.primary,
onPressed: handleSubmit,
child: Text('제출'),
)
// 다른 변형
CoButton(
variant: CoreButtonVariant.outline,
onPressed: handleCancel,
child: Text('취소'),
)
// 비활성화 (onPressed null)
CoButton(
variant: CoreButtonVariant.primary,
onPressed: null,
child: Text('비활성화'),
)
// 기본 버튼 — variant 는 위젯 파라미터
CoButton(
variant: CoreButtonVariant.primary,
onPressed: handleSubmit,
child: text('제출'),
)
// 다른 변형
CoButton(
variant: CoreButtonVariant.outline,
onPressed: handleCancel,
child: text('취소'),
)
// 비활성화
CoButton(
variant: CoreButtonVariant.primary,
enabled: false,
child: text('비활성화'),
)
시맨틱 vs 스타일 — 분리 패턴 (Epic #1302)#
CoButton 의 인스턴스 정체성을 결정하는 시맨틱 enum 은 위젯 파라미터로,
chrome / dimensional 미세 조정은 buttonStyle 슬롯으로 분리됩니다.
| 분류 | 어디로 | 예시 |
|---|---|---|
| 시맨틱 enum (정체성) | 위젯 파라미터 | variant, size, shape |
| chrome / dimensional (자유 override) | buttonStyle: CoreButtonStyle(...) |
width, paddingH, backgroundColor, labelStyle |
| 인터랙션 / 콜백 / 포커스 | 위젯 파라미터 | onPressed, enabled, focusNode |
같은 의미의 필드를 두 군데에서 받지 않습니다 — Material / shadcn / Radix / MUI / CoCard 모두 동일 패턴입니다.
CoButton(
variant: CoreButtonVariant.primary, // ← 시맨틱 (위젯 파라미터)
size: CoreComponentSize.lg, // ← 시맨틱 (위젯 파라미터)
onPressed: handleSubmit, // ← 콜백 (위젯 파라미터)
buttonStyle: CoreButtonStyle( // ← chrome 미세 override
paddingH: 24,
width: 200,
labelStyle: CoreTextStyle(
fontWeight: CoreFontWeight.semiBold,
),
leadingIconStyle: CoreIconStyle(size: 18),
),
leading: Icon(Icons.check),
child: Text('Submit'),
)
하위 시맨틱 변경 — asChild 패턴#
composite 컴포넌트 (CoSelect, CoDialog, CoPopover 등) 가 자기 trigger 의
시맨틱 (variant) 을 외부에서 조정해야 한다면, sub-component 의 Style 안에
packed 시키지 않고 위젯 자체를 슬롯으로 받습니다 (shadcn asChild
패턴).
// composite 의 sub-component variant 를 외부에서 결정
CoSelect(
trigger: CoButton(variant: .outline, child: Text('Select…')),
options: [...],
)
CoreSelectStyle.triggerStyle: CoreButtonStyle? 같은 nested 슬롯은 chrome
미세 조정용 입니다 — variant 는 들어가지 않습니다.
머지 체인 (Merge Chain)#
buttonStyle 은 다음 체인을 거쳐 최종 값으로 해석됩니다 (오른쪽이 이김):
design system default for variant
→ CoreButtonTheme.style // 프로젝트 공통
→ CoreButtonTheme.variantStyles[widget.variant] // variant 별
→ 부모 컴포넌트 슬롯 오버라이드 // 예: CorePopoverStyle.triggerStyle
→ widget.buttonStyle // 인스턴스별
각 레이어는 CoreButtonStyle 의 어떤 필드든 부분적으로 채울 수 있습니다.
nested slot styles (labelStyle / leadingIconStyle
/ trailingIconStyle) 은
null 이 아닌 경우 재귀 머지 됩니다 — labelStyle.fontWeight
만 override 해도
labelStyle.fontSize / labelStyle.color 는 이전 레이어 값 유지.
테마 적용#
ThemeData.fromCore(
coreComponentTheme: CoreComponentTheme(
button: CoreButtonTheme(
style: CoreButtonStyle(paddingH: 16), // 모든 버튼 공통
variantStyles: {
CoreButtonVariant.primary: CoreButtonStyle( // primary 만 더 큰 padding
paddingH: 24,
labelStyle: CoreTextStyle(
fontWeight: CoreFontWeight.semiBold,
),
),
},
animationDuration: AnimationConstants.short,
disableHoverEffect: false,
),
),
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child |
Widget / Component |
required | 버튼 본문 (label) |
onPressed |
VoidCallback? |
null |
클릭 핸들러. null 이면 비활성화 |
leading |
Widget? / Component? |
null |
텍스트 앞 아이콘/위젯 |
trailing |
Widget? / Component? |
null |
텍스트 뒤 아이콘/위젯 |
enabled |
bool? |
null |
활성화 여부 (onPressed != null 과 결합) |
expanded |
bool |
false |
부모 너비 채우기 |
variant |
CoreButtonVariant |
primary |
시맨틱 변형 (위젯 파라미터) |
size |
CoreComponentSize |
md |
크기 토큰 (위젯 파라미터) |
shape |
CoreButtonShape |
rectangle |
형태 (위젯 파라미터) |
buttonStyle |
CoreButtonStyle? |
null |
chrome / dimensional / nested slot 묶음 |
alignment |
AlignmentGeometry? |
null |
콘텐츠 정렬 |
marginAlignment |
AlignmentGeometry? |
null |
마진 정렬 |
onTap* / onLongPress* |
callback | null |
인터랙션 콜백 |
onHover / onFocus |
ValueChanged<bool>? |
null |
상태 변경 콜백 |
disableTransition |
bool |
false |
상태 전환 애니메이션 끄기 |
disableHoverEffect |
bool |
false |
hover 시각 효과 끄기 |
disableFocusOutline |
bool |
false |
포커스 아웃라인 끄기 |
enableFeedback |
bool? |
null |
햅틱 피드백 (Web no-op) |
dashedBorder |
bool |
false |
대시 보더 |
dashedBorderColor |
Color? / String? |
null |
대시 보더 색 |
focusNode |
FocusNode? |
null |
포커스 노드 |
statesController |
WidgetStatesController? |
null |
외부 상태 컨트롤러 |
CoreButtonStyle 필드 (chrome / dimensional / nested slot 만)#
| 필드 | 타입 | 설명 |
|---|---|---|
backgroundColor | Color? / String? | 배경색 |
foregroundColor |
Color? / String? |
텍스트/아이콘색 (foreground) |
borderColor | Color? / String? | 보더 색 |
borderWidth | double? | 보더 두께 (logical px) |
borderRadius | double? | 보더 반경 (logical px) |
height | double? | 고정 높이 |
width | double? | 고정 너비 |
minWidth | double? | 최소 너비 |
maxWidth | double? | 최대 너비 |
paddingH | double? | 수평 패딩 |
paddingV | double? | 수직 패딩 |
gap | double? | leading/trailing ↔ label 간격 |
labelStyle |
CoreTextStyle? |
label 텍스트 스타일 (fontSize/fontWeight/color 등) |
leadingIconStyle |
CoreIconStyle? |
leading 아이콘 스타일 (size/color) |
trailingIconStyle |
CoreIconStyle? |
trailing 아이콘 스타일 (size/color) |
variant / size / shape 는 위젯 파라미터로 들어가며 CoreButtonStyle
안에는 들어가지 않습니다.
변형 (Variants)#
Primary#
가장 강조되는 주요 액션에 사용합니다.
CoButton(variant: CoreButtonVariant.primary, onPressed: handleAction, child: Text('Primary'))
Secondary#
보조 액션에 사용합니다.
CoButton(variant: CoreButtonVariant.secondary, onPressed: handleAction, child: Text('Secondary'))
Ghost#
배경 없이 텍스트만 표시합니다. 덜 중요한 액션에 적합합니다.
CoButton(variant: CoreButtonVariant.ghost, onPressed: handleAction, child: Text('Ghost'))
Outline#
테두리만 있는 버튼입니다.
CoButton(variant: CoreButtonVariant.outline, onPressed: handleAction, child: Text('Outline'))
Destructive#
위험한 작업(삭제 등)에 사용합니다.
CoButton(variant: CoreButtonVariant.destructive, onPressed: handleDelete, child: Text('삭제'))
동작 스펙 (Behavior)#
상태 전환#
default→hover→pressed→defaultdisabled상태에서는 모든 인터랙션 무시, 시각적으로 흐리게 표시
포커스 관리#
- Tab 키로 포커스 이동 시 포커스 아웃라인 표시
- 포커스 상태에서 Enter/Space 로 활성화
disableFocusOutline: true로 포커스 아웃라인 제거 가능
애니메이션#
- hover / pressed 상태 전환:
CoreButtonTheme.animationDuration(기본 150 ms) disableTransition: true로 트랜지션 비활성화 가능CoreButtonTheme.disableHoverEffect: true로 hover 시각 효과 끔 (전역)
leading / trailing#
leading: 버튼 텍스트 앞에 아이콘/위젯 배치trailing: 버튼 텍스트 뒤에 아이콘/위젯 배치buttonStyle.leadingIconStyle/trailingIconStyle로 size/color 미세 조정
사용 가이드라인 (Usage Guidelines)#
✅ Do#
한 화면에 Primary 버튼은 하나만 사용하세요.
CoButton(variant: CoreButtonVariant.primary, onPressed: handleSave, child: Text('저장'))
CoButton(variant: CoreButtonVariant.outline, onPressed: handleCancel, child: Text('취소'))
Primary 는 가장 중요한 액션 하나에만 사용하고, 보조 액션은 outline 이나 ghost 를 사용합니다.
❌ Don't#
여러 Primary 버튼을 나란히 배치하지 마세요.
CoButton(variant: CoreButtonVariant.primary, onPressed: handleSave, child: Text('저장'))
CoButton(variant: CoreButtonVariant.primary, onPressed: handleCancel, child: Text('취소'))
사용자가 주요 액션을 구분할 수 없어 혼란을 야기합니다.
✅ Do#
버튼 레이블은 동작을 명확히 설명하세요.
CoButton(variant: CoreButtonVariant.destructive, onPressed: handleDelete, child: Text('계정 삭제'))
사용자가 버튼을 누르기 전에 결과를 예측할 수 있어야 합니다.
❌ Don't#
모호한 레이블을 사용하지 마세요.
CoButton(variant: CoreButtonVariant.destructive, onPressed: handleDelete, child: Text('확인'))
위험한 작업에 "확인" 만 쓰면 사용자가 결과를 예측할 수 없습니다.
접근성 (Accessibility)#
키보드 인터랙션#
| 키 | 동작 |
|---|---|
Enter | 버튼 활성화 (클릭과 동일) |
Space | 버튼 활성화 (클릭과 동일) |
Tab | 다음 포커스 가능 요소로 이동 |
Shift+Tab | 이전 포커스 가능 요소로 이동 |
스크린 리더#
-
Flutter:
Semantics로 버튼 역할과 레이블 자동 전달.enabled: false시 "비활성화" 상태 안내 - Web:
<button>요소 사용으로 네이티브 접근성 자동 적용
터치 타겟#
- 최소 터치 타겟 크기: 48×48 dp (Material 가이드라인)
마이그레이션 (Epic #1302)#
이전 버전에서 chrome / dimensional 필드는 위젯의 flat 파라미터였습니다. 이제
buttonStyle: CoreButtonStyle(...) 안으로 이동했습니다. variant / size /
shape 시맨틱 enum 은 위젯 파라미터로 그대로 유지 됩니다.
// Before — flat 필드
CoButton(
variant: CoreButtonVariant.primary,
size: CoreComponentSize.lg,
width: 200,
paddingH: 24,
textStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
iconSize: 18,
onPressed: () {},
leading: Icon(Icons.check),
child: Text('Submit'),
)
// After — variant/size 위젯 파라미터, chrome 만 buttonStyle
CoButton(
variant: CoreButtonVariant.primary, // ← 위젯 파라미터 그대로
size: CoreComponentSize.lg, // ← 위젯 파라미터 그대로
onPressed: () {},
buttonStyle: CoreButtonStyle(
width: 200,
paddingH: 24,
labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
leadingIconStyle: CoreIconStyle(size: 18),
),
leading: Icon(Icons.check),
child: Text('Submit'),
)
기존 외부 프로젝트 호환을 위한 deprecated 라벨은 제공하지 않으며, 각 사용처를 직접 새 API 로 옮겨주세요.