MediaQuery | CoUI
LogoCoUI

MediaQuery

반응형 브레이크포인트 기반 조건부 표시/숨김 컴포넌트

MediaQuery#

MediaQueryVisibility감싸는 박스의 너비에 따라 child를 보이거나 alternateChild로 대체합니다. 임의 px(minWidth/maxWidth)과 Tailwind-표준 breakpoint(minBreakpoint/maxBreakpoint) 양쪽을 지원합니다.

재는 축이 viewport 가 아니라 컨테이너라는 점이 중요합니다 — Flutter 는 LayoutBuilder 의 부모 제약을 읽고(제약이 unbounded 면 ambient MediaQuery 너비로 폴백), Web 은 wrapper 에 container-type: inline-size 를 실어 @container 질의를 씁니다. 사이드바 안에 넣으면 화면이 넓어도 사이드바 폭을 기준으로 판정됩니다.

Live Preview#

Web
Visible at md and above
Visible below md
Flutter
Loading Flutter...
class MediaQueryDefaultExample extends StatelessComponent {
  const MediaQueryDefaultExample({super.key});

  @override
  Component build(BuildContext context) {
    final cs = context.theme.colorScheme;
    return MediaQueryVisibility(
      minBreakpoint: .md,
      alternateChild: div(
        classes: 'w-full p-${CoreSpace.scale.space12} '
            'bg-${cs.surfaceContainer} text-${cs.onSurface}',
        [Text('Visible below md')],
      ),
      child: div(
        classes: 'w-full p-${CoreSpace.scale.space12} '
            'bg-${cs.primaryContainer} text-${cs.onPrimaryContainer}',
        [Text('Visible at md and above')],
      ),
    );
  }
}
class MediaQueryDefaultExample extends StatelessWidget {
  const MediaQueryDefaultExample({super.key});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final cs = theme.colorScheme;
    final ts = theme.typography;
    return MediaQueryVisibility(
      minBreakpoint: .md,
      alternateChild: Container(
        width: double.infinity,
        padding: EdgeInsets.all(CoreSpace.space12),
        color: cs.surfaceContainer.toValue(),
        child: DefaultTextStyle.merge(
          style: ts.bodyMedium.toValue(theme: Theme.of(context)).copyWith(color: cs.onSurface.toValue()),
          child: const Text('Visible below md'),
        ),
      ),
      child: Container(
        width: double.infinity,
        padding: EdgeInsets.all(CoreSpace.space12),
        color: cs.primaryContainer.toValue(),
        child: DefaultTextStyle.merge(
          style: ts.bodyMedium.toValue(theme: Theme.of(context)).copyWith(color: cs.onPrimaryContainer.toValue()),
          child: const Text('Visible at md and above'),
        ),
      ),
    );
  }
}

사용 시기#

  • 작은 화면에서는 모바일 전용 UI, 큰 화면에서는 데스크탑 UI를 보여줄 때
  • 폭이 좁을 때만 특정 도움말/축약 뷰를 보여주고 싶을 때
  • 광고/배너처럼 특정 해상도 이상에서만 노출하고 싶을 때

기본 사용법#

MediaQueryVisibility(
  minBreakpoint: .md,        // ≥ 768px에서 child 표시
  alternateChild: Text('모바일 전용'),
  child: Text('md 이상 전용'),
)

// 임의 px
MediaQueryVisibility(
  minWidth: 900,
  maxWidth: 1280,
  child: Text('900–1280px 범위에서만 표시'),
)
MediaQueryVisibility(
  minBreakpoint: .md,
  alternateChild: div([Text('모바일 전용').bodyMedium.onSurface]),
  child: div([Text('md 이상 전용').bodyMedium.onSurface]),
)

// 임의 px — CoUI가 scoped @container 규칙을 inline으로 주입합니다
MediaQueryVisibility(
  minWidth: 900,
  maxWidth: 1280,
  child: div([Text('900–1280px 범위에서만 표시').bodyMedium.onSurface]),
)

Props#

속성타입기본값설명
child Widget / Component 범위 내일 때 보여줄 콘텐츠 (required)
alternateChild Widget? / Component? null 범위 밖일 때 대체 콘텐츠
minWidth double? null 최소 컨테이너 너비 (px)
maxWidth double? null 최대 컨테이너 너비 (px)
minBreakpoint CoreBreakpoint? null 최소 breakpoint (xs/sm/md/lg/xl/xxl)
maxBreakpoint CoreBreakpoint? null 최대 breakpoint

우선순위: minWidth/maxWidth (widget) > minBreakpoint/maxBreakpoint (widget) > CoreMediaQueryVisibilityTheme.style 의 px > 그 style 의 breakpoint.

브레이크포인트 값#

이름최소 너비 (px)
xs0
sm640
md768
lg1024
xl1280
xxl1536

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

✅ Do#

임의 px 대신 breakpoint 토큰 사용

MediaQueryVisibility(
  minBreakpoint: .md,
  alternateChild: const Text('모바일 전용'),
  child: const Text('데스크탑 전용'),
)

CoreBreakpoint(xs/sm/md/lg/xl/xxl)는 디자인 시스템 전체가 공유하는 척도입니다. minWidth/maxWidth 로 임의 px 를 직접 넣으면 다른 컴포넌트의 브레이크포인트와 어긋날 수 있습니다.


❌ Don't#

좁은 컨테이너 안에 넣고 화면 전체 폭을 기대하지 않기

// ❌ 사이드바 폭(예: 300px) 기준으로 판정됨 — 화면 전체 폭이 아님
Sidebar(
  child: MediaQueryVisibility(
    minWidth: 900,
    child: const Text('900px 이상에서만'),
  ),
)

MediaQueryVisibility 는 viewport 가 아니라 감싸는 컨테이너의 너비를 기준으로 판정합니다(Flutter LayoutBuilder 부모 제약 / Web @container). 좁은 컨테이너 안에 넣으면 화면이 아무리 넓어도 그 컨테이너 폭 기준으로 숨겨집니다.

접근성 (Accessibility)#

역할 / Semantics#

반응형 가시성 유틸리티이므로 자체 semantics 를 만들지 않습니다. Flutter 는 LayoutBuilderSizedBox(child: …) 를 돌려줄 뿐 Semantics 호출이 없고, Web 은 CSS containment(container-type: inline-size) 를 실어 나르는 wrapper <div> 와 주입된 <style> 요소만 내보내며 role / aria-* 를 붙이지 않습니다. 접근성 트리에 나타나는 것은 통과한 child(또는 alternateChild) 뿐입니다.

키보드#

처리하는 키가 없습니다. 양쪽 생성자 모두 이벤트 파라미터 자체를 노출하지 않습니다.

포커스#

포커스 대상이 아니며 포커스를 관리하지도 않습니다. Flutter 에 FocusNode / Focus / ExcludeFocus 가 없고, Web 에 tabindex / inert / focus-visible 이 없습니다. Web 에서 숨겨진 분기가 포커스 순서 밖으로 빠지는 것은 오직 display: none 덕분이며, inertaria-hidden 이 붙어서가 아닙니다.

스크린 리더#

분기를 다루는 방식이 플랫폼마다 다릅니다. Flutter 는 조건에 맞는 분기만 build 하므로 맞지 않는 쪽은 애초에 생성되지 않아 semantics 트리에 닿지 않습니다. Web 은 두 분기를 모두 DOM 에 렌더한 뒤 scoped @container 규칙으로 display: none / display: contents 를 뒤집습니다 — display: none 은 숨겨진 분기를 접근성 트리에서 제거하므로, 일반적인 브라우저에서 중복 안내가 생기지는 않습니다.

알려진 제약#

  • Web 에서는 숨겨진 분기가 서빙되는 HTML 에 그대로 남아 있습니다. 크롤러, 페이지 내 찾기, computed display 를 보지 않는 도구는 그 내용을 그대로 봅니다. Flutter 에는 이 노출이 없습니다.
  • 억제 수단이 display: none 하나뿐입니다aria-hiddeninert 도 없으므로, 소비자 CSS 가 display 를 덮어쓰면 숨겨져 있어야 할 분기가 접근성 트리와 포커스 순서에 그대로 되살아납니다.
  • 너비에만 반응합니다 — 높이·orientation·prefers-* 같은 사용자 선호 질의는 다루지 않습니다. 그 축들은 전역 접근성 축 이 담당합니다.
  • Web 은 경계값이 설정되지 않으면 규칙 자체를 주입하지 않고 빠져나갑니다minWidth / maxWidth / minBreakpoint / maxBreakpoint 중 아무것도 주지 않으면 alternateChild 에 도달할 방법이 없습니다.

관련 컴포넌트#

  • Scrollable Client — 스크롤 가능한 컨테이너에 반응형 컨텐츠를 넣을 때.