diff --git a/.changeset/smooth-singers-mate.md b/.changeset/smooth-singers-mate.md new file mode 100644 index 000000000..b85d87f63 --- /dev/null +++ b/.changeset/smooth-singers-mate.md @@ -0,0 +1,5 @@ +--- +'@microsoft/atlas-css': minor +--- + +Add container query mixins and tokens. New mixins: `container()` for marking elements as query containers, and `container-320`, `container-480`, `container-640`, `container-800` for mobile-first container size queries. Refactored the timeline component to use the new mixins. diff --git a/css/src/components/README.md b/css/src/components/README.md index f936f3427..d04c2bb4d 100644 --- a/css/src/components/README.md +++ b/css/src/components/README.md @@ -33,6 +33,10 @@ Although we favor unabbreviated names in our design system, we've made one excep - extra large - xl - extra extra large - xl +## Container query token and mixin naming + +Container query tokens and mixins use the pixel value in the name rather than t-shirt sizes (e.g., `$container-query-480` and `@include container-480` instead of `$container-query-md` and `@include container-md`). This differs from the t-shirt size convention used elsewhere because container query breakpoints describe the intrinsic width of a component's container, not a relative size within a component's own API. A pixel-named token is immediately unambiguous — `container-480` tells you the exact threshold — whereas `container-md` would require looking up the value, and could be confused with viewport breakpoints that also use t-shirt sizes. + ## Example ```scss diff --git a/css/src/components/timeline.scss b/css/src/components/timeline.scss index c171e2b77..6436500ed 100644 --- a/css/src/components/timeline.scss +++ b/css/src/components/timeline.scss @@ -1,5 +1,6 @@ @use 'sass:math'; @use '../tokens/index.scss' as tokens; +@use '../mixins/index.scss' as mixins; $timeline-content-font-size: tokens.$font-size-8 !default; $timeline-timestamp-font-size: tokens.$font-size-9 !default; $timeline-timestamp-font-weight: tokens.$weight-semilight !default; @@ -47,7 +48,7 @@ $timeline-item-badge-transform-rtl: translate( } } - @container (min-width: #{tokens.$container-query-md}) { + @include mixins.container-480 { .timeline-item-template { display: grid; width: $timeline-item-template-width; diff --git a/css/src/mixins/container-queries.scss b/css/src/mixins/container-queries.scss new file mode 100644 index 000000000..0657e557f --- /dev/null +++ b/css/src/mixins/container-queries.scss @@ -0,0 +1,67 @@ +@use 'sass:string'; +@use '../tokens/index.scss' as tokens; + +/// Sets an element as a container query context. +/// @param {String} $name [null] - Optional container name for targeted queries. +/// @param {String} $type [inline-size] - The container type (inline-size or size). +@mixin container($name: null, $type: inline-size) { + container-type: $type; + + @if $name { + container-name: $name; + } +} + +// Mobile-first container query mixins + +/// @param {String} $name [null] - Optional container name to target. +@mixin container-320($name: null) { + $prefix: ''; + + @if $name { + $prefix: $name + ' '; + } + + @container #{$prefix}(min-width: #{tokens.$container-query-320}) { + @content; + } +} + +/// @param {String} $name [null] - Optional container name to target. +@mixin container-480($name: null) { + $prefix: ''; + + @if $name { + $prefix: $name + ' '; + } + + @container #{$prefix}(min-width: #{tokens.$container-query-480}) { + @content; + } +} + +/// @param {String} $name [null] - Optional container name to target. +@mixin container-640($name: null) { + $prefix: ''; + + @if $name { + $prefix: $name + ' '; + } + + @container #{$prefix}(min-width: #{tokens.$container-query-640}) { + @content; + } +} + +/// @param {String} $name [null] - Optional container name to target. +@mixin container-800($name: null) { + $prefix: ''; + + @if $name { + $prefix: $name + ' '; + } + + @container #{$prefix}(min-width: #{tokens.$container-query-800}) { + @content; + } +} diff --git a/css/src/mixins/index.scss b/css/src/mixins/index.scss index 8a3e46a60..e2a3aafc0 100644 --- a/css/src/mixins/index.scss +++ b/css/src/mixins/index.scss @@ -1,4 +1,5 @@ @forward './media-queries.scss'; +@forward './container-queries.scss'; @forward './code-block.scss'; @forward './colors.scss'; @forward './control.scss'; diff --git a/css/src/tokens/containers.scss b/css/src/tokens/containers.scss index fa7adf3cc..69de9e265 100644 --- a/css/src/tokens/containers.scss +++ b/css/src/tokens/containers.scss @@ -1,5 +1,8 @@ /** * @sass-export-section="container" */ -$container-query-md: 480px !default; +$container-query-320: 320px !default; +$container-query-480: 480px !default; +$container-query-640: 640px !default; +$container-query-800: 800px !default; //@end-sass-export-section diff --git a/site/src/tokens/breakpoints.md b/site/src/tokens/breakpoints.md index b86bf7796..24b1375c9 100644 --- a/site/src/tokens/breakpoints.md +++ b/site/src/tokens/breakpoints.md @@ -1,13 +1,27 @@ --- title: Breakpoints -description: Atlas CSS breakpoints tokens and media queries tokens +description: Atlas CSS breakpoints tokens, media query mixins, and container query mixins template: token token: breakpoints --- # Breakpoints and media queries -Available media queries: +Atlas provides viewport-based media query mixins for responsive layouts. These follow a mobile-first approach — styles apply from the specified breakpoint and up. + +```scss +@use '@microsoft/atlas-css/src/mixins' as mixins; + +.my-element { + display: block; + + @include mixins.tablet { + display: flex; + } +} +``` + +Available media query mixins: ```scss @mixin tablet { @@ -31,23 +45,96 @@ Available media queries: // Orientation @mixin orientation-portrait { - @media screen and (max-aspect-ratio: 1/1), - screen and (min-resolution: 120dpi) and (max-aspect-ratio: 1/1) { + @media screen and (aspect-ratio <= 1/1), + screen and (resolution >= 120dpi) and (aspect-ratio <= 1/1) { @content; } } @mixin orientation-landscape { - @media screen and (min-aspect-ratio: 1/1), - screen and (min-resolution: 120dpi) and (min-aspect-ratio: 1/1) { + @media screen and (aspect-ratio >= 1/1), + screen and (resolution >= 120dpi) and (aspect-ratio >= 1/1) { @content; } } @mixin orientation-square { - @media screen and (aspect-ratio: 1/1), - screen and (min-resolution: 120dpi) and (aspect-ratio: 1/1) { + @media screen and (aspect-ratio <= 1/1), + screen and (resolution >= 120dpi) and (aspect-ratio <= 1/1) { @content; } } ``` + +## Container queries + +Container queries let a component respond to the size of its _parent container_ rather than the viewport. This is useful when the same component can appear in different layout contexts (sidebar, main content, full-width) and should adapt accordingly. + +Atlas provides two types of container query mixins: one for marking a parent element as a container, and size-based query mixins for applying styles at specific container widths. + +### Marking a container + +Use the `container` mixin on the parent element that child elements should respond to. + +```scss +@use '@microsoft/atlas-css/src/mixins' as mixins; + +.card-grid { + @include mixins.container; +} +``` + +You can also pass an optional name to target a specific container: + +```scss +.card-grid { + @include mixins.container('card-grid'); +} +``` + +### Container query breakpoints + +Container query breakpoints use pixel values in their names rather than t-shirt sizes. This makes the exact threshold immediately clear and avoids confusion with the viewport breakpoints, which serve a different purpose. + +| Token | Value | Mixin | +| --- | --- | --- | +| `$container-query-320` | 320px | `container-320` | +| `$container-query-480` | 480px | `container-480` | +| `$container-query-640` | 640px | `container-640` | +| `$container-query-800` | 800px | `container-800` | + +### Querying a container + +Use the container query mixins inside child elements. Like viewport media queries, these are mobile-first — styles apply when the container is _at least_ the specified width. + +```scss +@use '@microsoft/atlas-css/src/mixins' as mixins; + +.card { + // Stack by default + display: block; + + // Side-by-side when the container is >= 480px + @include mixins.container-480 { + display: flex; + } +} +``` + +To target a named container, pass the name as an argument: + +```scss +.card { + @include mixins.container-480('card-grid') { + display: flex; + } +} +``` + +### When to use container queries vs media queries + +| Scenario | Use | +| --- | --- | +| Page-level layout changes (e.g., sidebar collapses) | Media queries (`tablet`, `desktop`) | +| Component adapts to the space it's placed in | Container queries (`container-480`, etc.) | +| Component appears in only one layout context | Either works — media queries are simpler |