FocusOutline | CoUI
LogoCoUI

FocusOutline

focused 일 때 child 둘레에 포커스 링을 그리는 순수 시각 래퍼 (인프라)

FocusOutline#

focusedtrue 일 때 child 둘레에 포커스 링을 그리는 순수 시각 래퍼(인프라)입니다. 링만 그릴 뿐 포커스 추적은 호출자 책임입니다 — 올바른 "포커스" 정의가 호출 지점마다 다르기 때문(텍스트 필드는 _focusNode.hasFocus, 리스트 항목은 선택 인덱스 등). 입력·버튼 등이 내부적으로 사용합니다.

Live Preview#

사용법#

FocusOutline(
  focused: true, // 호출자가 추적한 포커스 상태
  child: Card(child: Text('포커스된 요소').bodyMedium),
)

빠른 오버라이드 (Chain)#

이미 만든 FocusOutline 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 == CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

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

  @override
  Widget build(BuildContext context) {
    return FocusOutline(
          focused: true,
          child: Card(child: Text('포커스된 요소').bodyMedium),
        )
        .withStyle(
          const CoreFocusOutlineStyle(
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderWidth: CoreStrokeWidth.stroke3,
            duration: Duration(milliseconds: CoreDuration.fast),
          ),
        )
        .radius16;
  }
}
class FocusOutlineChainExample extends StatelessComponent {
  const FocusOutlineChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return FocusOutline(
          focused: true,
          child: Card(child: Text('포커스된 요소').bodyMedium),
        )
        .withStyle(
          const CoreFocusOutlineStyle(
            borderColor: CoreColor.token(CoreColors.tertiary),
            borderWidth: CoreStrokeWidth.stroke3,
            duration: Duration(milliseconds: CoreDuration.fast),
          ),
        )
        .radius16;
  }
}

Props#

파라미터타입기본값설명
child Widget / Component required 링으로 감쌀 콘텐츠
focused bool false 링 표시 여부 (호출자가 추적)
shape CoreFocusOutlineShape rectangle 링 모양 (rectangle / circle)
focusOutlineStyle CoreFocusOutlineStyle? null 링 색·두께·offset·radius 단일 진입점

스타일 시스템 (Style System)#

CoreFocusOutlineStyle 필드#

필드타입설명
align double? Distance from the child's edge to the ring's outer edge (logical px, pre-scaling).
borderRadius CoreBorderRadius? Border radius override (rectangle shape only). No defaultBorderRadius , deliberately — the right value belongs to whatever this ring is drawn around, so no constant can hold it. Hosts inject their own resolved radius into this slot ( Button passes focusOutlineStyle?.borderRadius ?? <its own resolved radius> ; the subfocus infra passes its focus-ring radius), and a host that wants square ring corners simply leaves it null: Flutter's ring-radius helper returns .zero on null, and Web spreads 'border-radius': ?radius so the rectangle arm emits no property at all. A constant would round every focus ring in the kit — including the ones deliberately drawn around square children — and would be the wrong number for each host besides ( radius8 buttons, radius16 cards). Contrast [circleBorderRadius], which does have a default: the circle shape has one correct radius (full pill) regardless of host. @hostDerived tells the Figma reader this is "value comes from the host", not "nothing is drawn" — otherwise it exports the field as an absence and the canvas is asked to show square corners.
circleBorderRadius CoreBorderRadius? Border radius override applied when the focus outline shape is circle. Defaults to [defaultCircleBorderRadius] (all-9999 token) so the box-shadow ring follows a full pill; designers can override to tune the circular ring curvature.
borderColorCoreColor?Ring colour override.
borderWidth double? Ring width override (logical px, pre-scaling).
offsetColor CoreColor? Offset background colour override — the colour that fills the gap between the child's edge and the ring's inner edge. Web equivalent: Tailwind ring-offset-<color> .
duration Duration? Animation duration for the focus ring fade transition.
ringOpacity double? Focus-ring opacity multiplier override. null → [defaultRingOpacity]. Applied by FocusOutline 's own resolver via CoreColor.scaleAlpha on top of whichever borderColor is in effect (default or per-instance override) — setting ringOpacity alone, without also touching borderColor , dims the ring on both platforms.

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

✅ Do#

호출자가 추적한 실제 포커스 상태를 focused에 전달

FocusOutline(
  focused: focusNode.hasFocus,
  shape: CoreFocusOutlineShape.circle,
  child: Avatar(initials: 'JD'),
)

FocusOutline은 자체 FocusNode를 갖지 않는 순수 시각 레이어입니다 — 링이 정확한 순간에 나타나려면 child를 실제로 포커스 가능하게 만드는 위젯의 포커스 상태를 그대로 넘겨야 합니다. 원형 콘텐츠에는 shape: circle을 지정해 링이 사각 모서리로 어색하게 그려지지 않게 합니다.


❌ Don't#

focused 값만으로 child가 키보드 포커스를 받는다고 가정하지 않기

// ❌ Container에는 Focus/FocusNode가 없어 Tab으로 도달할 수 없음
FocusOutline(
  focused: isActive,
  child: Container(color: Colors.blue),
)

FocusOutlineIgnorePointer로 감싸인 장식 레이어일 뿐이라 hit-test 도, 포커스 요청도 하지 않습니다. child 자체가 Focus / FocusableActionDetector / 네이티브 버튼 등으로 실제 포커스를 받을 수 있어야 링이 의미를 가집니다.

접근성 (Accessibility)#

역할 / 시맨틱#

양 플랫폼 모두 아무 role 도 emit 하지 않습니다. Flutter 는 링을 Stacks / Position / IgnorePointer / AnimatedOpacity 로 쌓은 장식용 박스라 시맨틱 노드를 만들지 않고, Web 은 role / aria-* 가 없는 <div> 하나입니다. FocusOutline순수 시각 레이어이며 접근성 트리에는 존재하지 않습니다.

키보드#

처리하는 키가 없습니다. 양 플랫폼 모두 키 핸들러를 등록하지 않습니다.

포커스#

이 컴포넌트는 포커스를 감지·요청·보유·트랩하지 않습니다. 이름과 달리 포커스에 관여하는 코드가 없습니다 — focused 는 기본값 false 인 평범한 호출자 제공 bool 이고, 링은 그 값으로 계산된 그림자·장식 레이어일 뿐입니다.

  • Flutter: FocusNode / Focus / FocusScope / FocusableActionDetector 가 없습니다. 링 레이어는 IgnorePointer 로 감싸여 hit-test 를 가져가지 않습니다.
  • Web: tabindex 가 없고 :focus / :focus-visible CSS 선택자도 없습니다. 링이 브라우저 네이티브 포커스에 반응하지 않습니다.

child 를 포커스 가능하게 만드는 것도, 포커스 상태를 추적해 focused 를 갱신하는 것도 전적으로 호출자 책임입니다.

스크린 리더#

아무것도 추가하지 않습니다. child 는 이 래퍼가 없을 때와 정확히 동일하게 읽히며, 화면에 보이는 링에 대응하는 보조기술 신호는 양 플랫폼 모두 없습니다.

알려진 제약#

  • 호출자가 focused: true 로 다시 렌더하지 않으면 링은 나타나지 않습니다. Web 은 :focus-visible 규칙이 없어 네이티브 포커스를 스스로 따라가지 못합니다.
  • 키보드 포커스와 포인터 포커스를 구분하지 못합니다 — 그 판단은 호출자의 bool 안에만 존재합니다. :focus-visible 에 해당하는 동작이 필요하면 호출자가 직접 구현해야 합니다.
  • Flutter 의 링은 Stacks 오버레이로 child 바깥에 그려지며 레이아웃 공간을 예약하지 않으므로, 인접 콘텐츠와 겹칠 수 있습니다.

포커스 표시가 강제 색상 모드에서 어떻게 다뤄지는지 등 전 컴포넌트 공통 사항은 전역 접근성 축을 참고하세요.