ThemeController#
ThemeController는 선택 가능한 테마 스와치를 줄바꿈 그리드로 표시하는 통합 테마 선택 컴포넌트입니다. 각 스와치는 테마의 팔레트 색상을 미리보기로 보여주며, 선택 시 테마 이름을
onThemeChanged로 전달합니다.
Live Preview#
class ThemeControllerDefaultExample extends StatefulComponent {
const ThemeControllerDefaultExample({super.key});
@override
State<ThemeControllerDefaultExample> createState() =>
_ThemeControllerDefaultExampleState();
}
class _ThemeControllerDefaultExampleState
extends State<ThemeControllerDefaultExample> {
String _selected = 'Ocean';
@override
Component build(BuildContext context) {
return ThemeController(
selectedTheme: _selected,
onThemeChanged: (name) => setState(() => _selected = name),
themes: const [
CoreThemeOption(
name: 'Ocean',
primaryColor: CoreColor.fromRGB(14, 165, 233),
secondaryColor: CoreColor.fromRGB(99, 102, 241),
),
CoreThemeOption(
name: 'Forest',
primaryColor: CoreColor.fromRGB(34, 197, 94),
secondaryColor: CoreColor.fromRGB(132, 204, 22),
),
CoreThemeOption(
name: 'Sunset',
primaryColor: CoreColor.fromRGB(249, 115, 22),
secondaryColor: CoreColor.fromRGB(239, 68, 68),
),
],
);
}
}
class ThemeControllerDefaultExample extends StatefulWidget {
const ThemeControllerDefaultExample({super.key});
@override
State<ThemeControllerDefaultExample> createState() =>
_ThemeControllerDefaultExampleState();
}
class _ThemeControllerDefaultExampleState
extends State<ThemeControllerDefaultExample> {
String _selected = 'Ocean';
@override
Widget build(BuildContext context) {
return ThemeController(
selectedTheme: _selected,
onThemeChanged: (name) => setState(() => _selected = name),
themes: const [
CoreThemeOption(
name: 'Ocean',
primaryColor: CoreColor.fromRGB(14, 165, 233),
secondaryColor: CoreColor.fromRGB(99, 102, 241),
),
CoreThemeOption(
name: 'Forest',
primaryColor: CoreColor.fromRGB(34, 197, 94),
secondaryColor: CoreColor.fromRGB(132, 204, 22),
),
CoreThemeOption(
name: 'Sunset',
primaryColor: CoreColor.fromRGB(249, 115, 22),
secondaryColor: CoreColor.fromRGB(239, 68, 68),
),
],
);
}
}
class ThemeControllerChainExample extends StatefulComponent {
const ThemeControllerChainExample({super.key});
@override
State<ThemeControllerChainExample> createState() => _ThemeControllerChainExampleState();
}
class _ThemeControllerChainExampleState extends State<ThemeControllerChainExample> {
String _selected = 'Ocean';
@override
Component build(BuildContext context) {
return ThemeController(
selectedTheme: _selected,
onThemeChanged: (name) => setState(() => _selected = name),
themes: const [
CoreThemeOption(
name: 'Ocean',
primaryColor: CoreColor.fromRGB(14, 165, 233),
secondaryColor: CoreColor.fromRGB(99, 102, 241),
),
CoreThemeOption(
name: 'Forest',
primaryColor: CoreColor.fromRGB(34, 197, 94),
secondaryColor: CoreColor.fromRGB(132, 204, 22),
),
CoreThemeOption(
name: 'Sunset',
primaryColor: CoreColor.fromRGB(249, 115, 22),
secondaryColor: CoreColor.fromRGB(239, 68, 68),
),
],
).withStyle(
const CoreThemeControllerStyle(
buttonSize: CoreSize.size64,
buttonRadius: CoreBorderRadius.all(CoreRadius.radius4),
selectedBorderColor: CoreColor.token(CoreColors.primary),
selectedBorderWidth: CoreStrokeWidth.stroke3,
),
);
}
}
class ThemeControllerChainExample extends StatefulWidget {
const ThemeControllerChainExample({super.key});
@override
State<ThemeControllerChainExample> createState() => _ThemeControllerChainExampleState();
}
class _ThemeControllerChainExampleState extends State<ThemeControllerChainExample> {
String _selected = 'Ocean';
@override
Widget build(BuildContext context) {
return ThemeController(
selectedTheme: _selected,
onThemeChanged: (name) => setState(() => _selected = name),
themes: const [
CoreThemeOption(
name: 'Ocean',
primaryColor: CoreColor.fromRGB(14, 165, 233),
secondaryColor: CoreColor.fromRGB(99, 102, 241),
),
CoreThemeOption(
name: 'Forest',
primaryColor: CoreColor.fromRGB(34, 197, 94),
secondaryColor: CoreColor.fromRGB(132, 204, 22),
),
CoreThemeOption(
name: 'Sunset',
primaryColor: CoreColor.fromRGB(249, 115, 22),
secondaryColor: CoreColor.fromRGB(239, 68, 68),
),
],
).withStyle(
const CoreThemeControllerStyle(
buttonSize: CoreSize.size64,
buttonRadius: CoreBorderRadius.all(CoreRadius.radius4),
selectedBorderColor: CoreColor.token(CoreColors.primary),
selectedBorderWidth: CoreStrokeWidth.stroke3,
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 사용자가 여러 색상 테마 중 하나를 고르도록 할 때
- 설정 화면에서 테마 미리보기와 함께 선택지를 제공할 때
대신 다른 컴포넌트를 사용하세요:
Toggle: 라이트/다크 두 가지만 전환할 때Select: 색상 프리뷰 없이 텍스트 목록으로 충분할 때
기본 사용법 (Basic Usage)#
ThemeController(
selectedTheme: _selected,
onThemeChanged: (name) => setState(() => _selected = name),
themes: const [
CoreThemeOption(
name: 'Ocean',
primaryColor: CoreColor.fromRGB(14, 165, 233),
secondaryColor: CoreColor.fromRGB(99, 102, 241),
),
CoreThemeOption(
name: 'Forest',
primaryColor: CoreColor.fromRGB(34, 197, 94),
secondaryColor: CoreColor.fromRGB(132, 204, 22),
),
],
)
ThemeController(
selectedTheme: _selected,
onThemeChanged: (name) => setState(() => _selected = name),
themes: const [
CoreThemeOption(
name: 'Ocean',
primaryColor: CoreColor.fromRGB(14, 165, 233),
secondaryColor: CoreColor.fromRGB(99, 102, 241),
),
CoreThemeOption(
name: 'Forest',
primaryColor: CoreColor.fromRGB(34, 197, 94),
secondaryColor: CoreColor.fromRGB(132, 204, 22),
),
],
)
빠른 오버라이드 (Chain)#
이미 만든 ThemeController 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class ThemeControllerChainExample extends StatefulWidget {
const ThemeControllerChainExample({super.key});
@override
State<ThemeControllerChainExample> createState() => _ThemeControllerChainExampleState();
}
class _ThemeControllerChainExampleState extends State<ThemeControllerChainExample> {
String _selected = 'Ocean';
@override
Widget build(BuildContext context) {
return ThemeController(
selectedTheme: _selected,
onThemeChanged: (name) => setState(() => _selected = name),
themes: const [
CoreThemeOption(
name: 'Ocean',
primaryColor: CoreColor.fromRGB(14, 165, 233),
secondaryColor: CoreColor.fromRGB(99, 102, 241),
),
CoreThemeOption(
name: 'Forest',
primaryColor: CoreColor.fromRGB(34, 197, 94),
secondaryColor: CoreColor.fromRGB(132, 204, 22),
),
CoreThemeOption(
name: 'Sunset',
primaryColor: CoreColor.fromRGB(249, 115, 22),
secondaryColor: CoreColor.fromRGB(239, 68, 68),
),
],
).withStyle(
const CoreThemeControllerStyle(
buttonSize: CoreSize.size64,
buttonRadius: CoreBorderRadius.all(CoreRadius.radius4),
selectedBorderColor: CoreColor.token(CoreColors.primary),
selectedBorderWidth: CoreStrokeWidth.stroke3,
),
);
}
}
class ThemeControllerChainExample extends StatefulComponent {
const ThemeControllerChainExample({super.key});
@override
State<ThemeControllerChainExample> createState() => _ThemeControllerChainExampleState();
}
class _ThemeControllerChainExampleState extends State<ThemeControllerChainExample> {
String _selected = 'Ocean';
@override
Component build(BuildContext context) {
return ThemeController(
selectedTheme: _selected,
onThemeChanged: (name) => setState(() => _selected = name),
themes: const [
CoreThemeOption(
name: 'Ocean',
primaryColor: CoreColor.fromRGB(14, 165, 233),
secondaryColor: CoreColor.fromRGB(99, 102, 241),
),
CoreThemeOption(
name: 'Forest',
primaryColor: CoreColor.fromRGB(34, 197, 94),
secondaryColor: CoreColor.fromRGB(132, 204, 22),
),
CoreThemeOption(
name: 'Sunset',
primaryColor: CoreColor.fromRGB(249, 115, 22),
secondaryColor: CoreColor.fromRGB(239, 68, 68),
),
],
).withStyle(
const CoreThemeControllerStyle(
buttonSize: CoreSize.size64,
buttonRadius: CoreBorderRadius.all(CoreRadius.radius4),
selectedBorderColor: CoreColor.token(CoreColors.primary),
selectedBorderWidth: CoreStrokeWidth.stroke3,
),
);
}
}
Props / Parameters#
ThemeController#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
themes |
List<CoreThemeOption> |
필수 | 선택 가능한 테마 옵션 목록 |
selectedTheme |
String? |
null |
현재 선택된 테마 이름 |
onThemeChanged |
void Function(String) |
필수 | 테마 선택 핸들러 (옵션 이름 전달) |
variant |
CoreThemeControllerVariant |
standard |
시각 변형 |
themeControllerStyle |
CoreThemeControllerStyle? |
null |
크롬 / 치수 스타일 오버라이드 |
CoreThemeOption#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
name |
String |
필수 | 테마 이름 (onThemeChanged 식별자) |
primaryColor | CoreColor | 필수 | 주요 팔레트 색상 프리뷰 |
secondaryColor |
CoreColor? |
null |
보조 팔레트 색상 프리뷰 |
accentColor |
CoreColor? |
null |
액센트 팔레트 색상 프리뷰 |
neutralColor |
CoreColor? |
null |
중립 팔레트 색상 프리뷰 |
스타일 시스템 (Style System)#
CoreThemeControllerStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
buttonSize |
double? |
Size of each theme button (logical px). |
previewSize |
double? |
Size of each colour-preview circle (logical px). |
buttonSpacing |
double? |
Spacing between adjacent theme buttons (logical px) — consumed as native
Wrap.spacing
/
Wrap.runSpacing
on Flutter and as the flex container
gap
CSS property on Web.
|
buttonRadius |
CoreBorderRadius? |
Corner radius of a theme button. |
previewBorderRadius |
CoreBorderRadius? |
Corner radius of the colour-preview circle inside a swatch button. Defaults to a fully round dot — see [defaultPreviewBorderRadius]. |
selectedBorderColor |
CoreColor? |
Border colour of the selected theme button. |
unselectedBorderColor |
CoreColor? |
Border colour of an unselected theme button. |
selectedBorderWidth |
double? |
Border thickness of the selected theme button (logical px). |
unselectedBorderWidth |
double? |
Border thickness of an unselected theme button (logical px). |
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the composed per-swatch
Clickable
(press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.
|
CoreThemeControllerStyle 변형별 기본값 (CoreThemeControllerVariantStyle)#
| 필드 | standard |
|---|---|
buttonBackgroundColor | surface |
동작 스펙 (Behavior)#
인터랙션#
- 선택: 스와치 클릭 시
onThemeChanged(name)호출 -
선택 표시: 선택된 스와치는 강조 보더(
primary, 2px), 나머지는 기본 보더(outline, 1px)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
selectedTheme 값을 themes 목록의 name 과 정확히 일치시키기
ThemeController(
selectedTheme: 'Ocean', // themes 안 CoreThemeOption.name 과 동일해야 함
onThemeChanged: (name) => setState(() => _selected = name),
themes: const [
CoreThemeOption(name: 'Ocean', primaryColor: ...),
CoreThemeOption(name: 'Forest', primaryColor: ...),
],
)
이유: 선택 표시는 selectedTheme 문자열과 각 CoreThemeOption.name 을 비교해 결정되므로, 값이 다르면 어떤 스와치에도 강조 보더가 표시되지 않습니다.
❌ Don't#
라이트/다크 2택 전환에 사용하지 않기
// ❌ 두 개짜리 선택지에 그리드 스와치 선택기 사용
ThemeController(
themes: const [
CoreThemeOption(name: 'Light', primaryColor: ...),
CoreThemeOption(name: 'Dark', primaryColor: ...),
],
selectedTheme: mode,
onThemeChanged: (name) => setMode(name),
)
이유: ThemeController는 여러 팔레트 중 하나를 고르는 그리드 UI로 설계되어 있습니다. 두 상태만 전환할 때는 Toggle이 더 적은 탭 동작과 명확한 on/off 시맨틱을 제공합니다.
접근성 (Accessibility)#
스크린 리더#
-
Flutter:
Semantics(selected: isSelected, button: true, label: themeName)자동 적용 -
Web: 각 스와치
role="button"+aria-pressed+aria-label적용
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | ThemeController | ThemeController |
| 렌더 루트 | Wrap | <div> (flex flex-wrap) |
| 스와치 | Container + BoxDecoration |
<div> + inline CSS border |