Icon
CoUI 디자인 시스템의 아이콘 컴포넌트
목차
CoUI 디자인 시스템의 통일된 아이콘 컴포넌트입니다. Iconify 생태계(Lucide, Radix, Bootstrap, HeroIcons) 아이콘을 IconName
참조로 렌더합니다. Flutter와 Web이 동일한 API와 동일한 아이콘 레퍼런스를 사용합니다.
앱은 네 카탈로그를 자유롭게 쓸 수 있지만, CoUI 자신의 컴포넌트는
LucideIcons하나만 씁니다. 각 세트는 자기 그리드와 자기 획 두께로 그려져, 같은size를 요청해도 시각적 무게가 다르게 도착하기 때문입니다 — 한 벌에서 온 체크와 다른 벌에서 온 chevron 이 나란히 있으면 선택이 아니라 렌더 버그로 읽힙니다. 한 앱 안에서 섞을 때도 같은 이유로 인접한 컨트롤끼리는 한 세트로 맞추는 편이 좋습니다.
기본 사용법 (Basic Usage)#
Icon의 모든 시각 커스터마이징은 iconStyle: CoreIconStyle(...)로 단일화되어 있습니다.
// 기본 아이콘 (디자인 시스템 default — 24px / onSurface)
const Icon(LucideIcons.house);
// 크기 지정
const Icon(
LucideIcons.house,
iconStyle: CoreIconStyle(size: 28),
);
// 다른 아이콘 세트
const Icon(RadixIcons.chevronLeft, iconStyle: CoreIconStyle(size: 16));
const Icon(BootstrapIcons.gear, iconStyle: CoreIconStyle(size: 20));
// HeroIcons 스타일 선택
const Icon(HeroIcons.arrowLeft.outline, iconStyle: CoreIconStyle(size: 24));
const Icon(HeroIcons.arrowLeft.solid, iconStyle: CoreIconStyle(size: 24));
// 크기 + 색상
Icon(
LucideIcons.heart,
iconStyle: CoreIconStyle(
size: 28,
color: Theme.of(context).colorScheme.tertiary,
),
);
// 같은 스타일을 여러 인스턴스에 재사용
final dangerIcon = CoreIconStyle(
size: 20,
color: Theme.of(context).colorScheme.error,
);
Icon(LucideIcons.alertTriangle, iconStyle: dangerIcon);
Icon(LucideIcons.x, iconStyle: dangerIcon);
// 기본 아이콘
const Icon(LucideIcons.house);
// 크기 지정
const Icon(
LucideIcons.house,
iconStyle: CoreIconStyle(size: 28),
);
// 다른 아이콘 세트
const Icon(RadixIcons.chevronLeft, iconStyle: CoreIconStyle(size: 16));
const Icon(BootstrapIcons.gear, iconStyle: CoreIconStyle(size: 20));
// HeroIcons 스타일 선택
const Icon(HeroIcons.arrowLeft.outline, iconStyle: CoreIconStyle(size: 24));
const Icon(HeroIcons.arrowLeft.solid, iconStyle: CoreIconStyle(size: 24));
// 크기 + 색상 — Web 의 color 는 Tailwind colour token 문자열
Icon(
LucideIcons.heart,
iconStyle: CoreIconStyle(
size: 28,
color: context.colorScheme.tertiary, // 'tertiary' 토큰
),
);
빠른 오버라이드 (Chain)#
이미 만든 Icon 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 iconStyle
인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class IconChainExample extends StatelessWidget {
const IconChainExample({super.key});
@override
Widget build(BuildContext context) {
return const Icon(
LucideIcons.house,
iconStyle: CoreIconStyle(size: 28),
).withStyle(
const CoreIconStyle(
size: CoreIconSize.size32,
color: CoreColor.token(CoreColors.primary),
),
);
}
}
class IconChainExample extends StatelessComponent {
const IconChainExample({super.key});
@override
Component build(BuildContext context) {
return const Icon(
LucideIcons.house,
iconStyle: CoreIconStyle(size: 28),
).withStyle(
const CoreIconStyle(
size: CoreIconSize.size32,
color: CoreColor.token(CoreColors.primary),
),
);
}
}
Live Preview#
코드 보기
class IconDefaultExample extends StatelessComponent {
const IconDefaultExample({super.key});
@override
Component build(BuildContext context) {
return const Icon(LucideIcons.house, iconStyle: CoreIconStyle(size: 28));
}
}
class IconDefaultExample extends StatelessWidget {
const IconDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return const Icon(LucideIcons.house, iconStyle: CoreIconStyle(size: 28));
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 버튼, 메뉴, 네비게이션 항목에 시각적 힌트가 필요할 때
- 텍스트 없이 아이콘만으로 의미를 전달해야 할 때 (접근성 레이블 필수)
- CoUI 디자인 시스템의 통일된 아이콘 세트를 사용할 때
대신 다른 컴포넌트를 사용하세요:
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
icon |
IconName |
필수 (positional) | 표시할 아이콘 (LucideIcons.* 등) |
iconStyle |
CoreIconStyle? |
null |
아이콘 크기/색상 |
스타일 시스템 (Style System)#
Icon 의 모든 시각 오버라이드는 단일 iconStyle (CoreIconStyle) 으로 흐릅니다. IconName
식별자만 위젯 파라미터로 직접 전달합니다.
CoreIconStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
size |
double? |
Icon size in logical pixels (Flutter) / CSS pixels (Web). null → inherit. |
color |
CoreColor? |
Icon colour. null → inherit. |
Resolve chain#
design system default
→ CoreIconTheme.style // 프로젝트 공통
→ parent component slot override // 예: CoreButtonStyle.leadingIconStyle
→ widget.iconStyle // 인스턴스별
각 레이어는 CoreIconStyle 의 어떤 필드든 부분적으로 채울 수 있습니다. null 인 필드는 다음 레이어로 넘겨주고, non-null 인 필드는 해당 레이어에서 확정됩니다.
색상 처리 차이점#
-
Flutter:
Color객체로 받아ColorFilter.mode(color, BlendMode.srcIn)으로 tint -
Web: Tailwind colour token 문자열(
'tertiary'등)로 받아text-${token}class 적용 - 양쪽 모두
currentColor기반 SVG를 tint하는 결과는 동일
테마 연동#
프로젝트 레벨 기본값은 CoreComponentTheme.icon에 CoreIconTheme.style로 지정합니다.
CoUIApp(
theme: ThemeData.fromCore(
CoreThemePresets.light,
coreComponentTheme: const CoreComponentTheme(
icon: CoreIconTheme(
style: CoreIconStyle(
size: 20,
color: CoreColor.token(CoreColors.onSurfaceVariant),
),
),
),
),
home: MyHomePage(),
)
이후 iconStyle이 null이거나 size/color가 null인 모든 Icon은 위 default를 따릅니다.
크기 권장값#
| 용도 | 크기 |
|---|---|
| 인라인 텍스트 | 16.0 |
| 버튼 내 아이콘 | 20.0 |
| 기본 아이콘 | 24.0 |
| 강조 아이콘 | 32.0 |
| 히어로 아이콘 | 48.0 |
동작 스펙 (Behavior)#
- IconName의 id를 벡터 글리프로 표시합니다. 크기 단위는 논리 px이며 iconStyle과 테마·ambient IconTheme에서 해석합니다.
-
CoUI 컴포넌트가 직접 그리는 아이콘은 패키지에 번들(
CoreBundledIcons)되어 양 플랫폼 모두 네트워크 없이 그립니다 — Flutter 는 첫 프레임에, Web 은<iconify-icon>스크립트 없이 인라인<svg>로(서버 렌더 마크업에도 들어갑니다). 그 밖의 아이콘은 Flutter 가 IconResolver 캐시를 읽고 없으면 preload한 뒤 렌더하며(데이터가 아직 없을 때는 해석된 크기의 빈 정사각형), Web 은<iconify-icon>이 Iconify 에서 받아 그립니다. - Lucide 글리프의 획은 원본의 비례 획이 아니라 아이콘 크기가 정하는 규칙 하나를 따릅니다. 값과 적용 범위는 아래 Lucide 획 규칙에 있습니다.
- 표시용이며 클릭·선택·비활성·접근 가능한 이름을 자동 생성하지 않습니다. 아이콘을 액션으로 쓰려면 Button / Fab 에 넣고 이름을 호출부에서 제공합니다.
Lucide 획 규칙#
Lucide 원본은 모든 글리프를 24 격자에 stroke-width="2"로 그립니다. 그대로 쓰면 렌더 획이 크기 ÷ 12로 커져서, 10에서는 픽셀보다 가늘고 32에서는 본문 글자보다 무겁습니다. 이 비례 대신 코어의 규칙 하나(CoreIconStroke)가 아이콘 크기로 렌더 획을 정하고, Flutter와 Web이 같은 규칙을 읽습니다.
렌더 획 = max(1.25, 토큰 크기 ÷ 16)
SVG stroke-width (24 격자) = 렌더 획 × 24 ÷ 토큰 크기 = max(1.5, 30 ÷ 토큰 크기)
렌더 획은 토큰 px로 잰 화면 위 두께이고, SVG stroke-width는 글리프 안 24 격자 좌표의 값입니다. SVG가 크기 ÷ 24로 줄어 그려지므로 같은 렌더 획이 크기마다 다른 SVG 값으로 쓰입니다. 20 이하는 1.25로 고정되고 24부터 크기를 따라 커집니다. SVG 값은 소수점 넷째 자리까지 씁니다.
| 토큰 크기 | 렌더 획 | SVG stroke-width |
|---|---|---|
| 10 | 1.25 | 3 |
| 12 | 1.25 | 2.5 |
| 14 | 1.25 | 2.1429 |
| 16 | 1.25 | 1.875 |
| 18 | 1.25 | 1.6667 |
| 20 | 1.25 | 1.5 |
| 24 | 1.5 | 1.5 |
| 28 | 1.75 | 1.5 |
| 32 | 2 | 1.5 |
사다리 밖 크기도 같은 공식입니다(8 → 1.25, 22 → 1.375, 48 → 3).
-
토큰 크기 기준:
Icon은 받은size를 토큰 크기, 곧 테마 scaling을 곱하기 전의 크기로 읽습니다. scaling이 박스를 키우면 획도 같은 비율로 커지므로, 휴대폰 1.25배 화면에서 토큰 20은 25px 박스에 획 1.5625로 그려집니다. 컴포넌트가 이미 scaling을 곱한 크기를Icon에 넘기면 규칙은 그 곱한 값을 토큰 크기로 읽으므로, scaling이 1이 아닌 앱에서는 그 컴포넌트의 아이콘 박스와 획이 의도보다 커집니다. -
Lucide만: 아이콘 id 접두가
lucide인 글리프에만 적용합니다. HeroIcons · Radix · Bootstrap은 원본 획 그대로 그립니다. -
보더 토큰과 분리:
CoreStrokeWidth나--coui-stroke-width-*를 읽지 않습니다. neo-brutalism처럼 보더 두께를 올리는 표면 프리셋 아래에서도 아이콘은 굵어지지 않습니다. -
컴포넌트마다 획을 따로 정하지 않음: 컴포넌트가 그리는 Lucide 아이콘도 같은 규칙을 따릅니다. Checkbox의 체크 표시도 평범한
LucideIcons.check이며, 체크 글리프의 토큰 크기(8 ~ 16)는 모두 하한 1.25에 걸려xs(8)를 포함한 모든 체크박스 크기에서 1.25로 그려집니다. 일부 선택 가로줄(markThickness)은 이 하한(CoreIconStroke.minRenderedWidth)에서 값을 가져오므로 체크와 가로줄이 모든 크기에서 같은 두께로 읽히고, 하한이 바뀌어도 둘이 같이 움직입니다. -
스타일 필드 없음: 획은 크기에서 유도되는 값이라
CoreIconStyle의 필드는size와color뿐이고, 호출부가 획을 따로 고르지 않습니다. -
플랫폼별 적용 범위: Flutter는 모든 Lucide 아이콘에 적용하고, Web은 번들 아이콘의 인라인
<svg>에만 적용합니다. 자세한 내용은 크로스 플랫폼 차이점을 참고하세요.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
색상을 생략해 부모 색을 자동으로 상속
Icon(LucideIcons.check)
iconStyle.color를 생략하면 Web은 currentColor로 부모 색을 상속하고 Flutter는 ambient IconTheme을 따릅니다 — 배경색이 다른 버튼/배지 안에 넣어도 매번 색을 새로 지정할 필요 없이 자동으로 어울리는 색이 됩니다.
❌ Don't#
한 화면 안에서 아이콘 세트를 섞어 쓰지 않기
// ❌ 인접한 아이콘에 서로 다른 세트 사용
Row(
children: [
Icon(LucideIcons.chevronRight),
Icon(RadixIcons.chevronRight),
],
)
각 세트는 자기 그리드와 획 두께로 그려져 같은 size를 줘도 시각적 무게가 다르게 도착합니다 — 인접한 컨트롤끼리 세트가 섞이면 선택이 아니라 렌더 버그로 보입니다. CoUI 컴포넌트 내부는 물론, 앱 코드에서도 인접한 아이콘끼리는 LucideIcons 한 세트로 통일하세요.
접근성 (Accessibility)#
역할#
양 플랫폼 모두 자기 role 이나 이름을 내보내지 않습니다.
Flutter Icon 은 Semantics 를 직접 부르지 않고, 실제 렌더러인 SvgPicture.string
에 semanticsLabel 도 넘기지 않습니다 (icon.dart:118-123). flutter_svg 는 excludeFromSemantics
기본값(false)일 때 Semantics(image: true, label: '') 를 붙이므로, 남는 것은 "이미지"라는 플래그 하나와 빈 이름뿐입니다.
Web Icon 은 그리는 요소에 role / aria-* 를 붙이지 않습니다. 번들 아이콘(CoreBundledIcons)의 인라인
<svg> 에는 그리기 속성(viewBox 등)과 호출자가 넘긴 속성이, 번들 밖 아이콘의 <iconify-icon>
요소에는 icon 속성과 호출자가 넘긴 속성만 실립니다.
요소에 aria-hidden 이 자동으로 붙지도 않습니다. 장식용 아이콘을 접근성 트리에서 확실히 빼려면 Icon(..., attributes: {'aria-hidden': 'true'})
로 직접 주거나, 이 저장소가 내부에서 하듯 아이콘을 감싼 요소에 붙이세요.
키보드#
처리하는 키가 없습니다. Flutter 생성자는 key / icon / iconStyle 셋만 받아 제스처·포커스 파라미터 자체가 없고 (icon.dart:32-36), Web 은
eventHandlers 를 받긴 하지만 (icon.dart:44) tabindex 를 내보내지 않아 키 이벤트가 도달할 포커스 지점이 생기지 않습니다.
스크린 리더#
아이콘 자신에게서 나오는 이름은 없습니다. Flutter 쪽은 라벨이 빈 문자열인 image 노드이고, Web 쪽은 이름 없는 인라인 <svg>
이거나 텍스트 노드가 0개인 커스텀 요소입니다 — 어느 쪽도 읽을 것을 갖고 있지 않습니다. 상태 통보(live region)도 없습니다.
따라서 이름은 조상이 제공해야 합니다. 아이콘이 Button / Tooltip / 라벨 있는 행 안에 있으면 그 조상의 이름이 안내의 전부가 되고, 텍스트 라벨 없이 홀로 선 아이콘이라면 Flutter 는
Semantics(label: ...) 로 감싸고 Web 은 attributes: {'aria-label': ...} 를 주거나 부모 요소에 이름을 붙여야 합니다.
포커스 관리#
포커스 대상이 아닙니다. Flutter 에 FocusNode / Focus 가 없고 Web 에 tabindex 가 없어 어느 플랫폼에서도 traversal 정지점이 되지 않습니다. 오버레이가 아니므로 진입·이탈·트랩 모두 해당 없습니다.
알려진 제약#
-
이름 슬롯이 계약에 없고, 두 플랫폼의 보완 경로 모양이 다릅니다.
CoreIconContract는icon과iconStyle둘만 노출합니다 (icon_contract.dart:11-33). Web 은UiComponent라attributes로 요소에 직접aria-label을 실을 수 있지만 (icon.dart:36-45), FlutterIcon은StatefulWidget이고 attributes 표면이 없어 바깥에서Semantics로 감싸는 것만 가능합니다 — 같은 의도의 코드가 플랫폼별로 갈립니다. -
컴포넌트 내부에서 만들어지는 아이콘 전용 버튼은 이름을 얻을 방법이 없습니다.
Chip의 remove,Calendar의 이전/다음,Window의 창 컨트롤,ColorPicker의 스포이드,Sortable의 드래그 핸들은 양 플랫폼 모두child: Icon(...)하나만 넘기고 (chip.dart:191/chip.dart:176등) 그 버튼의attributes/Semantics진입점을 호출자에게 열어 주지 않아, 스크린 리더가 "버튼"이라고만 읽습니다. 우회 경로가 없습니다. -
Flutter 는 SVG 가 도착하기 전 한 프레임 동안 접근성 트리에서 아예 사라집니다. 아이콘 데이터가 비동기로 해석되는 동안에는
SizedBox.square만 반환되어 (icon.dart:106-109) image 플래그조차 없다가, 로드가 끝나면 나타납니다.
모션 감소 · 고대비 · 최소 터치 타깃처럼 모든 컴포넌트에 공통으로 적용되는 축은 전역 접근성 축 에 정리되어 있습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 글리프 | IconResolver 데이터로 SvgPicture.string |
번들 아이콘은 인라인
<svg>
, 그 밖은
<iconify-icon icon="id">
custom element
|
| 색상 적용 | resolver의 ColorFilter.srcIn | CSS currentColor / inline 색 |
| 로딩 | 컴포넌트 아이콘은 번들(즉시), 그 밖은 캐시·preload와 빈 크기 대역 | 컴포넌트 아이콘은 번들(스크립트·요청 없음), 그 밖은 iconify custom element 런타임 |
| Lucide 획 | 모든 Lucide 아이콘(번들 · 캐시에서 받은 것)에 획 규칙 적용 |
번들(
CoreBundledIcons
) 아이콘의 인라인
<svg>
에만 적용. 번들 밖 아이콘을 그리는
<iconify-icon>
은 Iconify의 획(24 격자에 2, 크기 ÷ 12)을 그대로 써서 대부분의 크기에서 Flutter와 획이 다릅니다 — 앱이 직접 고른 Lucide 아이콘이 번들에 없으면 Web에서는 규칙이 닿지 않습니다
|
| 이름·역할 보강 | 호출부 Semantics 래퍼 | caller attributes를 custom element에 전달 |