Floating Action Button
화면 위에 떠 있는 그 화면의 주 동작 하나 — 오른쪽 아래의 브랜드 원 56 에 흰 아이콘 24. 폰(768 미만)의 탭 바가 없는 화면(할 일 · 더치페이)에 둔다. 탭 바가 있는 화면의 추가는 Bottom Navigation 가운데 + 가, 데스크톱의 주 동작은 화면 머리의 Button 이 맡는다.
구조는 당근 SEED Floating Action Button(Apache-2.0)을 따른다 — "화면 상에 떠 있으며 주요 액션을 실행하는 버튼" 이고 "화면 내 주요 액션으로 하나만 존재할 수 있습니다". 오른쪽 아래 · 화면 끝과 아래 고정 요소에서 20 · 원 56 · 아이콘 24(SEED 의 아이콘만 모양 — extended=false). 값은 groo 토큰이고, SEED 와 다른 자리는 맨 아래 "SEED 와 다른 점" 에 적었다(2026-10-04 사용자 결정). groo 에 처음 두는 스펙이다 — 지금 Desk 웹 · 앱의 52 원(Fab · PFloatingActionButton)을 정리한다.
수치 원본은 floating-action-button.yaml이다. 수치 표 자리([표: …])와 그림 자리([그림: …])는 사이트가 그 파일로 그린다 — GitHub 에서는 그 파일 · 그림 코드로 가는 링크로 보인다.
직접 골라 보기
아이콘 · 아래 고정 요소(없음 · 바닥 버튼) · 안전 영역(홈 표시줄 있음 · 없음) · 스낵바를 고르면 스펙대로 그린 화면과 그 코드가 바뀐다. 목록을 스크롤해도 버튼은 그 자리에 있다.
import { Plus } from "lucide-react"
import { FloatingActionButton } from "@/components/ui/floating-action-button"
import { SnackbarAvoidOverlap } from "@/components/ui/snackbar"
<SnackbarAvoidOverlap>
<FloatingActionButton icon={<Plus />} aria-label="할 일 추가" onClick={openAddTodo} />
</SnackbarAvoidOverlap>Anatomy
| 부위 | 설명 |
|---|---|
| ⓐ Container | 원 56 — 브랜드 채움 · 그림자 s3. |
| ⓑ Icon | 흰 선 아이콘 24 — 보이는 글은 없다. |
부위
| 부위 | 설명 |
|---|---|
root | 버튼 — 원 56. 오른쪽 아래에 떠 있다 |
icon | 아이콘 — 흰 선 아이콘 24 |
focusRing | 키보드 포커스 링(웹) |
Properties
모양
원 56 · 브랜드 채움(bg-brand-solid — Desk 파랑 · HR 초록) · 흰 아이콘 24(static-white, 선 2.5) · 그림자 shadow-s3. 원 · 아이콘은 px 다 — 글자 크기를 따르지 않는다(SEED, 03-G 1A). 글을 붙이지 않는다 — 무엇을 하는 버튼인지는 아이콘과 이름(aria-label)이 말한다. 색을 바꾸지 않는다(SEED — "색상 변경이 불가능합니다").
버튼
| 속성 | 값 | 비고 |
|---|---|---|
| 크기 | 56px | 원 — 아이콘만(SEED extended=false). 누르는 영역도 56. AA ✓ · AAA ✓ |
| 모서리 | radius-full 9999px | |
| 배경 | color-bg-brand-solid HR #075B3C · Desk #0147AD | 브랜드 채움 — 화면의 주 동작 하나. 바꾸지 않는다(SEED) |
| 그림자 | shadow-s3 | 떠 있는 높이 — 목록 위에 뜬다(Elevation Local 2) |
| 자리 | 오른쪽 아래(fixed) | 스크롤해도 그 자리 — 숨거나 접히지 않는다 |
| 오른쪽 바깥 여백 | 20px | 화면 끝 → 버튼 20 + 오른쪽 안전 영역(SEED) |
| 아래 바깥 여백 | 20px | 화면 아래 끝(또는 바닥 고정 버튼 위 끝) → 버튼 20 + 아래 안전 영역(SEED) |
| z-index | 50 | z-sticky — specs/z-index.md L1. 스낵바(L6)는 이 위 8 에 뜬다 |
| 커서 | pointer | |
| 전환 대상 | background-color | |
| 전환 시간 | motion-duration-color-transition 150ms | |
| 전환 곡선 | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) |
아이콘
| 속성 | 값 | 비고 |
|---|---|---|
| 아이콘 | 24px | lucide 선 아이콘(plus 등) — 가운데 |
| 색 | color-static-white #FFFFFF | |
| 선 굵기 | 2.5px | 채운 원 위의 흰 아이콘 — 탭 바 가운데 + 와 같다 |
자리
오른쪽 아래다 — 화면 끝에서 20, 아래 끝에서 20. 아래에 고정된 것(바닥 버튼)이 있으면 그 위 끝에서 20 이다. 안전 영역이 있으면 그만큼 더 띄운다(Layout 의 Safe Area — 여백은 안전 영역 경계부터). 스크롤해도 그 자리에 있고 숨거나 접히지 않는다.
State
| 상태 | 모습 |
|---|---|
enabled | 브랜드 채움 |
hovered | 웹 — bg-brand-solid-pressed, 축소 없음 |
pressed | bg-brand-solid-pressed + 2px 거리 축소 |
focused | 웹 — 키보드 포커스에만 원 바깥 2px 링 |
상태
| 상태 | 배경 | 그림자 | 배율 | 축소 시간 | 축소 곡선 | 링 두께 | 링 간격 | 링 색 |
|---|---|---|---|---|---|---|---|---|
enabled 기본 | color-bg-brand-solid HR #075B3C · Desk #0147AD — 브랜드 채움 — 화면의 주 동작 하나. 바꾸지 않는다(SEED) | shadow-s3 — 떠 있는 높이 — 목록 위에 뜬다(Elevation Local 2) | — | — | — | — | — | — |
hovered 웹 — 누름과 같은 색, 축소 없음(v106) | color-bg-brand-solid-pressed HR #064F33 · Desk #013D96 | shadow-s3 — 떠 있는 높이 — 목록 위에 뜬다(Elevation Local 2) | — | — | — | — | — | — |
pressed 누름 — 누름 색 + 2px 거리 축소 | color-bg-brand-solid-pressed HR #064F33 · Desk #013D96 | shadow-s3 — 떠 있는 높이 — 목록 위에 뜬다(Elevation Local 2) | 2px 거리 축소 — 기준 56 → 0.964(SEED scaleScope: self · v104) — 모션 줄이기면 하지 않는다 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | — | — | — |
focused 웹 — 키보드 포커스에만 링 | color-bg-brand-solid HR #075B3C · Desk #0147AD — 브랜드 채움 — 화면의 주 동작 하나. 바꾸지 않는다(SEED) | shadow-s3 — 떠 있는 높이 — 목록 위에 뜬다(Elevation Local 2) | — | — | — | 2px | 2px — 원 바깥 — 화면(바탕) 위에 그린다 | color-stroke-focus-ring #1A1F2E · 다크 #F5F6FA |
모션
| 전환 | 시간 | 곡선 | 대상 | 비고 |
|---|---|---|---|---|
색 | motion-duration-color-transition 150ms | motion-ease-easing cubic-bezier(0.35, 0, 0.35, 1) | background-color | |
누름 | motion-duration-pressed-scale 150ms | motion-ease-pressed-scale cubic-bezier(0, 0, 0.15, 1) | scale | 2px 거리 축소 — 모션 줄이기면 하지 않는다 |
Guidelines
화면에 하나 — 주 동작만
떠 있는 버튼은 그 화면에서 가장 많이 하는 일 하나다(할 일 추가 · 더치페이 만들기). 화면에 둘 이상 두지 않고, 덜 중요한 동작을 떠 있게 하지 않는다 — 그런 동작은 상단 바 · 화면 안 버튼이다. 아이콘만으로 뜻이 통하는 동작에만 쓴다(추가 + 처럼).
탭 바 · 데스크톱과 겹치지 않게
| 화면 | 주 동작 |
|---|---|
| 폰 · 탭 바가 있는 화면(홈 · 가계부 · 캘린더 · 전체) | 탭 바 가운데 + — 떠 있는 버튼을 두지 않는다 |
| 폰 · 탭 바가 없는 화면(할 일 · 더치페이) | Floating Action Button |
| 데스크톱(768 이상) | 화면 머리의 Button(데스크톱 머리의 주 버튼 · 본문 제목 옆) — 떠 있는 버튼을 두지 않는다 |
스낵바는 위에
떠 있는 버튼이 있으면 Snackbar 는 그 위 8 에 뜬다(SEED — "Snackbar는 항상 Floating Action Button 위에 표시됩니다") — 버튼을 SnackbarAvoidOverlap 으로 감싼다. 목록의 마지막 줄이 버튼에 가리지 않게 목록 아래에 버튼 높이만큼 여백을 둔다(56 + 20 + 20).
이름
보이는 글이 없으니 이름(aria-label)이 곧 버튼의 글이다 — 동작 이름("할 일 추가" · "더치페이 만들기"). "추가" · "+" 처럼 무엇을 하는지 모를 이름을 쓰지 않는다.
두지 않는 것
- 글이 붙은 모양(Extended)과 스크롤에 따라 접기 — 늘 아이콘만 56 이다(사용자 결정 11B).
- 메뉴를 여는 떠 있는 버튼(스피드 다이얼) — HR 대시보드에 하나 있다. 앱 적용 때 화면과 함께 정한다.
- Contextual Floating Button(SEED — 조건부로 여럿 뜨는 보조 버튼) — 쓸 자리가 아직 없어 들이지 않는다.
코드
레시피 recipes/shadcn/components/ui/floating-action-button.tsx 를 쓴다. 아래 미리보기는 스펙 값으로 그린 모습이다.
FloatingActionButton— 원 56(<button type="button">).icon(lucide 아이콘) ·aria-label필수 ·offsetBottom(아래 고정 요소의 높이 — 기본 0, 그 위 20 에 선다). 자리(오른쪽 아래 · 20 · 안전 영역)는 스스로 잡는다. 그 밖은<button>속성.
앱은 같은 값을 Dart 로 둔다(PFloatingActionButton — 56 · tooltip 대신 Semantics(button: true, label)).
할 일 — 추가
import { Plus } from "lucide-react"
import { FloatingActionButton } from "@/components/ui/floating-action-button"
import { SnackbarAvoidOverlap } from "@/components/ui/snackbar"
<SnackbarAvoidOverlap>
<FloatingActionButton icon={<Plus />} aria-label="할 일 추가" onClick={openAddTodo} />
</SnackbarAvoidOverlap>Behavior
| 인터랙션 | 동작 |
|---|---|
| 누르기 | 그 화면의 주 동작(추가 시트 · 새 화면) |
| 스크롤 | 그 자리에 그대로 — 숨거나 접히지 않는다 |
| 스낵바가 뜸 | 버튼 위 8 에 뜬다 |
| 화면 키보드가 열림 | 버튼을 가린다 |
Tab | 화면 내용 다음 차례 — 마우스 호버에 누름 색 |
Accessibility
| 기준 | 검증 |
|---|---|
| WCAG 1.4.11 Non-text contrast(≥ 3:1) | 흰 아이콘 bg-brand-solid 위 Desk 8.38 · 다크 8.36 · HR 8.17 · 8.36 ✓. 원 면은 화면과 Desk 7.76 · 다크 1.96 · HR 7.56 · 1.96 — 다크에서는 흰 아이콘이 버튼을 알린다. 키보드 포커스 링 Desk 7.76 · 6.90 · HR 7.56 · 6.88 ✓ |
| WCAG 2.4.11 Focus Not Obscured — Minimum(AA) | 목록 아래에 버튼 높이만큼 여백을 둬 마지막 줄이 버튼 밑에 남지 않는다 ✓ |
| WCAG 2.5.8 Target Size — Minimum(AA ≥ 24×24) | 56 ✓ |
| WCAG 2.5.5 Target Size — Enhanced(AAA ≥ 44×44) | 56 ✓ |
| WCAG 4.1.2 Name, Role, Value | aria-label 필수 — 동작 이름("할 일 추가") ✓ |
| ARIA | <button type="button">(폼 안에서 제출되지 않게) · 아이콘 aria-hidden. 앱은 Semantics(button: true, label) |
Do / Don't
✅ Do
- 탭 바 없는 폰 화면의 주 동작 하나 — 오른쪽 아래.
- 화면 끝 · 아래 고정 요소에서 20 + 안전 영역.
- 이름은 동작 이름으로.
- 스낵바는 버튼 위에.
❌ Don't
- 글을 붙이거나 스크롤에 접기.
- 화면에 둘 이상 · 덜 중요한 동작 · 브랜드 아닌 색.
- 탭 바가 있는 화면 · 데스크톱에 떠 있는 버튼.
- 홈 표시줄에 걸친 자리(안전 영역 무시).
Specification
floating-action-button.yaml 의 규칙을 하나도 빼지 않고 조건마다 그린다 — 웹 · 앱이 Floating Action Button 을 만들 때 이 값을 그대로 쓴다. 조건이 없는 Base 가 모든 조합에 걸리고, 상태는 enabled 에서 바뀌는 값만 적었다.
Base
| 부위 | 상태 | 속성 | 값 |
|---|---|---|---|
root | enabled | 크기size | 56px원 — 아이콘만(SEED extended=false). 누르는 영역도 56. AA ✓ · AAA ✓ |
| 아래 바깥 여백marginBottom | 20px화면 아래 끝(또는 바닥 고정 버튼 위 끝) → 버튼 20 + 아래 안전 영역(SEED) | ||
| 오른쪽 바깥 여백marginRight | 20px화면 끝 → 버튼 20 + 오른쪽 안전 영역(SEED) | ||
| 모서리radius | radius-full9999px(알약) | ||
| 배경background | bg-brand-solidDesk#0147AD다크 #1049A4HR#075B3C다크 #1B583E브랜드 채움 — 화면의 주 동작 하나. 바꾸지 않는다(SEED) | ||
| 그림자shadow | shadow-s30 8px 24px -4px rgba(15, 18, 28, 0.10), 0 2px 6px -2px rgba(15, 18, 28, 0.05) · 다크 0 8px 24px -4px rgba(0, 0, 0, 0.50), 0 2px 6px -2px rgba(0, 0, 0, 0.25), inset 0 1px 0 0 rgba(255, 255, 255, 0.08)떠 있는 높이 — 목록 위에 뜬다(Elevation Local 2) | ||
| 커서cursor | pointer | ||
| z-indexzIndex | 50z-sticky — specs/z-index.md L1. 스낵바(L6)는 이 위 8 에 뜬다 | ||
| 전환 대상transitionProperty | background-color | ||
| 전환 시간transitionDuration | motion-duration-color-transition150ms | ||
| 전환 곡선transitionEasing | motion-ease-easingcubic-bezier(0.35, 0, 0.35, 1) | ||
| 자리position | 오른쪽 아래(fixed)스크롤해도 그 자리 — 숨거나 접히지 않는다 | ||
icon | enabled | 아이콘size | 24pxlucide 선 아이콘(plus 등) — 가운데 |
| 색color | static-white#FFFFFF | ||
| 선 굵기strokeWidth | 2.5px채운 원 위의 흰 아이콘 — 탭 바 가운데 + 와 같다 | ||
root | hovered | 배경background | bg-brand-solid-pressedDesk#013D96다크 #1A5AC2HR#064F33다크 #0C6C49 |
root | pressed | 배경background | bg-brand-solid-pressedDesk#013D96다크 #1A5AC2HR#064F33다크 #0C6C49 |
| 배율scale | 2px 거리 축소기준 56 → 0.964(SEED scaleScope: self · v104) — 모션 줄이기면 하지 않는다 | ||
| 축소 시간scaleDuration | motion-duration-pressed-scale150ms | ||
| 축소 곡선scaleEasing | motion-ease-pressed-scalecubic-bezier(0, 0, 0.15, 1) | ||
focusRing | focused | 링 두께width | 2px |
| 링 색color | stroke-focus-ringDesk#0147AD다크 #7AA9F6HR#075B3C다크 #2ABE84 | ||
| 링 간격offset | 2px원 바깥 — 화면(바탕) 위에 그린다 |
SEED 와 다른 점
- 아이콘만 56 하나 — SEED 코드 기본은 글이 붙은 48(
extended)이고 스크롤하면 라벨을 숨길 수 있다. groo 는 늘 아이콘만이다(사용자 결정 11B). - groo 브랜드 채움 — SEED 는 당근 주황 하나다. Desk 파랑 · HR 초록, 흰 아이콘(대비 8.38 · 8.17 — SEED 주황 위 흰색은 2.94).
- 그림자는 groo
shadow-s3— SEED shadow.s3 와 같은 자리(v105). - 누름 · 호버는
bg-brand-solid-pressed+ 2px 거리 축소(v104 · v106). type="button"— SEED 는type이 없어 폼 안에서 제출된다.- 여백 20 은 안전 영역 경계부터 — SEED 문서의 20(예제는 16)을 따르고, 안전 영역을 더한다(SEED Safe Area 그림).
- Menu 타입 · Contextual Floating Button 은 두지 않는다 — 쓸 자리가 아직 없다.
- z-index 는 specs/z-index.md 의 L1(
z-sticky50).
Migration notes
2026-10-21 — 단위(03-G 1A)
웹 size-14(rem)가 글자 200% 에서 112 가 됐다 — 레시피 size-[56px] · 아이콘 size-[24px]. 비교 페이지 https://claude.ai/artifact/A3oCdxNvDwkNBPFPE36bmF.
2026-10-04 — SEED Floating Action Button(아이콘만)으로 새로 둔다
사용자가 화면 틀 · 이동 비교 페이지에서 정했다 — SEED 아이콘만 56(11B: 원 56 · 아이콘 24 · bg-brand-solid · 흰 아이콘 · 화면에 하나 · 오른쪽 아래, 화면 끝 · 아래 고정 요소에서 20 + 안전 영역 · 이름은 aria-label). 글이 붙은 Extended · 스크롤 접힘(11A)과 지금 52(11C)는 고르지 않았다. 탭 바 가운데 + 를 떠 있는 버튼으로 빼는 안(6C)도 고르지 않았다.
제품은 앱 적용 단계에서 옮긴다(2026-10-04 조사).
- Desk 웹 —
shared/ui/groo/fab.tsx:12-33(할 일TodoPage.tsx:734· 더치페이DutchPayPage.tsx:576, 폰 전체 화면). 52 원 · + 22 ·shadow-lg· 아래 24 · 오른쪽 18 고정 · z 20 이라 홈 표시줄(34)과 10 겹친다(fab.tsx:22, F23). 56 · 24 · 20 + 안전 영역 · z-sticky 로. - Desk 앱 —
shared/widgets/p_floating_action_button.dart:10-47(할 일todo_screen.dart:272· 더치페이dutch_pay_screen.dart:60). 52 · elevation 6 · 아이콘 22 · 오른쪽 16 · 아래 16 + 안전 영역, 이름은 tooltip 뿐이고 버튼 표시가 없다(PButton처럼 Semantics 가 없다). - HR 대시보드 SpeedDial —
shared/ui/speed-dial/SpeedDial.tsx:45·DashboardContent.tsx:244(아래 32 · 오른쪽 32 · z 50, 메뉴 조사 M7). 메뉴를 여는 떠 있는 버튼이라 이 스펙 밖이다 — 앱 적용 때 정한다. - 쓰지 않는 것 — 앱
PSpeedDial(호출 0, F27). 지운다(2026-10-17 사용자 결정 — 03-F 결정 5A).
Field
입력 하나를 감싸는 둘레 — 칸 이름(라벨) · 필수 또는 선택 표시 · 설명 · 오류 · 글자 수를 한 모양으로 붙인다. Text Input · Textarea 와 Checkbox · Radio · Select Box 묶음을 감싼다.
Footer
사이트 맨 아래의 바닥 — 브랜드 · 사이트맵(탐색 영역)과 정책 링크 · 안내 글 · 저작권(안내 영역)을 블록으로 골라 조립한다. 지금은 groo-home(hangroo.io) 맨 아래 하나이고, 나중에 SSO 로그인 · 가입 아래와 설명서 · 문서 사이트의 바닥이 같은 블록을 쓴다. Desk · HR 의 제품 화면에는 두지 않는다 — 정책 링크는 설정 목록의 줄(List)이다.