FAB
화면의 주요 액션을 강조하는 플로팅 액션 버튼 컴포넌트
목차
화면의 주요 액션을 강조하는 플로팅 액션 버튼(Floating Action Button)입니다. Fab은 단독 버튼으로도, label을 주면 라벨이 붙은
Extended 알약으로도, 탭하면 액션 메뉴가 열리는 트리거로도 사용할 수 있습니다 — 열린 메뉴는 Popup 패널 안에 MenuItem
행이 하나씩 놓인 카드이고, 패널은 FAB 의 끝 가장자리(오른쪽 아래 FAB 이면 오른쪽 끝)에 맞춰 열립니다. Flutter와 Web이 동일한 Fab
API를 공유합니다.
기본 사용법 (Basic Usage)#
// 기본 FAB
Fab(
trigger: const Icon(LucideIcons.plus),
onPressed: handleCreate,
)
// 소형 FAB
Fab(
trigger: const Icon(LucideIcons.plus),
size: CoreFabSize.mini,
onPressed: handleCreate,
)
// Extended FAB (라벨이 붙은 알약 — 데스크톱 기본형)
Fab(
trigger: const Icon(LucideIcons.plus),
label: const Text('New note'),
onPressed: handleCreate,
)
// 메뉴 FAB (탭하면 액션 메뉴가 열림)
Fab(
trigger: const Icon(LucideIcons.plus),
actions: [
FabAction(
icon: const Icon(LucideIcons.pencil),
label: const Text('Edit'),
onPressed: handleEdit,
),
FabAction(
icon: const Icon(LucideIcons.share2),
label: const Text('Share'),
onPressed: handleShare,
),
],
)
// 기본 FAB
Fab(
trigger: const Icon(LucideIcons.plus),
onPressed: handleCreate,
)
// 소형 FAB
Fab(
trigger: const Icon(LucideIcons.plus),
size: CoreFabSize.mini,
onPressed: handleCreate,
)
// Extended FAB (라벨이 붙은 알약 — 데스크톱 기본형)
Fab(
trigger: const Icon(LucideIcons.plus),
label: Text('New note'),
onPressed: handleCreate,
)
// 메뉴 FAB
Fab(
trigger: const Icon(LucideIcons.plus),
actions: [
FabAction(
icon: const Icon(LucideIcons.pencil),
label: const Text('Edit'),
onPressed: handleEdit,
),
FabAction(
icon: const Icon(LucideIcons.share2),
label: const Text('Share'),
onPressed: handleShare,
),
],
)
빠른 오버라이드 (Chain)#
이미 만든 Fab 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class FabChainExample extends StatelessWidget {
const FabChainExample({super.key});
@override
Widget build(BuildContext context) {
return Fab(
trigger: const Icon(LucideIcons.plus),
onPressed: () {},
expandDirection: CoreFabExpandDirection.down,
actions: [
FabAction(
icon: const Icon(LucideIcons.pencil),
label: const Text('Edit'),
),
FabAction(
icon: const Icon(LucideIcons.share2),
label: const Text('Share'),
),
],
).withStyle(
const CoreFabStyle(
diameter: CoreSize.size72,
iconStyle: CoreIconStyle(
size: CoreIconSize.size32,
color: CoreColor.token(CoreColors.onPrimary),
),
popupStyle: CorePopupStyle(
backgroundColor: CoreColor.token(CoreColors.primaryContainer),
borderRadius: CoreBorderRadius.all(CoreRadius.radius16),
),
spacing: CoreSpace.space24,
),
);
}
}
class FabChainExample extends StatelessComponent {
const FabChainExample({super.key});
@override
Component build(BuildContext context) {
return Fab(
trigger: const Icon(LucideIcons.plus),
onPressed: () {},
expandDirection: CoreFabExpandDirection.down,
actions: [
FabAction(
icon: const Icon(LucideIcons.pencil),
label: Text('Edit'),
),
FabAction(
icon: const Icon(LucideIcons.share2),
label: Text('Share'),
),
],
).withStyle(
const CoreFabStyle(
diameter: CoreSize.size72,
iconStyle: CoreIconStyle(
size: CoreIconSize.size32,
color: CoreColor.token(CoreColors.onPrimary),
),
popupStyle: CorePopupStyle(
backgroundColor: CoreColor.token(CoreColors.primaryContainer),
borderRadius: CoreBorderRadius.all(CoreRadius.radius16),
),
spacing: CoreSpace.space24,
),
);
}
}
Live Preview#
코드 보기
class FabDefaultExample extends StatelessComponent {
const FabDefaultExample({super.key});
@override
Component build(BuildContext context) {
return Fab(
trigger: const Icon(LucideIcons.plus),
onPressed: () {},
expandDirection: CoreFabExpandDirection.down,
actions: [
FabAction(
icon: const Icon(LucideIcons.pencil),
label: Text('Edit'),
),
FabAction(
icon: const Icon(LucideIcons.share2),
label: Text('Share'),
),
],
);
}
}
class FabDefaultExample extends StatelessWidget {
const FabDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return Fab(
trigger: const Icon(LucideIcons.plus),
onPressed: () {},
expandDirection: CoreFabExpandDirection.down,
actions: [
FabAction(
icon: const Icon(LucideIcons.pencil),
label: const Text('Edit'),
),
FabAction(
icon: const Icon(LucideIcons.share2),
label: const Text('Share'),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 화면 내에서 가장 중요한 단일 액션을 명확히 강조하고 싶을 때 (새 항목 만들기, 작성, 공유)
- 스크롤해도 항상 접근 가능해야 하는 주요 CTA(Call to Action)가 필요할 때
- 하나의 트리거에서 여러 관련 액션을 메뉴로 열어 보여주고 싶을 때
대신 다른 컴포넌트를 사용하세요:
Props / Parameters#
W 는 Flutter의 Widget, Web의 Jaspr Component 입니다. W?
는 선택 슬롯입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
trigger | W | 필수 | 메인 FAB 내부에 표시할 콘텐츠 (보통 아이콘) |
label |
W? |
null |
값을 주면
Extended
FAB(원이 아니라 알약)이 된다.
trigger
는 leading 아이콘 자리로 옮겨간다.
actions
와 동시에 지정할 수 없다
|
onPressed |
VoidCallback? |
null |
메인 FAB 클릭 핸들러. actions가 있으면 메뉴 토글로 동작 |
actions |
List<FabAction>? |
null |
메뉴 행이 되는 액션 목록 |
open |
bool |
false |
메뉴 열림 상태 (controlled 모드) |
onOpenChanged |
void Function(bool)? |
null |
메뉴 열림 상태 변경 콜백 |
size |
CoreFabSize |
CoreFabSize.regular |
FAB 크기 (mini / regular / large) |
expandDirection |
CoreFabExpandDirection |
CoreFabExpandDirection.up |
메뉴가 열리는 방향 (
up
/
down
/
left
/
right
/
fan
—
fan
은
up
과 같다)
|
closeIcon |
W? |
null |
메뉴 열림 시 표시할 아이콘 (기본값: X 아이콘) |
tooltip |
String? |
null |
메인 FAB 툴팁 텍스트 |
fabStyle |
CoreFabStyle? |
null |
지름 / 간격 / 애니메이션 + nested
buttonStyle
·
actionItemStyle
·
iconStyle
·
popupStyle
·
popoverStyle
등 모든 chrome 단일 진입점
|
FabAction#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
icon | W | 필수 | 메뉴 행의 leading 아이콘 |
label | W? | null | 메뉴 행의 라벨 |
onPressed |
VoidCallback? |
null |
액션 클릭 핸들러 |
tooltip |
String? |
null |
액션 툴팁 텍스트 |
스타일 시스템 (Style System)#
CoreFabStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
buttonStyle |
CoreButtonStyle? |
Main FAB circle chrome override — raw-forwarded to the composed
Button
. Carries background / foreground / shadow / radius / focus ring.
null
(or any unset field) defers to [defaultButtonStyle]. Three fields are owned elsewhere and restated after this slot, so a value here does not reach the circle:
height
/
width
belong to [diameter] (the circle needs them equal), and the glyph size in
leadingIconStyle
belongs to [iconStyle]. Every other
leadingIconStyle
sub-field (a colour, say) is merged on top of [iconStyle] and reaches the glyph.
|
extendedButtonStyle |
CoreButtonStyle? |
Extended
(라벨을 가진) FAB 의 원 chrome — 합성되는
Button
으로 raw-forward 된다. Extended 는 원이 아니라 알약이라 폭이 라벨을 따라간다. 높이는 원형과 같은 [diameter] 이고, 좌우 패딩과 아이콘–라벨 간격만 사이즈별로 다르다([defaultsBySize]) — 라벨 크기는
CoreFabSize.toExtendedButtonSize
가 고른
Button
사이즈에서 상속받으므로 여기서 다시 진술하지 않는다.
null
(또는 설정되지 않은 필드)이면 [defaultsBySize] 의 해당 사이즈 값으로, 그다음 [defaultButtonStyle] 로 내려간다.
|
actionItemStyle |
CoreMenuItemStyle? |
Action-row chrome override — raw-forwarded to each composed
MenuItem
row.
null
(or any unset field) defers to [defaultActionItemStyle], which states only the hosted corner (8): otherwise the row is the
MenuItem
at
CoreFabContract.actionItemSize
as it draws itself. The row's leading icon, label typography, insets, gap and corner are this slot's
leadingIconStyle
/
labelStyle
/
padding
/
contentGapStyle
/
borderRadius
— the
MenuItem
's own fields, not a Fab-side restatement.
|
actionButtonStyle |
CoreButtonStyle? |
Deprecated (since 0.164)
— Use actionItemStyle — the action rows are MenuItem rows now, so their chrome is a CoreMenuItemStyle. The action-row chrome of the
Button
rows. Still honoured: [effectiveActionItemStyle] folds the fields a
MenuItem
row also has —
padding
,
borderRadius
,
iconLabelGapStyle
(→
contentGapStyle
),
height
(→
minHeight
),
leadingIconStyle
,
labelStyle
,
backgroundColor
,
foregroundColor
,
hoverBackgroundColor
— under [actionItemStyle], which wins field by field. Button-only fields (shadow, border, focus outline, …) have no row counterpart and are dropped.
|
actionLabelTextStyle |
CoreTextStyle? |
Deprecated (since 0.164)
— Use actionItemStyle.labelStyle — the label belongs to the composed MenuItem row. The action-row label style of the
Button
rows. Still honoured as the row's
labelStyle
under [actionItemStyle] (see [effectiveActionItemStyle]).
|
actionIconStyle |
CoreIconStyle? |
Deprecated (since 0.164)
— Use actionItemStyle.leadingIconStyle — the glyph belongs to the composed MenuItem row. The action-row leading icon style of the
Button
rows. Still honoured as the row's
leadingIconStyle
under [actionItemStyle] (see [effectiveActionItemStyle]).
|
diameter |
double? |
Main FAB circular diameter override (logical px).
null
defers to [defaultsBySize] for the active size. Forwarded to the composed
Button
's
width
/
height
— the single entry point for both, so a
height
/
width
in [buttonStyle] is overridden by this.
|
iconStyle |
CoreIconStyle? |
Main FAB icon style override — styles the icon glyph the FAB passes as the composed
Button
's child (its
size
is the only source of the glyph size; a
color
pins the glyph colour).
null
defers to [defaultsBySize] for the active size. A
leadingIconStyle
stated in [buttonStyle] merges on top of this for every field except
size
.
|
spacing |
double? |
Gap between the main FAB and the menu panel (logical px) override. Pure placement-offset scalar — never consumed as a flex gap by
Gap
/ CSS
gap
.
|
duration |
Duration? |
Menu open / close animation duration override — the panel's own open / close motion, unless [popupStyle] pins one. |
popoverStyle |
CorePopoverStyle? |
Overlay host chrome override. Nested [CorePopoverStyle] raw-forwarded to the composed
Popover
that hosts the menu (child-component-composition).
null
(or any unset field) defers to [defaultPopoverStyle] — chrome-less, because the visible box is the
Popup
in [popupStyle]. Override e.g. to tune the placement offset.
|
popupStyle |
CorePopupStyle? |
Menu panel chrome override. Nested [CorePopupStyle] raw-forwarded to the composed
Popup
that holds the action rows.
null
(or any unset field) defers to [defaultPopupStyle] — the Fab's own card.
|
크기 (Sizes)#
| 크기 | 값 | 지름(원형) | 아이콘 | Extended 높이 | Extended 좌우 패딩 |
|---|---|---|---|---|---|
| Mini | CoreFabSize.mini |
48px | 24px | 48px | 20px |
| Regular | CoreFabSize.regular |
56px | 24px | 56px | 24px |
| Large | CoreFabSize.large |
72px | 28px | 72px | 28px |
Mini 가 48 인 것은 터치 타겟 하한이 48 이기 때문입니다 — FAB 은 그 하한을 깨면서까지 작아질 이유가 없습니다.
Extended 는 높이가 원형과 같고 폭만 라벨을 따라 늘어납니다. 라벨 크기는 그 높이에 대응하는 Button
사이즈(lg / xl / xl2)에서 상속받으므로, 같은 높이의 Button 과 라벨이 갈리지 않습니다.
동작 스펙 (Behavior)#
인터랙션#
-
클릭/탭:
actions가 없으면onPressed콜백 실행.actions가 있으면 speed-dial을 펼치거나 접음 - speed-dial 액션 클릭: 해당 액션의
onPressed실행 후 speed-dial 자동 닫힘 - 호버: 커서가 포인터로 전환
- 포커스: 포커스 링 표시로 키보드 탐색 가능 상태 표시
Controlled / Uncontrolled#
onOpenChanged를 제공하지 않으면Fab이 내부 상태로 speed-dial 열림/닫힘을 관리합니다 (uncontrolled)onOpenChanged를 제공하면open값으로 외부에서 상태를 제어합니다 (controlled)
애니메이션#
-
메뉴는
Popover의 등장/퇴장 모션과fabStyle의 nested popover / popup 스타일을 사용합니다. 현재 액션별 stagger나 fan 원형 배열은 없습니다 - expandDirection은 패널이 열릴 위치이며, 내부 행은 주어진 순서로 세로 배치합니다. fan은 up과 같은 위치입니다
메뉴 너비#
-
패널 너비는 내용을 따르되 최소 160(
layout/160) · 최대 280(layout/280) 사이로 묶이고, 행은 패널 너비를 채웁니다 - 280 을 넘는 라벨은 줄바꿈하지 않고 한 줄로 말줄임(…)합니다 — 행 높이는 48 그대로입니다
패널 여백과 행 모서리#
-
패널 안쪽 여백은 6 으로,
Popup의 목록 여백(defaultListPadding)입니다. Fab 이 여는 것은 행동 목록이라Menu와 같은 여백을 씁니다. 패널 반경은Popup의 패널 역할 값 12 입니다 -
행 모서리는 8 입니다. 같은 판 안의 목록 행(
ItemPicker·Autocomplete·PhoneInput·OptionItem)이 모두 8 이라 Fab 도 같은 값을 씁니다.MenuItemlg 가 혼자 설 때의 12 는 쓰지 않습니다
옛 행 스타일 필드 (Deprecated)#
메뉴 행이 Button(variant: .menu) 에서 MenuItem 으로 바뀌면서 행 chrome 의 진입점이 actionItemStyle(CoreMenuItemStyle) 하나가 됐습니다. 옛 필드는 0.164 부터
@Deprecated 이고 아직 동작합니다 — MenuItem 에 같은 뜻의 필드가 있으면 그쪽으로 옮겨 적용하고, 같은 필드를 actionItemStyle
이 말하면 actionItemStyle 이 이깁니다.
| 옛 | 새 |
|---|---|
actionButtonStyle |
actionItemStyle
(
padding
·
borderRadius
·
iconLabelGapStyle
→
contentGapStyle
·
height
→
minHeight
·
leadingIconStyle
·
labelStyle
· 배경 · 전경 · hover 배경만 옮겨지고, 그림자 · 테두리 · 포커스 아웃라인 같은 Button 전용 필드는 버려집니다)
|
actionIconStyle | actionItemStyle.leadingIconStyle |
actionLabelTextStyle | actionItemStyle.labelStyle |
CoreFabContract.actionButtonSize |
CoreFabContract.actionItemSize (CoreMenuItemSize.lg) |
사용 가이드라인 (Usage Guidelines)#
✅ Do#
한 화면에 하나의 FAB만 사용
Fab(
trigger: const Icon(LucideIcons.plus),
tooltip: '새 노트 작성',
onPressed: handleCreateNote,
)
FAB은 화면에서 가장 중요한 단일 액션을 나타내야 합니다. 여러 개를 사용하면 사용자가 어느 것이 주요 액션인지 혼란스러워집니다. 이건 취향이 아니라 이 컴포넌트의 정의입니다 — 두 번째 FAB 을 놓고 싶어지는 순간은 그중 하나가 FAB 이 아니어야 한다는 신호이므로, 덜 중요한 쪽을 Button 으로 내리거나 둘을 하나의 메뉴 FAB(actions)으로 묶으세요.
✅ Do#
데스크톱에서는 Extended 를 쓴다
// 데스크톱 — 라벨이 붙은 알약
Fab(
trigger: const Icon(LucideIcons.plus),
label: const Text('New note'),
onPressed: handleCreate,
)
포인터로 쓰는 넓은 화면에서 아이콘 하나만 떠 있으면 그 액션이 무엇인지 호버하기 전까지 알 수 없습니다. 툴팁은 알아내는 비용을 미룰 뿐 없애지 못합니다 — 가로 공간이 충분한 화면에서는 라벨을 직접 보여주는 편이 항상 낫습니다. 원형 FAB 은 라벨을 놓을 자리가 없는 좁은 화면의 형태입니다.
❌ Don't#
FAB 을 비활성화하지 않는다 — 숨긴다
// ❌ 쓸 수 없는 FAB 을 회색으로 띄워둔다
Fab(
trigger: const Icon(LucideIcons.plus),
onPressed: null,
)
// ✅ 쓸 수 없으면 자리에서 뺀다
if (canCreate)
Fab(
trigger: const Icon(LucideIcons.plus),
onPressed: handleCreate,
)
FAB 은 콘텐츠 위에 떠서 화면의 주요 액션 하나를 주장하는 형태입니다. 그 주장이 지금 유효하지 않다면 회색으로 남겨 자리를 차지할 이유가 없습니다 — 비활성 FAB 은 "여기 무언가 있는데 왜 안 되는지는 말하지 않겠다" 로 읽히고, 그 이유를 담을 자리가 FAB 에는 없습니다. 이유를 보여줘야 하는 상황이라면 그건 FAB 이 아니라 인라인 Button + 안내 문구의 일입니다.
❌ Don't#
파괴적인 액션에 FAB 사용 금지
// ❌ 삭제(파괴적) 액션에 FAB 사용
Fab(
trigger: const Icon(LucideIcons.trash2),
onPressed: handleDeleteAll,
)
FAB은 눈에 잘 띄기 때문에 실수로 클릭하기 쉽습니다. 삭제 같은 파괴적인 액션에는 확인 다이얼로그가 있는 일반 버튼을 사용하세요.
✅ Do#
speed-dial로 관련 액션을 묶어 제공
Fab(
trigger: const Icon(LucideIcons.plus),
actions: [
FabAction(
icon: const Icon(LucideIcons.pencil),
label: const Text('Edit'),
onPressed: handleEdit,
),
FabAction(
icon: const Icon(LucideIcons.share2),
label: const Text('Share'),
onPressed: handleShare,
),
],
)
하나의 트리거에서 관련된 보조 액션을 펼쳐 보여주면 화면을 깔끔하게 유지할 수 있습니다.
접근성 (Accessibility)#
역할#
| 플랫폼 | 메인 FAB | speed-dial 패널 |
|---|---|---|
| Flutter |
CoUISemantics(expanded: …)
로 감싼
Button
—
Button
은
Clickable
을 통해
Semantics(button: true, enabled: …)
를 얹습니다
|
역할 없음.
Popover
를
modal: false
로 합성하므로 배리어도
BlockSemantics
도 만들지 않습니다
|
| Web |
native
<button type="button">
.
tooltip
이 있으면
aria-label
,
actions
가 있으면
aria-expanded
|
Popover
가 트리거 래퍼
<div>
에
aria-haspopup="dialog"
+
aria-expanded
를, 패널 버블에
role="dialog"
를 얹습니다
|
expanded / aria-expanded 는 actions 가 비어 있지 않을 때만 붙습니다 — 아무것도 열지 않는 버튼에
false 를 붙이면 "접힘, 열려면 활성화" 로 읽히기 때문입니다. 액션 행은 양쪽 다 MenuItem
이고
(Web 은 role="menuitem"), Web 은 action.tooltip 이 있을 때만
aria-label 을 받습니다.
Fab 에는 enabled 파라미터가 없습니다. onPressed 를 null 로 두면 합성된
Button 이 비활성으로
내려가 Web 에서 disabled + aria-disabled="true" + tabindex="-1"
이 됩니다.
키보드#
| 키 | 동작 |
|---|---|
Tab / Shift+Tab | 메인 FAB 로 이동 / 이탈 |
Enter · Space |
메인 FAB 활성화 — actions 가 있으면 speed-dial 토글, 없으면 onPressed |
Enter · Space (액션 행 위에서) | 그 액션 실행 후 speed-dial 닫기 |
Escape | Web 에서만 speed-dial 닫기 |
액션 사이를 오가는 화살표 키는 구현되어 있지 않습니다. Flutter 는 Clickable 의 활성화 단축키
(SingleActivator(.enter) / SingleActivator(.space)
→ ActivateIntent)로, Web 은 native <button> 의
브라우저 기본 동작으로 활성화됩니다.
스크린 리더#
-
Extended FAB 는
label텍스트가 이름이 됩니다. 아이콘 전용 FAB 의 이름·힌트 경로는tooltip입니다 —trigger슬롯에 넣는Icon은 어느 플랫폼에서도 이름을 만들지 않습니다(Flutter 는SvgPicture.string을semanticsLabel없이, Web 은 텍스트 없는<iconify-icon>을 렌더합니다). - Web 의
tooltip은aria-label로 나가 접근 가능한 이름 그 자체가 됩니다. -
Flutter 의
tooltip은Tooltip이Semantics(tooltip: message)로 얹어 이름이 아니라 힌트입니다. -
액션은
label슬롯을 채우면 그 텍스트가 행 안에 렌더되어 읽히고, 아이콘만 넣으면 Web 은action.tooltip→aria-label, Flutter 는Tooltip힌트로만 전달됩니다. - 열림/닫힘은
expanded/aria-expanded로 통보되며, 라이브 리전은 어느 쪽에도 없습니다.
포커스 관리#
-
진입: speed-dial 이 열려도 포커스는 메인 FAB 에 남습니다. Flutter 는
modal: false라 전용FocusScopeNode를 만들지 않고requestFocus()도 modal 일 때만 실행하며, WebPopover는focus()를 호출하는 경로가 없습니다. - 이탈: 닫을 때 트리거로 되돌리는 로직이 없습니다 — 애초에 포커스가 옮겨가지 않았으므로 메인 FAB 에 그대로 있습니다.
- 트랩: 없습니다. Flutter 는 modal 배리어를 만들지 않아 배경이 시맨틱 트리에 남고, Web 도 패널이 Tab 을 가두지 않습니다.
-
패널은 양 플랫폼 모두 오버레이 레이어에 그려집니다(Flutter
Overlay, Web.popoverportal). 그래서 액션 행이 트리거 바로 다음 tab 순서에 오지 않습니다 — 메인 FAB 에서Tab을 누르면 액션이 아니라 트리거 다음 요소로 갑니다. -
포커스 표시: 메인 FAB(원형 · extended 모두)의 링은 링과 FAB 사이 2px 간격을
surface밴드로 칠합니다. FAB 는 스크롤하는 콘텐츠 위에 떠 있어 링이 어떤 색과 맞닿을지 알 수 없으므로, 링 + 밴드의 두 색 인디케이터로 어느 배경에서도 대비를 확보합니다. 기본값은CoreFabStyle.defaultButtonStyle의focusOutlineStyle.offsetColor에 있고, 다른Button의 간격은 투명입니다.
알려진 제약#
-
Flutter 에서
Escape로 speed-dial 을 닫을 수 없습니다.Popover는Escape를modal && consumeOutsideTaps일 때만 배선하는데Fab이modal을 넘기지 않아 기본값false로 남기 때문입니다. Web 은 document keydown 리스너라 포커스 위치와 무관하게 닫힙니다. 대안: controlledopen+onOpenChanged로 열림 상태를 호출부가 소유하고, 호출부가 자체 키 처리로false를 씁니다. -
아이콘만 넣은 FAB 는 Flutter 에서 이름 없이 "버튼" 으로만 읽힙니다.
tooltip은 힌트라 이름을 대신하지 못하고,trigger/action.icon은 컴포넌트가 그대로 렌더하므로 안쪽에 이름을 심을 자리가 없습니다. 대안: 호출부가 Flutter 쪽에서Semantics(label: …, child: Fab(…))로 감쌉니다. Web 은tooltip만 주면 해결됩니다. -
Web 의 speed-dial 이 다이얼로그로 안내됩니다. 합성된
Popover가 패널에role="dialog"를, 트리거에aria-haspopup="dialog"를 고정으로 얹습니다. 액션 목록에는 메뉴 계열 역할이 더 맞지만Fab이 이 두 값을 노출하지 않아 대안이 없습니다.
주 버튼의 크기 사다리는 다음과 같습니다:
- Mini: 48px (중요도 낮은 보조 액션에만 사용 — 48 은 터치 타겟 하한이므로 그 아래로 줄이지 않습니다)
- Regular: 56px (기본, 권장)
- Large: 72px (특별히 강조가 필요한 경우)
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Fab | Fab |
| 클릭 핸들러 | onPressed | onPressed |
| elevation | CoreShadow 토큰 → BoxShadow |
CoreShadow 토큰 → CSS box-shadow |
| speed-dial 토글 | controlled open 상태 + 애니메이션 |
controlled open 상태 + CSS transition |
| 메뉴 행 | MenuItem (lg, 48px) |
MenuItem (lg, 48px, role="menuitem") |
| 메뉴 패널 정렬 | FAB 끝 가장자리 (up · down) | 같음 |
메뉴 패널의 끝 정렬은 물리 오른쪽입니다(topEnd / bottomEnd). 오른쪽에서 왼쪽으로 쓰는 로케일에서도
패널은 뒤집히지 않고 FAB 의 오른쪽 끝에 맞춥니다 — 배치 어휘가 방향을 모릅니다. left · right
는 그대로 가운데 정렬입니다.