반응형 디자인
적절한 반응형 경계를 선택하고 페이지 열과 재사용 요약을 조정하며 레이아웃이 바뀌는 너비를 검증하세요.
이 페이지의 내용
#개요
페이지 레이아웃과 재사용 카드에는 서로 다른 반응형 규칙이 필요할 수 있습니다. 페이지는 뷰포트를 따르고 카드는 부모가 제공하는 공간을 따르게 하세요. Master CSS에서는 스타일을 바꿀 클래스 뒤에 조건을 붙입니다.
먼저 설치를 완료하세요. 예제는 기본 프리셋과 @import '@master/css';를 불러오는 앱 스타일시트를 사용합니다. 각 미리보기에는 경계를 관찰하는 데 필요한 컨트롤이 있으며, 생성된 CSS에서 함께 제시된 클래스의 출력을 볼 수 있습니다.
#변화의 기준 선택
작업 공간의 열 수처럼 페이지 전체에 관한 결정에는 뷰포트 조건을 사용하세요. 같은 컴포넌트가 주 열, 사이드바 또는 다른 제한된 영역에 배치될 수 있다면 컨테이너 쿼리를 사용하세요.
| 결정 기준 | 작성 방법 | 측정 대상 |
|---|---|---|
| 페이지 뷰포트 | grid-cols(3)@md | 뷰포트 너비 |
| 컴포넌트의 가용 공간 | grid-cols(2)@container(--md) | 가장 가까운 적격 상위 컨테이너의 너비 |
하나의 뷰포트, 두 컴포넌트 너비
루트 크기가 16px일 때 28rem은 448px입니다. 이는 실제 컨테이너 너비입니다. 같은 뷰포트 안에서 넓은 요약은 두 트랙으로, 좁은 요약은 세로로 배치됩니다.
텍스트 설명
- 같은 뷰포트. 두 카드는 같은 페이지에 있습니다. 뷰포트 중단점은 두 카드에서 같은 너비를 봅니다.
- 다른 컨테이너. 주 컨테이너는 448px, 사이드바 컨테이너는 160px 너비입니다.
- 영역별 반응. 각 상위 요소는 container-inline을 사용합니다. 하위 카드는 grid-cols(1)과 grid-cols(2)@container(--md)를 사용하므로 주 카드만 프리셋의 28rem 쿼리에 도달합니다.
그림의 두 카드는 같은 뷰포트를 공유하지만 부모 너비는 다릅니다. 뷰포트 조건은 둘에 동시에 일치합니다. 컨테이너 쿼리는 좁은 인스턴스는 세로 배치를 유지하고 넓은 인스턴스만 열로 나누게 합니다.
#페이지를 뷰포트에 맞추기
한 열로 시작하고 콘텐츠 공간이 충분해지는 곳에서 열을 추가하세요. 이 워크플로는 프리셋의 2xs 임계값 37.5rem과 md 임계값 64rem을 사용합니다. 표준 초기 글자 크기 16px에서는 각각 600px와 1024px입니다.
뷰포트에 맞춰 변하는 페이지 열
<section aria-label="Publishing workflow" class="grid-cols(1) gap-md grid-cols(2)@2xs grid-cols(3)@md"> <article class="p-md r-lg b(1) b-solid b-line-divider bg-surface-raised"> <h3 class="m(0) text-sm font-regular fg-text-strong">Draft</h3> <p class="mb(0) mt-xs text-sm fg-text-muted">Collect the changes.</p> </article> <article class="p-md r-lg b(1) b-solid b-line-divider bg-surface-raised"> <h3 class="m(0) text-sm font-regular fg-text-strong">Review</h3> <p class="mb(0) mt-xs text-sm fg-text-muted">Check the examples.</p> </article> <article class="p-md r-lg b(1) b-solid b-line-divider bg-surface-raised"> <h3 class="m(0) text-sm font-regular fg-text-strong">Publish</h3> <p class="mb(0) mt-xs text-sm fg-text-muted">Share the release notes.</p> </article></section>생성된 CSS
@layer theme { :root, :host { --color-zinc-40: oklch(68.6% .0028 265); --color-zinc-60: oklch(48.6% .003 265); --color-zinc-90: oklch(24.6% .0022 265); --color-zinc-100: oklch(15% .0016 265) } :root, :host { --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90)); --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12)); --color-text-strong: light-dark(var(--color-zinc-100), var(--color-white)); --color-text-muted: light-dark(var(--color-zinc-60), var(--color-zinc-40)) } :root, :host { --color-white: oklch(100% 0 none) } :root, :host { --font-weight-regular: 400; --font-size-sm: .875rem; --text-sm: var(--font-size-sm); --text-sm--line-height: 1.5; --text-sm--letter-spacing: 0; --radius-lg: .5rem; --spacing-xs: .5rem; --spacing-md: 1rem }}@layer utilities { .text-sm { font-size: var(--text-sm); line-height: var(--text-sm--line-height, normal); letter-spacing: var(--text-sm--letter-spacing, normal) } .grid-cols\(1\) { display: grid; grid-template-columns: repeat(1, minmax(0, 1fr)) } .bg-surface-raised { background-color: var(--color-surface-raised) } .b-line-divider { border-color: var(--color-line-divider) } .r-lg { border-radius: var(--radius-lg) } .b-solid { border-style: solid } .b\(1\) { border-width: calc(1 * 1px) } .fg-text-muted { color: var(--color-text-muted) } .fg-text-strong { color: var(--color-text-strong) } .font-regular { font-weight: var(--font-weight-regular) } .gap-md { gap: var(--spacing-md) } .m\(0\) { margin: calc(0 * 1px) } .mt-xs { margin-top: var(--spacing-xs) } .mb\(0\) { margin-bottom: calc(0 * 1px) } .p-md { padding: var(--spacing-md) } @media (width >= 37.5rem) { .grid-cols\(2\)\@2xs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)) } } @media (width >= 64rem) { .grid-cols\(3\)\@md { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) } }}grid-cols(1)이 기본 레이아웃입니다. 조건은 열 수만 바꾸며 콘텐츠 순서, 간격, 표면 스타일은 유지됩니다. 3열은 내용이 들어갈 때만 유용합니다. 앱에서 임계값을 채택하기 전에 긴 라벨로 시험해 보세요.
@md는 프리셋의 @custom-media --md 정의를 읽으며 --breakpoint-md 테마 변수는 읽지 않습니다. @md를 사용하는 모든 클래스의 임계값을 바꾸려면 앱 CSS 진입점에서 사용자 지정 미디어를 다시 선언하세요.
@import '@master/css';@custom-media --md (width >= 48rem);새 경계가 전체 프리셋이 아닌 한 레이아웃에만 해당하면 프로젝트 고유 이름을 사용하세요.
@custom-media --workspace-wide (width >= 72rem);<section class="grid-cols(1) grid-cols(3)@workspace-wide">…</section>중단점 레퍼런스에 프리셋 이름과 값이 나열되어 있습니다. 콘텐츠의 필요에 따라 임계값을 선택하세요. md 같은 이름이 특정 기기를 뜻하지는 않습니다.
#컴포넌트를 컨테이너에 맞추기
상위 요소에 container-inline을 추가한 뒤 하위 요소에 컨테이너 조건을 지정하세요. 여기서는 좁은 영역에서 요약과 상태가 세로로 쌓이며 상위 요소가 프리셋의 --md 컨테이너 임계값 28rem에 도달하면 2열이 됩니다.
컨테이너에 맞춰 변하는 요약
Release notes
Review the changes before publishing.
- Status
- Draft ready
- Next step
- Editorial review
<section class="container-inline"> <article class="grid-cols(1) gap-lg p-md b-solid b(1) b-line-divider r-lg bg-surface-raised grid-cols(2)@container(--md)"> <div class="min-w(0)"> <h3 class="m(0) text-md font-regular fg-text-strong">Release notes</h3> <p class="mb(0) mt-xs text-sm fg-text-muted">Review the changes before publishing.</p> </div> <dl class="grid gap-xs m(0) text-sm"> <div> <dt class="fg-text-muted">Status</dt> <dd class="m(0) fg-text-strong">Draft ready</dd> </div> <div> <dt class="fg-text-muted">Next step</dt> <dd class="m(0) fg-text-strong">Editorial review</dd> </div> </dl> </article></section>생성된 CSS
@layer utilities { .grid-cols\(1\) { display: grid; grid-template-columns: repeat(1, minmax(0, 1fr)) } .container-inline { container-type: inline-size } @container (width >= 28rem) { .grid-cols\(2\)\@container\(--md\) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)) } }}브라우저 창을 그대로 둔 채 컨테이너 슬라이더를 움직이세요. 뷰포트가 아닌 상위 요소의 크기를 바꿉니다. 글자 크기 16px에서 447px와 448px를 비교하면 448px에서 두 번째 열이 나타나야 합니다. 컨트롤은 기사의 가용 너비 안에 머무르므로 기사 자체가 임계값보다 좁다면 브라우저를 넓히세요.
상위 요소가 쿼리 경계를 만들고 하위 요소가 레이아웃을 바꿉니다. 요소는 자신의 컨테이너 쿼리에서 자기 크기를 상위 요소로 사용할 수 없습니다. 카드 높이를 콘텐츠에 맡겨 줄바꿈에 따라 자연스럽게 늘어나게 하세요.
#두 크기 체계 구분
@md와 @container(--md)는 서로 다른 정의를 사용합니다.
| 클래스 또는 값 | 프리셋에서의 의미 |
|---|---|
@md | 사용자 지정 미디어의 64rem 뷰포트 임계값 |
@container(--md) | 사용자 지정 컨테이너 조건의 28rem 컨테이너 임계값 |
max-w-md | --container-md 크기 토큰을 사용하는 최대 너비 |
--container-md를 바꾸면 너비 상한만 바뀌며 두 쿼리 임계값은 변하지 않습니다. @custom-container로 재사용 가능한 컨테이너 조건을 정의하거나 grid-cols(2)@container(width>=30rem) 같은 로컬 쿼리를 작성하세요. 정의 구문과 전체 크기 체계는 이름이 있는 컨테이너 조건을 참고하세요.
#바깥 컨테이너에 이름 지정
이름이 없는 크기 쿼리는 가장 가까운 적격 상위 요소를 측정합니다. 중첩 레이아웃에 다른 컨테이너가 생기면 안쪽 경계가 기준이 됩니다. 바깥 너비를 측정하려면 (container-name:outer)로 바깥 컨테이너에 이름을 붙이고 @container(outer|--md)를 사용하세요.
측정할 상위 요소 선택
<section class="(container-name:outer) container-inline p-md r-lg b(1) b-solid b-line-divider"> <p class="mt(0) text-sm fg-text-muted">Outer container</p> <div class="container-inline (max-width:14rem)"> <p class="text-sm fg-text-muted">Inner container · at most 14rem</p> <div class="p-md r-md bg-line-subtle b-line-divider b(1) b-solid b-text-blue@container(--md)"> <h3 class="m(0) text-sm font-regular">Nearest container</h3> <p class="mb(0) mt-xs text-sm"> <span class="hidden@container(--md)">Below 28rem</span> <span class="hidden inline@container(--md)">At least 28rem</span> </p> </div> <div class="mt-md p-md r-md bg-line-subtle b-line-divider b(1) b-solid b-text-blue@container(outer|--md)"> <h3 class="m(0) text-sm font-regular">Named outer container</h3> <p class="mb(0) mt-xs text-sm"> <span class="hidden@container(outer|--md)">Below 28rem</span> <span class="hidden inline@container(outer|--md)">At least 28rem</span> </p> </div> </div></section>생성된 CSS
@layer theme { :root, :host { --color-zinc-40: oklch(68.6% .0028 265); --color-zinc-60: oklch(48.6% .003 265); --color-blue-30: oklch(72.87% .1468 249.3); --color-blue-60: oklch(51.83% .2687 266.1) } :root, :host { --color-line-divider: light-dark(oklch(0% 0 none / .12), oklch(100% 0 none / .12)); --color-line-subtle: light-dark(oklch(0% 0 none / .06), oklch(100% 0 none / .06)); --color-text-muted: light-dark(var(--color-zinc-60), var(--color-zinc-40)); --color-text-blue: light-dark(var(--color-blue-60), var(--color-blue-30)) } :root, :host { --font-weight-regular: 400; --font-size-sm: .875rem; --text-sm: var(--font-size-sm); --text-sm--line-height: 1.5; --text-sm--letter-spacing: 0; --radius-md: .375rem; --radius-lg: .5rem; --spacing-xs: .5rem; --spacing-md: 1rem }}@layer utilities { .text-sm { font-size: var(--text-sm); line-height: var(--text-sm--line-height, normal); letter-spacing: var(--text-sm--letter-spacing, normal) } .bg-line-subtle { background-color: var(--color-line-subtle) } .b-line-divider { border-color: var(--color-line-divider) } .r-lg { border-radius: var(--radius-lg) } .r-md { border-radius: var(--radius-md) } .b-solid { border-style: solid } .b\(1\) { border-width: calc(1 * 1px) } .fg-text-muted { color: var(--color-text-muted) } .container-inline { container-type: inline-size } .hidden { display: none } .font-regular { font-weight: var(--font-weight-regular) } .m\(0\) { margin: calc(0 * 1px) } .mt-md { margin-top: var(--spacing-md) } .mt-xs { margin-top: var(--spacing-xs) } .mb\(0\) { margin-bottom: calc(0 * 1px) } .mt\(0\) { margin-top: calc(0 * 1px) } .p-md { padding: var(--spacing-md) } .\(container-name\:outer\) { container-name: outer } .\(max-width\:14rem\) { max-width: 14rem } @container (width >= 28rem) { .b-text-blue\@container\(--md\) { border-color: var(--color-text-blue) } } @container (width >= 28rem) { .inline\@container\(--md\) { display: inline } } @container (width >= 28rem) { .hidden\@container\(--md\) { display: none } } @container outer (width >= 28rem) { .b-text-blue\@container\(outer\|--md\) { border-color: var(--color-text-blue) } } @container outer (width >= 28rem) { .inline\@container\(outer\|--md\) { display: inline } } @container outer (width >= 28rem) { .hidden\@container\(outer\|--md\) { display: none } }}두 상태 표시는 같은 좁은 내부 컨테이너 안에 있습니다. 바깥 경계를 넓히면 이름이 있는 쿼리의 결과만 바뀝니다. 컨테이너 이름은 상위 요소를 선택할 뿐 요소가 자기 자신을 조회하게 만들지는 않습니다.
이름 있는 쿼리는 더 가까운 컨테이너를 건너뜁니다
이름은 적격 상위 요소를 선택합니다. 쿼리 대상 요소는 그 상위 요소 안에 있어야 합니다. 컨테이너는 자기 자신을 조회하지 않습니다.
텍스트 설명
중첩된 article과 card 컨테이너 경계가 한 자식을 감쌉니다. 같은 파란색 표시는 자식의 article 쿼리를 바깥의 이름 있는 article 경계와 연결하며, 더 가까운 card 경계는 보라색으로 남습니다.
#바뀌는 스타일에만 조건 추가
공유 스타일은 조건 없이 유지하세요. 다른 값이 필요한 속성과 범위에만 조건을 추가하세요. 예를 들어 aside는 md 미만에서 돌출 표면을 사용하고 더 큰 레이아웃에는 해당 배경을 적용하지 않을 수 있습니다.
<aside class="p-md bg-surface-raised@max-md">…</aside>생성된 CSS
@layer theme { :root, :host { --color-zinc-90: oklch(24.6% .0022 265) } :root, :host { --color-surface-raised: light-dark(var(--color-white), var(--color-zinc-90)) } :root, :host { --color-white: oklch(100% 0 none) }}@layer utilities { @media not (width >= 64rem) { .bg-surface-raised\@max-md { background-color: var(--color-surface-raised) } }}@max-md는 프리셋 @md의 여집합으로 두 조건은 겹치지 않습니다. --md를 다시 선언하면 두 경계가 함께 바뀝니다. hidden@media(52.125rem<=width<=64rem) 같은 로컬 범위 조건은 프로젝트의 명명된 임계값을 바꾸지 않고 특정 구간을 지정할 수 있습니다. 쿼리 구문은 조건 레퍼런스를 참고하세요.
최솟값 조건과 여집합은 겹치지 않고 만남
점은 경계의 제외와 포함을 나타냅니다. 축은 프리셋 md 임계값 주변의 일부이며 기기 크기 차트가 아닙니다.
텍스트 설명
- 프리셋 @md는 width >= 64rem에서 시작합니다. @max-md는 그 여집합이며 정확한 임계값은 제외합니다.
- 63.999rem에서는 max-md만, 64rem에서는 md만 일치합니다. 경계에는 틈이나 중복이 없습니다.
- 이미 확장된 manifest 정의를 교체하면 여집합도 다시 선언하세요. 컴파일된 정의 하나를 바꿔도 다른 정의가 다시 확장되지는 않습니다.
표면을 다시 지정하지 않고 레이아웃 변경
조건부 레이아웃 클래스는 관련 없는 선언을 유지합니다. 이 도식은 핵심과 관계없는 카드 내용을 생략했습니다.
텍스트 설명
두 카드는 표면, 반지름, 텍스트를 공유합니다. 부모가 임계값을 넘으면 레이아웃만 세로 배치에서 가로 배치로 바뀝니다.
#스크린샷뿐 아니라 경계 검증
각 임계값 바로 아래와 정확히 도달한 지점을 테스트하세요. 페이지 예제에서는 실제 iframe 뷰포트를 599/600px와 1023/1024px에서 확인하세요. 컴포넌트 예제에서는 페이지 뷰포트를 고정하고 글자 크기 16px에서 상위 요소를 447/448px로 확인하세요.
결과가 예상과 다르면 하위 요소의 계산된 grid-template-columns를 검사하세요. 조건이 일치하는지, 컨테이너 쿼리가 의도한 적격 상위 요소를 사용하는지, 캐스케이드에서 다른 규칙이 우선하지는 않는지 확인하세요.
마지막으로 긴 텍스트, 확대된 텍스트, 좁은 레이아웃을 시험하세요. 모든 너비에서 읽기 순서를 유지하고 줄바꿈을 허용하며 카드 높이를 고정하지 마세요. 레이아웃은 쿼리 경계와 별개로 그리드와 간격 결정을 설명합니다.







