CommandSearch | CoUI
본문으로 건너뛰기
LogoCoUI

CommandSearch

커맨드 팔레트를 여는 트리거 — 검색처럼 생긴 버튼

목차

CommandSearch 는 누르면 커맨드 팔레트를 여는 트리거입니다. 검색 필드처럼 생겼지만 검색 필드가 아니라 버튼입니다 — 글자는 앱이 onPressed 에서 여는 Command(보통 dialog 형태)에서 입력하고, 트리거는 탭 정지 하나만 차지합니다.

기본 사용법 (Basic Usage)#

CommandSearch(
  shortcut: const Kbd(keys: ['⌘K']),
  onPressed: () => openCommandPalette(context),
)
CommandSearch(
  shortcut: const Kbd(keys: ['⌘K']),
  onPressed: () => openCommandPalette(),
)

폭은 부모가 정합니다 — 트리거는 받은 폭을 채우므로 Flutter 에서는 폭이 정해진 자리 (Expanded, SizedBox, 앱 바의 가운데 칸)에 놓습니다. 폭 토큰은 없습니다.

Live Preview#

구조 (Anatomy)#

시작부터 끝까지 세 칸을 놓고, 칸 사이는 contentGapStyle 하나입니다. 안내 글이 남은 폭을 모두 차지하고 넘치면 말줄임합니다.

부분역할슬롯
leading앞 글리프 — 없으면 검색 아이콘leading
hint안내 글 — 없으면 활성 로케일의 팔레트 안내 문구hint
shortcut단축키 표시 — 관례상 Kbdshortcut

Figma: ❖ CommandSearch

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 앱 바 가운데 칸이나 사이드바 위에 "검색 · 명령" 진입점을 둘 때
  • 누르면 Command 를 dialog 로 여는 단일 트리거가 필요할 때

대신 다른 컴포넌트를 사용하세요:

  • Command: 팔레트 자체 — 검색 입력과 결과 목록을 화면에 바로 펼쳐 둘 때
  • TextField: 그 자리에서 바로 글자를 입력받아야 할 때. 읽기 전용 TextField 를 트리거 모양으로 비틀면 입력을 거부하는 textbox 로 읽히고, 입력할 수 없는 곳에 캐럿이 놓입니다

Props / Parameters#

W 는 Flutter의 Widget / Web의 Jaspr Component입니다.

속성타입기본값설명
size CoreCommandSize CoreCommandSize.defaultSize(md, 40px) 밀도 — 32 / 40 / 48. 여는 Command 팔레트와 같은 사다리라 둘이 한 크기를 나눌 수 있습니다
hint String? null → 로케일의 commandSearch 안내 글. 열리는 팔레트의 검색 필드와 같은 문구가 기본값입니다
leading W? null → 검색 아이콘 앞 글리프. 넘긴 글리프도 트리거의 크기와 색을 주변 아이콘 테마로 받습니다
shortcut W? null 끝의 단축키 표시. 관례상 Kbd(keys: ['⌘K']). 키 입력을 듣는 것은 앱의 몫입니다
enabled bool? null → onPressed != null 활성화 가능 여부. 비활성 트리거는 다른 비활성 버튼처럼 탭 순서에서 빠집니다
onPressed void Function()? null 활성화 콜백 — 앱이 여기서 Command 를 엽니다
commandSearchStyle CoreCommandSearchStyle? null 모든 chrome 이 지나는 단일 슬롯 (아래 표 참고)

CoreCommandSearchStyle 필드#

필드타입설명
height double? Trigger height (logical px). null → the size ladder, which a theme density moves one rung; a height stated here stays as stated.
padding CoreEdgeInsets? Content insets — directional, start and end. null → the size ladder.
contentGapStyle CoreGapStyle? Gap between the leading glyph, the hint and the shortcut. null → the size ladder.
borderRadius CoreBorderRadius? Corner radius — also the focus ring's corner. null → the size row's corner (the height × 0.25).
leadingIconStyle CoreIconStyle? Leading glyph chrome — size, and a colour that wins over [foregroundColor] at rest. null → the size ladder.
hintTextStyle CoreTextStyle? Hint typography — role, and a colour that wins over [foregroundColor] at rest. null → the size ladder.
backgroundColor CoreColor? Resting face. null → [defaultBackgroundColor].
foregroundColor CoreColor? Ink of the leading glyph and the hint. null → [defaultForegroundColor].
disabledBackgroundColor CoreColor? Face while disabled. null → [defaultDisabledBackgroundColor].
disabledForegroundColor CoreColor? Ink while disabled — the glyph and the hint. null → [defaultDisabledForegroundColor].
stateLayerColor CoreColor? Colour washed over the face on hover / focus / press. null → [defaultStateLayerColor].
stateLayer CoreStateLayer? How strongly [stateLayerColor] is washed over the face per state. null → [defaultStateLayer]. A disabled trigger takes no wash.
clickableStyle CoreClickableStyle? Nested chrome of the composed Clickable (focus ring, press feedback). null → [defaultClickableStyle]. Raw-forwarded; the ring takes the trigger's corner unless this states one.

스타일 시스템 (Style System)#

commandSearchStyle#

  • 시맨틱 / behaviour: 위젯 파라미터로 직접 (size, enabled, onPressed)
  • chrome / dimensional: CoreCommandSearchStyle 한 곳으로 (면 · 글자색, 높이, 여백, 칸 사이, 모서리, 앞 아이콘 · 안내 글 스타일, 상태 레이어, 포커스 링)

크기 사다리는 defaultsBySize 가 소유합니다.

size 높이 모서리 (× 0.25) 여백 시작 / 끝 칸 사이 앞 아이콘 안내 글
sm 32 8 12 / 8 8 16 bodySmall
md (기본) 40 10 12 / 10 8 18 bodyMedium
lg 48 12 16 / 12 12 20 bodyLarge

높이는 컨트롤 사다리의 단이라 밀도(CoreDensity)가 한 단 옮깁니다. 여백은 방향 여백(시작 / 끝)이라 오른쪽에서 왼쪽으로 쓰는 로케일에서 함께 뒤집힙니다 — 끝 여백이 작은 이유는 끝에 놓이는 Kbd 가 자기 여백을 갖기 때문입니다.

면은 surfaceContainer, 선은 없습니다. 트리거는 입력이 아니라 버튼이므로 입력이 갖는 경계를 그리지 않고, 앞 아이콘 · 안내 글 · 단축키가 트리거를 알립니다. 아이콘과 안내 글은 onSurfaceVariant 입니다 — 상태 레이어가 그 위를 덮어도 읽혀야 하기 때문입니다.

Resolve chain

CoreCommandSearchStyle.defaultsBySize[size]   // 크기 기본값
  → CoreCommandSearchStyle.defaultX           // 크기 무관 기본값
  → CoreCommandSearchTheme.style              // 프로젝트 공통
  → widget.commandSearchStyle                 // 인스턴스별

사용 예

CommandSearch(
  onPressed: () {},
  commandSearchStyle: const CoreCommandSearchStyle(
    backgroundColor: CoreColor.token(CoreColors.surfaceContainerHigh),
  ),
)

빠른 오버라이드 (Chain)

이미 만든 CommandSearch 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class CommandSearchChainExample extends StatelessWidget {
  const CommandSearchChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: CoreLayout.layout320,
      child:
          CommandSearch(
            hint: 'Search docs',
            shortcut: const Kbd(keys: ['⌘K']),
            onPressed: () {},
          ).withStyle(
            const CoreCommandSearchStyle(
              backgroundColor: CoreColor.token(
                CoreColors.surfaceContainerHigh,
              ),
              contentGapStyle: CoreGapStyle(size: CoreSpace.space12),
            ),
          ),
    );
  }
}
class CommandSearchChainExample extends StatelessComponent {
  const CommandSearchChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return div(
      [
        CommandSearch(
          hint: 'Search docs',
          shortcut: const Kbd(keys: ['⌘K']),
          onPressed: () {},
        ).withStyle(
          const CoreCommandSearchStyle(
            backgroundColor: CoreColor.token(
              CoreColors.surfaceContainerHigh,
            ),
            contentGapStyle: CoreGapStyle(size: CoreSpace.space12),
          ),
        ),
      ],
      classes: 'w-${CoreLayout.scale.layout320}',
    );
  }
}

변형 (Variants)#

크기#

CommandSearch(
  size: CoreCommandSize.sm,
  shortcut: const Kbd(keys: ['⌘K']),
  onPressed: () {},
)

안내 글 · 앞 글리프 바꾸기#

CommandSearch(
  hint: 'Search docs',
  leading: const Icon(LucideIcons.bookOpen),
  onPressed: () {},
)

동작 스펙 (Behavior)#

인터랙션#

  • 클릭/탭 · Enter / Space: 활성 상태면 onPressed 를 호출합니다. 팔레트를 여는 것은 앱이 그 콜백에서 합니다
  • 호버 / 포커스 / 눌림: 트리거가 스스로 상태 레이어를 그립니다 — onSurface 를 stateLayer 알파(hover 8% / focus 8% / pressed 12%)로 면 위에 덮습니다. 포커스가 hover 와 같은 강도인 이유는 실제 포커스 표시는 링(2 · 간격 2)이 담당하고 덮개는 보조 신호이기 때문입니다
  • 단축키: shortcut 은 표시만 합니다. ⌘K 를 듣고 팔레트를 여는 것은 앱의 몫입니다

상태 전환#

  • enabled 가 null 이면 onPressed 유무가 활성 여부를 정합니다
  • 비활성 트리거는 면이 disabledContainer, 아이콘 · 안내 글이 disabledContent 로 바뀌고 상태 레이어를 그리지 않으며 탭 순서에서 빠집니다. 합성된 Clickable 의 비활성 흐림은 끕니다 — 비활성 면과 글자색이 이미 비활성을 말합니다

애니메이션#

  • 면 · 덮개 전환은 합성된 Clickable 의 크롬 시계(CoreDuration 토큰)를 따라 포커스 링과 한 시간에 움직이며, 동작 줄이기에서는 즉시 바뀝니다

사용 가이드라인 (Usage Guidelines)#

Do#

같은 크기의 팔레트를 연다

// 앱이 여는 팔레트도 Command(size: CoreCommandSize.sm) 이다
CommandSearch(
  size: CoreCommandSize.sm,
  onPressed: () => openCommandPalette(context, size: CoreCommandSize.sm),
)

이유: 트리거와 팔레트가 같은 사다리(32 / 40 / 48)를 쓰므로, 같은 크기를 넘기면 누른 자리와 열린 팔레트의 행이 한 밀도로 읽힙니다.

Don't#

읽기 전용 텍스트 필드로 트리거를 흉내 내지 않는다

// ❌ 입력을 거부하는 textbox 로 읽히고, 입력할 수 없는 곳에 캐럿이 놓인다
TextField(readOnly: true, onTap: openPalette)

// ✅ 버튼으로 읽히고 탭 정지 하나만 차지한다
CommandSearch(onPressed: openPalette)

이유: 역할(textbox ↔ button)과 포커스 동작이 실제로 하는 일과 어긋납니다.

접근성 (Accessibility)#

역할#

  • Flutter: 합성된 Clickable 이 Semantics(button: true, enabled:) 를 싣습니다
  • Web: 루트가 role="button" 이고, 활성이면 tabindex="0", 비활성이면 aria-disabled="true" 를 냅니다

키보드#

키FlutterWeb
Enter / Space onPressed 호출 onPressed 호출
Tab다음 포커스 가능 요소로 (비활성이면 건너뜀)다음 포커스 가능 요소로 (비활성이면 건너뜀)
⌘K 등 단축키없음 — 앱이 등록없음 — 앱이 등록

스크린 리더#

접근 가능한 이름은 트리거 안의 글자에서 옵니다 — 안내 글과, shortcut 에 놓인 Kbd 의 키 이름("⌘K")이 함께 읽힙니다. 상태 통보(live region)는 없습니다.

포커스 관리#

  • 진입: 이동 없음 — 트리거는 오버레이가 아닙니다
  • 이탈: 팔레트를 닫은 뒤 포커스를 트리거로 돌려보내는 것은 팔레트를 연 오버레이 (Dialog 등)의 복귀 규칙입니다
  • 트랩: 없음

알려진 제약#

  • 비활성일 때 shortcut 에 놓인 Kbd 는 자기 면 · 글자색을 그대로 유지합니다 — 트리거는 호출자가 넘긴 위젯의 크롬을 다시 칠하지 않습니다
  • shortcut 문구만으로 키보드 단축키가 등록되지는 않습니다

전역 축(동작 줄이기 · 고대비 · 색 강제 모드 · 시맨틱 역할 · 최소 터치 타겟)은 접근성 페이지가 한 곳에서 다룹니다.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명CommandSearchCommandSearch
렌더 Clickable + Icon + Text + Gap Clickable + Icon + Text + Gap
상태 레이어 Clickable.onHover / onFocus / onPressedChange 로 감지 + resolver 의 상태별 값 선택 CSS :hover / :focus-visible / :active 범위 규칙이 덮개 요소를 칠함
방향 여백 EdgeInsetsDirectional padding-inline-start / padding-inline-end
포커스 링 모서리 링은 합성된 Clickable 이 자기 모서리로 그립니다. 트리거 모서리가 기본값이고, clickableStyle.borderRadius 를 적으면 박스와 다른 모서리로 그려집니다 링은 outline 이라 요소의 테두리 상자를 따릅니다. 트리거 모서리가 기본값이고, 링 모서리만 적으면 박스도 그 모서리가 됩니다. 트리거의 borderRadius 가 적혀 있으면 그쪽이 박스와 링을 함께 정합니다
Web 전용 HTML 표면 — attributes · classes · css · eventHandlers · id 통과

위 차이 밖에는 없습니다 — 이름 · 순서 · 기본값을 두 위젯 생성자에서 대조했습니다.

  • Command: 이 트리거가 여는 팔레트
  • Kbd: shortcut 칸에 놓는 단축키 표시
  • AppBar: 가운데 칸에 트리거를 놓는 자리

조합 예제#

AppBar(
  centerTitle: true,
  child: CommandSearch(
    shortcut: const Kbd(keys: ['⌘K']),
    onPressed: () => openCommandPalette(context),
  ),
)