Repository: grommet/grommet
Pull request #7533: Responsive container
Author: login: MikeKingdom
Created: 2025-03-01T00:14:43Z | Merged: 2025-03-17T16:47:06Z

UPDATED: The responsive container functionality is now done via
```jsx
<Box responsive="container">
```

TODO: add jest test case(s)

#### What does this PR do?
`<Box responsive="container"> sets up the ResponsiveContext and any responsive descendents such that their breakpoints are based on the width of this container rather than the entire window. This allows everything inside the container to do responsive things without knowing about what else takes up space around them in the window.

The ResponsiveContainer sets up a "container query" in the CSS which is used instead every where it would normally use a "media query" to change padding, font size etc based on breakpoints.

Note that Layers are still responsive to the whole window since today they still will be modal to the entire window and positioned relative to the window frame.

#### Where should the reviewer start?
components/Box/stories/Responsive container

This example shows the same component in two different situations, one inside a `<Box responsive="container">`. The panel on the right can be toggled on and off via the arrow button at the top. You can resize the window and toggle the right panel to see at which points each copy of the component on the left reacts in a responsive way. 
![image](https://github.com/user-attachments/assets/dc25c51c-2dae-47a9-aa3b-c9403f9efd2e)


#### What testing has been done on this PR?
Storybook

#### How should this be manually tested?
Storybook

#### Do Jest tests follow these best practices?

- [ ] `screen` is used for querying.
- [ ] The correct query is used. (Refer to [this list of queries](https://testing-library.com/docs/queries/about/#priority))
- [ ] `asFragment()` is used for snapshot testing.

#### Any background context you want to provide?

#### What are the relevant issues?

#### Screenshots (if appropriate)

#### Do the grommet docs need to be updated?
Yes

#### Should this PR be mentioned in the release notes?
Yes

#### Is this change backwards compatible or is it a breaking change?
Backwards compatible but requires styled-components v6 to be used


Diff:
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 05f63a32ecc..85d86764064 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -2,7 +2,6 @@
 
 ### Is this something you or your team is interested in contributing a pull request towards?
 
-
 ### Expected Behavior
 
 <!--- Tell us what should happen -->
diff --git a/package.json b/package.json
index 7d49b1a065d..7ee158126b7 100644
--- a/package.json
+++ b/package.json
@@ -213,7 +213,7 @@
   "bundlesize": [
     {
       "path": "./dist/grommet.min.js",
-      "maxSize": "175 kB"
+      "maxSize": "176 kB"
     }
   ],
   "keywords": [
diff --git a/src/js/components/Box/Box.js b/src/js/components/Box/Box.js
index 16378a08378..a07abd585ed 100644
--- a/src/js/components/Box/Box.js
+++ b/src/js/components/Box/Box.js
@@ -13,10 +13,13 @@ import { Keyboard } from '../Keyboard';
 
 import { StyledBox, StyledBoxGap } from './StyledBox';
 import { BoxPropTypes } from './propTypes';
+import { ResponsiveContainer } from './ResponsiveContainer';
 import { SkeletonContext, useSkeleton } from '../Skeleton';
 import { AnnounceContext } from '../../contexts/AnnounceContext';
 import { OptionsContext } from '../../contexts/OptionsContext';
+import { ResponsiveContainerContext } from '../../contexts';
 import { useThemeValue } from '../../utils/useThemeValue';
+import { supportsContainerQueries } from '../../utils/responsive';
 
 const Box = forwardRef(
   (
@@ -35,7 +38,7 @@ const Box = forwardRef(
       onClick,
       onFocus,
       overflow, // munged to avoid styled-components putting it in the DOM
-      responsive = true,
+      responsive: responsiveProp = true,
       tag,
       as,
       wrap, // munged to avoid styled-components putting it in the DOM,
@@ -54,6 +57,10 @@ const Box = forwardRef(
 
     const skeleton = useSkeleton();
 
+    const responsiveContainer = useContext(ResponsiveContainerContext);
+    const responsive =
+      responsiveContainer && responsiveProp ? 'container' : responsiveProp;
+
     let background = backgroundProp;
 
     const announce = useContext(AnnounceContext);
@@ -249,6 +256,16 @@ const Box = forwardRef(
       </StyledBox>
     );
 
+    if (responsiveProp === 'container') {
+      if (supportsContainerQueries()) {
+        content = <ResponsiveContainer>{content}</ResponsiveContainer>;
+      } else {
+        console.warn(
+          '<Box responsive="container"> requires styled-components v6 or later',
+        );
+      }
+    }
+
     if (onClick) {
       content = <Keyboard onEnter={onClick}>{content}</Keyboard>;
     }
diff --git a/src/js/components/Box/ResponsiveContainer.js b/src/js/components/Box/ResponsiveContainer.js
new file mode 100644
index 00000000000..eb80959d1c0
--- /dev/null
+++ b/src/js/components/Box/ResponsiveContainer.js
@@ -0,0 +1,63 @@
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import {
+  ResponsiveContainerContext,
+  ResponsiveContext,
+  ThemeContext,
+} from '../../contexts';
+import { StyledResponsiveContainer } from './StyledResponsiveContainer';
+import { deviceResponsive, getBreakpoint } from '../../utils';
+
+const responsiveContainerValue = true;
+
+export const ResponsiveContainer = ({ children }) => {
+  const theme = useContext(ThemeContext);
+  const ref = useRef(undefined);
+  const [stateResponsive, setStateResponsive] = useState();
+
+  useEffect(() => {
+    const element = ref?.current;
+
+    if (!element) return undefined;
+
+    let resizeObserver;
+    if (typeof window !== 'undefined' && window.ResizeObserver) {
+      resizeObserver = new window.ResizeObserver((entries) => {
+        window.requestAnimationFrame(() => {
+          if (!Array.isArray(entries) || !entries.length) {
+            return;
+          }
+          const size = entries[0]?.borderBoxSize?.[0]?.inlineSize;
+          if (size) {
+            setStateResponsive(getBreakpoint(size, theme));
+          }
+        });
+      });
+
+      resizeObserver.observe(element);
+    } else {
+      // fallback for server side rendering
+      const { width } = element.getBoundingClientRect();
+      setStateResponsive(getBreakpoint(width, theme));
+    }
+    return () => {
+      if (resizeObserver) {
+        resizeObserver.disconnect();
+      }
+    };
+  }, [theme]);
+
+  const responsive =
+    stateResponsive ||
+    deviceResponsive(navigator.userAgent, theme) ||
+    theme?.global?.deviceBreakpoints.tablet;
+
+  return (
+    <ResponsiveContext.Provider value={responsive}>
+      <ResponsiveContainerContext.Provider value={responsiveContainerValue}>
+        <StyledResponsiveContainer ref={ref}>
+          {children}
+        </StyledResponsiveContainer>
+      </ResponsiveContainerContext.Provider>
+    </ResponsiveContext.Provider>
+  );
+};
diff --git a/src/js/components/Box/StyledBox.js b/src/js/components/Box/StyledBox.js
index 7ae769bf4ed..b3ef578b6d9 100644
--- a/src/js/components/Box/StyledBox.js
+++ b/src/js/components/Box/StyledBox.js
@@ -46,7 +46,7 @@ const basisStyle = css`
 // https://stackoverflow.com/questions/36247140/why-doesnt-flex-item-shrink-past-content-size
 // we assume we are in the context of a Box going the other direction
 // TODO: revisit this
-const directionStyle = (direction, theme) => {
+const directionStyle = (direction, responsive, theme) => {
   const styles = [
     css`
       min-width: 0;
@@ -69,6 +69,7 @@ const directionStyle = (direction, theme) => {
         justify-content: flex-start;
         align-items: stretch;
       `,
+          responsive,
         ),
       );
     }
@@ -207,13 +208,19 @@ const gapStyle = (directionProp, gap, responsive, wrap, theme) => {
         };`,
       );
       if (responsiveMetric) {
-        styles.push(breakpointStyle(breakpoint, `gap: ${responsiveMetric};`));
+        styles.push(
+          breakpointStyle(breakpoint, `gap: ${responsiveMetric};`, responsive),
+        );
       }
     } else if (gap.row !== undefined) {
       styles.push(`row-gap: ${theme.global.edgeSize[gap.row] || gap.row};`);
       if (responsiveMetric) {
         styles.push(
-          breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`),
+          breakpointStyle(
+            breakpoint,
+            `row-gap: ${responsiveMetric};`,
+            responsive,
+          ),
         );
       }
     } else if (gap.column !== undefined) {
@@ -222,14 +229,24 @@ const gapStyle = (directionProp, gap, responsive, wrap, theme) => {
       );
       if (responsiveMetric) {
         styles.push(
-          breakpointStyle(breakpoint, `column-gap: ${responsiveMetric};`),
+          breakpointStyle(
+            breakpoint,
+            `column-gap: ${responsiveMetric};`,
+            responsive,
+          ),
         );
       }
     }
   } else if (directionProp === 'column' || directionProp === 'column-reverse') {
     styles.push(`row-gap: ${metric};`);
     if (responsiveMetric) {
-      styles.push(breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`));
+      styles.push(
+        breakpointStyle(
+          breakpoint,
+          `row-gap: ${responsiveMetric};`,
+          responsive,
+        ),
+      );
     }
   } else {
     styles.push(`column-gap: ${metric};`);
@@ -237,15 +254,18 @@ const gapStyle = (directionProp, gap, responsive, wrap, theme) => {
     if (responsiveMetric) {
       if (directionProp === 'row' || directionProp === 'row-reverse') {
         styles.push(
-          breakpointStyle(breakpoint, `column-gap: ${responsiveMetric};`),
+          breakpointStyle(
+            breakpoint,
+            `column-gap: ${responsiveMetric};`,
+            responsive,
+          ),
         );
       } else if (directionProp === 'row-responsive') {
         styles.push(
           breakpointStyle(
             breakpoint,
-            `
-          row-gap: ${responsiveMetric};
-        `,
+            `row-gap: ${responsiveMetric};`,
+            responsive,
           ),
         );
       }
@@ -268,7 +288,8 @@ const StyledBox = styled.div.withConfig(styledComponentsConfig)`
   ${(props) =>
     props.border && borderStyle(props.border, props.responsive, props.theme)}
   ${(props) =>
-    props.directionProp && directionStyle(props.directionProp, props.theme)}
+    props.directionProp &&
+    directionStyle(props.directionProp, props.responsive, props.theme)}
   ${(props) => props.heightProp && heightStyle(props.heightProp, props.theme)}
   ${(props) => props.widthProp && widthStyle(props.widthProp, props.theme)}
   ${(props) => props.flex !== undefined && flexStyle}
@@ -318,13 +339,21 @@ const gapGapStyle = (directionProp, gap, responsive, border, theme) => {
   if (directionProp === 'column' || directionProp === 'column-reverse') {
     styles.push(`height: ${metric};`);
     if (responsiveMetric) {
-      styles.push(breakpointStyle(breakpoint, `height: ${responsiveMetric};`));
+      styles.push(
+        breakpointStyle(breakpoint, `height: ${responsiveMetric};`, responsive),
+      );
     }
   } else {
     styles.push(`width: ${metric};`);
     if (responsiveMetric) {
       if (directionProp === 'row' || directionProp === 'row-reverse') {
-        styles.push(breakpointStyle(breakpoint, `width: ${responsiveMetric};`));
+        styles.push(
+          breakpointStyle(
+            breakpoint,
+            `width: ${responsiveMetric};`,
+            responsive,
+          ),
+        );
       } else if (directionProp === 'row-responsive') {
         styles.push(
           breakpointStyle(
@@ -333,6 +362,7 @@ const gapGapStyle = (directionProp, gap, responsive, border, theme) => {
           width: auto;
           height: ${responsiveMetric};
         `,
+            responsive,
           ),
         );
       }
@@ -378,6 +408,7 @@ const gapGapStyle = (directionProp, gap, responsive, border, theme) => {
               content: '';
               top: ${responsiveBorderOffset};
             }`,
+            responsive,
           ),
         );
       }
@@ -408,6 +439,7 @@ const gapGapStyle = (directionProp, gap, responsive, border, theme) => {
                 content: '';
                 left: ${responsiveBorderOffset};
               }`,
+              responsive,
             ),
           );
         } else if (directionProp === 'row-responsive') {
@@ -426,6 +458,7 @@ const gapGapStyle = (directionProp, gap, responsive, border, theme) => {
                 border-left: none;
                 ${responsiveBorderStyle(adjustedBorder2, theme)}
               }`,
+              responsive,
             ),
           );
         }
diff --git a/src/js/components/Box/StyledResponsiveContainer.js b/src/js/components/Box/StyledResponsiveContainer.js
new file mode 100644
index 00000000000..116ac3c96fc
--- /dev/null
+++ b/src/js/components/Box/StyledResponsiveContainer.js
@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+import { styledComponentsConfig } from '../../utils/styles';
+
+export const StyledResponsiveContainer = styled.div.withConfig(
+  styledComponentsConfig,
+)`
+  container-type: inline-size;
+`;
diff --git a/src/js/components/Box/index.d.ts b/src/js/components/Box/index.d.ts
index 017157f8b61..7cf72e91ec3 100644
--- a/src/js/components/Box/index.d.ts
+++ b/src/js/components/Box/index.d.ts
@@ -64,7 +64,7 @@ export interface BoxProps {
       }
     | string;
   pad?: PadType;
-  responsive?: boolean;
+  responsive?: boolean | 'container';
   round?: RoundType;
   skeleton?: SkeletonType;
   tag?: PolymorphicType;
diff --git a/src/js/components/Box/propTypes.js b/src/js/components/Box/propTypes.js
index 14a10ef9ba3..8bf71098e39 100644
--- a/src/js/components/Box/propTypes.js
+++ b/src/js/components/Box/propTypes.js
@@ -170,7 +170,10 @@ if (process.env.NODE_ENV !== 'production') {
     onClick: PropTypes.func,
     overflow: overflowPropType,
     pad: padPropType,
-    responsive: PropTypes.bool,
+    responsive: PropTypes.oneOfType([
+      PropTypes.bool,
+      PropTypes.oneOf(['container']),
+    ]),
     round: roundPropType,
     skeleton: skeletonPropType,
     tag: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
diff --git a/src/js/components/Box/stories/ResponsiveContainer.stories.js b/src/js/components/Box/stories/ResponsiveContainer.stories.js
new file mode 100644
index 00000000000..3b41390edc7
--- /dev/null
+++ b/src/js/components/Box/stories/ResponsiveContainer.stories.js
@@ -0,0 +1,118 @@
+import React, { useContext, useEffect } from 'react';
+
+import {
+  Box,
+  Button,
+  Heading,
+  ResponsiveContext,
+  Text,
+  ThemeContext,
+} from 'grommet';
+import { FormNext, FormPrevious } from 'grommet-icons';
+
+// Some content to show the effects of responsive layout.
+// The heading will change size at the small breakpoint
+// and the child Text items will switch from row to column
+// layout at the small breakpoint.
+const Content = ({ title, children }) => {
+  const size = useContext(ResponsiveContext);
+  const theme = useContext(ThemeContext);
+  const smallBreakpoint = theme.global.breakpoints.small.value;
+  return (
+    <Box pad="medium">
+      <Heading>{title}</Heading>
+      <Box direction="row-responsive" gap={{ column: 'medium', row: 'xsmall' }}>
+        <Text>I am row-responsive</Text>
+        <Text>Small breakpoint: {smallBreakpoint}</Text>
+        {children}
+        <Text>
+          Current size:&nbsp;<Text weight="bold">{size}</Text>{' '}
+        </Text>
+      </Box>
+    </Box>
+  );
+};
+
+const App = ({ title, responsive = true }) => {
+  const widthRef = React.useRef(undefined);
+  const containerRef = React.useRef(undefined);
+
+  // track the width of the container just so we can display it as it changes
+  useEffect(() => {
+    let resizeObserver;
+    const element = containerRef.current;
+    if (typeof window !== 'undefined' && window.ResizeObserver) {
+      resizeObserver = new window.ResizeObserver((entries) => {
+        const entry = entries[0].borderBoxSize[0];
+        const width = entry?.inlineSize;
+        if (widthRef.current) {
+          widthRef.current.innerText = `Container width: ${width.toFixed()}px`;
+        }
+      });
+      if (element) {
+        resizeObserver.observe(containerRef.current);
+      }
+    } else {
+      // fallback for server side rendering
+      const { width } = containerRef.current.getBoundingClientRect();
+      if (widthRef.current) {
+        widthRef.current.innerText = `Container width: ${width.toFixed()}px`;
+      }
+    }
+    return () => {
+      if (resizeObserver && element) {
+        resizeObserver.unobserve(element);
+      }
+    };
+  });
+
+  return (
+    <Box ref={containerRef} gap="medium" flex="grow" responsive={responsive}>
+      <Content title={title}>
+        <Text ref={widthRef} />
+      </Content>
+    </Box>
+  );
+};
+
+const SidePanel = () => {
+  const [open, setOpen] = React.useState(true);
+  const onToggle = () => setOpen((prev) => !prev);
+  return (
+    <Box direction="row" align="start">
+      <Button
+        icon={open ? <FormNext /> : <FormPrevious />}
+        onClick={onToggle}
+      />
+      {open && (
+        <Box
+          width="large"
+          border={{ side: 'left' }}
+          align="center"
+          justify="center"
+          fill="vertical"
+        >
+          <Text>Right hand side</Text>
+        </Box>
+      )}
+    </Box>
+  );
+};
+
+export const ResponsiveContainer = () => (
+  <Box direction="row" height="100vh" width="100vw" overflow="hidden">
+    <Box overflow="auto" fill="horizontal">
+      <App title="Responsive to container" responsive="container" />
+      <App title="Responsive to window" />
+    </Box>
+    <SidePanel />
+  </Box>
+);
+
+ResponsiveContainer.storyName = 'Responsive container';
+ResponsiveContainer.parameters = {
+  chromatic: { disable: true },
+};
+export default {
+  title: 'Layout/Box/Responsive container',
+};
diff --git a/src/js/components/Drop/StyledDrop.js b/src/js/components/Drop/StyledDrop.js
index 020c09dee78..0cfe23f373c 100644
--- a/src/js/components/Drop/StyledDrop.js
+++ b/src/js/components/Drop/StyledDrop.js
@@ -66,7 +66,8 @@ const StyledDrop = styled(Box)`
 
   ${(props) =>
     !props.plain &&
-    ((props.round && roundStyle(props.round, true, props.theme)) ||
+    ((props.round &&
+      roundStyle(props.round, props.responsive || true, props.theme)) ||
       `border-radius: ${props.theme.global.drop.border.radius};`)}
 
   position: fixed;
diff --git a/src/js/components/Grid/Grid.js b/src/js/components/Grid/Grid.js
index 6f7cc16cd73..577cba49482 100644
--- a/src/js/components/Grid/Grid.js
+++ b/src/js/components/Grid/Grid.js
@@ -1,8 +1,9 @@
-import React, { forwardRef } from 'react';
+import React, { forwardRef, useContext } from 'react';
 
 import { StyledGrid } from './StyledGrid';
 import { GridPropTypes } from './propTypes';
 import { useThemeValue } from '../../utils/useThemeValue';
+import { ResponsiveContainerContext } from '../../contexts';
 
 const Grid = forwardRef((props, ref) => {
   const {
@@ -11,7 +12,7 @@ const Grid = forwardRef((props, ref) => {
     border,
     fill, // munged to avoid styled-components putting it in the DOM
     height, // munged to avoid styled-components putting it in the DOM
-    responsive = true,
+    responsive: responsiveProp = true,
     rows, // munged to avoid styled-components putting it in the DOM
     tag,
     as,
@@ -19,6 +20,9 @@ const Grid = forwardRef((props, ref) => {
     ...rest
   } = props;
   const { passThemeFlag } = useThemeValue();
+  const responsiveContainer = useContext(ResponsiveContainerContext);
+  const responsive =
+    responsiveContainer && responsiveProp ? 'container' : responsiveProp;
 
   return (
     <StyledGrid
diff --git a/src/js/components/Grommet/Grommet.js b/src/js/components/Grommet/Grommet.js
index a3d288d13e9..ccd6fb833e9 100644
--- a/src/js/components/Grommet/Grommet.js
+++ b/src/js/components/Grommet/Grommet.js
@@ -10,8 +10,8 @@ import {
 import {
   deepMerge,
   backgroundIsDark,
+  deviceResponsive,
   getBreakpoint,
-  getDeviceBreakpoint,
   normalizeColor,
   useForwardedRef,
 } from '../../utils';
@@ -28,27 +28,6 @@ const FullGlobalStyle = createGlobalStyle`
   body { margin: 0; }
 `;
 
-const deviceResponsive = (userAgent, theme) => {
-  // log('--deviceResponsive', userAgent, theme);
-  /*
-   * Regexes provided for mobile and tablet detection are meant to replace
-   * a full-featured specific library due to contributing a considerable size
-   * into the bundle.
-   *
-   * User agents found https://deviceatlas.com/blog/list-of-user-agent-strings
-   */
-  if (userAgent) {
-    if (/(tablet|ipad|playbook|silk)|(android(?!.*mobile))/i.test(userAgent)) {
-      return getDeviceBreakpoint('tablet', theme);
-    }
-    if (/Mobile|iPhone|Android/.test(userAgent)) {
-      return getDeviceBreakpoint('phone', theme);
-    }
-    return getDeviceBreakpoint('computer', theme);
-  }
-  return undefined;
-};
-
 const defaultOptions = {};
 
 const Grommet = forwardRef((props, ref) => {
diff --git a/src/js/components/Heading/Heading.js b/src/js/components/Heading/Heading.js
index ba0eb09160e..4d22fb32f56 100644
--- a/src/js/components/Heading/Heading.js
+++ b/src/js/components/Heading/Heading.js
@@ -1,4 +1,4 @@
-import React, { forwardRef, useState } from 'react';
+import React, { forwardRef, useContext, useState } from 'react';
 import { useLayoutEffect } from '../../utils/use-isomorphic-layout-effect';
 
 import { StyledHeading } from './StyledHeading';
@@ -7,6 +7,7 @@ import { useForwardedRef } from '../../utils';
 import { useSkeleton } from '../Skeleton';
 import { HeadingSkeleton } from './HeadingSkeleton';
 import { useThemeValue } from '../../utils/useThemeValue';
+import { ResponsiveContainerContext } from '../../contexts';
 
 const Heading = forwardRef(
   (
@@ -16,7 +17,7 @@ const Heading = forwardRef(
       fill,
       level = 1,
       overflowWrap: overflowWrapProp,
-      responsive = true,
+      responsive: responsiveProp = true,
       weight,
       ...rest
     },
@@ -29,6 +30,9 @@ const Heading = forwardRef(
       overflowWrapProp || 'break-word',
     );
 
+    const responsiveContainer = useContext(ResponsiveContainerContext);
+    const responsive =
+      responsiveContainer && responsiveProp ? 'container' : responsiveProp;
     const skeleton = useSkeleton();
 
     // handle overflowWrap of heading
diff --git a/src/js/components/Heading/HeadingSkeleton.js b/src/js/components/Heading/HeadingSkeleton.js
index a89cbafb74b..e6543e038d6 100644
--- a/src/js/components/Heading/HeadingSkeleton.js
+++ b/src/js/components/Heading/HeadingSkeleton.js
@@ -2,16 +2,40 @@ import React, { forwardRef } from 'react';
 import { Skeleton } from '../Skeleton';
 import { useThemeValue } from '../../utils/useThemeValue';
 
-const HeadingSkeleton = forwardRef(({ as, level = 1, size }, ref) => {
-  const { theme } = useThemeValue();
+const HeadingSkeleton = forwardRef(
+  ({ as, level = 1, size = 'medium', responsive }, ref) => {
+    const { theme } = useThemeValue();
 
-  const levelStyle = theme.heading.level[level];
-  const data = levelStyle?.[size || 'medium'];
-  const height = data ? data.height : size;
-  return (
-    <Skeleton as={as} ref={ref} height={height} {...theme.heading.skeleton} />
-  );
-});
+    const height = theme.heading.level[level]?.[size]?.height || size;
+
+    let responsiveSize;
+    if (responsive && theme.heading.responsiveBreakpoint) {
+      const breakpoint =
+        theme.global.breakpoints[theme.heading.responsiveBreakpoint];
+      if (breakpoint) {
+        const responsiveHeight = theme.heading.level[level + 1]
+          ? theme.heading.level[level + 1][size]?.height
+          : theme.heading.level[level][size]?.height;
+
+        responsiveSize = {
+          breakpoint,
+          height: responsiveHeight || height,
+        };
+      }
+    }
+
+    return (
+      <Skeleton
+        as={as}
+        ref={ref}
+        height={height}
+        responsive={responsive}
+        responsiveSize={responsiveSize}
+        {...theme.heading.skeleton}
+      />
+    );
+  },
+);
 
 HeadingSkeleton.displayName = 'HeadingSkeleton';
 
diff --git a/src/js/components/Heading/StyledHeading.js b/src/js/components/Heading/StyledHeading.js
index ff14e423b76..bcd91368a2e 100644
--- a/src/js/components/Heading/StyledHeading.js
+++ b/src/js/components/Heading/StyledHeading.js
@@ -44,6 +44,7 @@ const sizeStyle = (props) => {
             line-height: ${responsiveData.height};
             max-width: ${(props.fillProp && 'none') || responsiveData.maxWidth};
           `,
+              props.responsive,
             ),
           );
         }
diff --git a/src/js/components/Notification/Notification.js b/src/js/components/Notification/Notification.js
index ecf1eb1de12..c11d217fd9d 100644
--- a/src/js/components/Notification/Notification.js
+++ b/src/js/components/Notification/Notification.js
@@ -23,7 +23,7 @@ const Message = ({ fill, direction, ...rest }) =>
   ) : (
     <Paragraph {...rest} fill={fill || false} />
   );
-  
+
 const adaptThemeStyle = (value, theme) => {
   let textStyle = value;
   let closeButtonStyle = value;
diff --git a/src/js/components/Skeleton/Skeleton.js b/src/js/components/Skeleton/Skeleton.js
index ef80df1a8a6..97e84c27f7b 100644
--- a/src/js/components/Skeleton/Skeleton.js
+++ b/src/js/components/Skeleton/Skeleton.js
@@ -1,12 +1,20 @@
-import React, { forwardRef } from 'react';
+import React, { forwardRef, useContext } from 'react';
 import { SkeletonPropTypes } from './propTypes';
 import { useSkeleton } from './SkeletonContext';
 import { StyledSkeleton } from './StyledSkeleton';
 import { useThemeValue } from '../../utils/useThemeValue';
+import { ResponsiveContainerContext } from '../../contexts';
 
 const Skeleton = forwardRef(
   (
-    { as, colors: colorsProp, width: widthProp, height: heightProp, ...rest },
+    {
+      as,
+      colors: colorsProp,
+      width: widthProp,
+      height: heightProp,
+      responsive: responsiveProp,
+      ...rest
+    },
     ref,
   ) => {
     const { theme, passThemeFlag } = useThemeValue();
@@ -14,6 +22,10 @@ const Skeleton = forwardRef(
     const skeleton = useSkeleton();
     const depth = skeleton?.depth || 0;
 
+    const responsiveContainer = useContext(ResponsiveContainerContext);
+    const responsive =
+      responsiveContainer && responsiveProp ? 'container' : responsiveProp;
+
     const colors = colorsProp || theme?.skeleton?.colors;
     const themeColors = colors[theme.dark ? 'dark' : 'light'];
     const background = themeColors[(depth + 1) % themeColors.length];
@@ -25,6 +37,7 @@ const Skeleton = forwardRef(
         background={background}
         widthProp={widthProp}
         heightProp={heightProp}
+        responsive={responsive}
         {...passThemeFlag}
         {...rest}
       />
diff --git a/src/js/components/Skeleton/StyledSkeleton.js b/src/js/components/Skeleton/StyledSkeleton.js
index a584e3ec7da..711d7873de5 100644
--- a/src/js/components/Skeleton/StyledSkeleton.js
+++ b/src/js/components/Skeleton/StyledSkeleton.js
@@ -9,6 +9,7 @@ import {
   widthStyle,
   styledComponentsConfig,
 } from '../../utils';
+import { breakpointStyle } from '../../utils/mixins';
 
 // Styling a div directly rather than just using
 // a Box since Box itself will react to a SkeletonContext
@@ -37,6 +38,14 @@ export const StyledSkeleton = styled.div.withConfig(styledComponentsConfig)`
       props.theme.box.responsiveBreakpoint,
       props.theme,
     )}
-  ${(props) => props.round && roundStyle(props.round, true, props.theme)}
+  ${(props) =>
+    props.round && roundStyle(props.round, props.responsive, props.theme)}
   ${(props) => props.theme?.skeleton?.extend}
+  ${(props) =>
+    props.responsiveSize &&
+    breakpointStyle(
+      props.responsiveSize.breakpoint,
+      heightStyle(props.responsiveSize.height, props.theme),
+      props.responsive,
+    )}
 `;
diff --git a/src/js/components/Skeleton/__tests__/__snapshots__/Skeleton-test.tsx.snap b/src/js/components/Skeleton/__tests__/__snapshots__/Skeleton-test.tsx.snap
index 0fb56b1e740..77ca6395193 100644
--- a/src/js/components/Skeleton/__tests__/__snapshots__/Skeleton-test.tsx.snap
+++ b/src/js/components/Skeleton/__tests__/__snapshots__/Skeleton-test.tsx.snap
@@ -244,6 +244,12 @@ exports[`Skeleton Box skeleton loading 1`] = `
   overflow-wrap: break-word;
 }
 
+@media only screen and (max-width: 768px) {
+  .c3 {
+    height: 40px;
+  }
+}
+
 @media only screen and (max-width: 768px) {
   .c7 {
     height: 3px;
@@ -400,6 +406,12 @@ exports[`Skeleton Box skeleton sizes loading 1`] = `
   overflow-wrap: break-word;
 }
 
+@media only screen and (max-width: 768px) {
+  .c3 {
+    height: 28px;
+  }
+}
+
 @media only screen and (max-width: 768px) {
   .c7 {
     height: 3px;
@@ -634,12 +646,6 @@ exports[`Skeleton with styling props 1`] = `
   -webkit-font-smoothing: antialiased;
 }
 
-@media only screen and (max-width: 768px) {
-  .c1 {
-    border-radius: 6px;
-  }
-}
-
 <div
     class="c0"
   >
diff --git a/src/js/contexts/ResponsiveContainerContext.js b/src/js/contexts/ResponsiveContainerContext.js
new file mode 100644
index 00000000000..d636fcf4c74
--- /dev/null
+++ b/src/js/contexts/ResponsiveContainerContext.js
@@ -0,0 +1,3 @@
+import React from 'react';
+
+export const ResponsiveContainerContext = React.createContext(false);
diff --git a/src/js/contexts/index.js b/src/js/contexts/index.js
index cbc5935962d..37cefd6d423 100644
--- a/src/js/contexts/index.js
+++ b/src/js/contexts/index.js
@@ -3,5 +3,6 @@ export * from './AnnounceContext';
 export * from './ContainerTargetContext';
 export * from './DataContext';
 export * from './ResponsiveContext';
+export * from './ResponsiveContainerContext';
 export * from './RootsContext';
 export * from './ThemeContext';
diff --git a/src/js/utils/border.js b/src/js/utils/border.js
index b93e36dad82..0e786859917 100644
--- a/src/js/utils/border.js
+++ b/src/js/utils/border.js
@@ -64,12 +64,12 @@ export const borderStyle = (borderData, responsive, theme) => {
     ) {
       styles.push(`border-${side}: ${value};`);
       if (responsiveStyle) {
-        styles.push(breakpointStyle(breakpoint, responsiveStyle));
+        styles.push(breakpointStyle(breakpoint, responsiveStyle, responsive));
       }
     } else if (side === 'end' || side === 'start') {
       styles.push(css`border-inline-${side}: ${value};`);
       if (responsiveStyle) {
-        styles.push(breakpointStyle(breakpoint, responsiveStyle));
+        styles.push(breakpointStyle(breakpoint, responsiveStyle, responsive));
       }
     } else if (side === 'vertical') {
       styles.push(css`
@@ -77,7 +77,7 @@ export const borderStyle = (borderData, responsive, theme) => {
         border-right: ${value};
       `);
       if (responsiveStyle) {
-        styles.push(breakpointStyle(breakpoint, responsiveStyle));
+        styles.push(breakpointStyle(breakpoint, responsiveStyle, responsive));
       }
     } else if (side === 'horizontal') {
       styles.push(css`
@@ -85,7 +85,7 @@ export const borderStyle = (borderData, responsive, theme) => {
         border-bottom: ${value};
       `);
       if (responsiveStyle) {
-        styles.push(breakpointStyle(breakpoint, responsiveStyle));
+        styles.push(breakpointStyle(breakpoint, responsiveStyle, responsive));
       }
     } else if (side === 'between') {
       // no-op
@@ -96,7 +96,7 @@ export const borderStyle = (borderData, responsive, theme) => {
         `,
       );
       if (responsiveStyle) {
-        styles.push(breakpointStyle(breakpoint, responsiveStyle));
+        styles.push(breakpointStyle(breakpoint, responsiveStyle, responsive));
       }
     }
     borderStyles.push(styles);
diff --git a/src/js/utils/mixins.js b/src/js/utils/mixins.js
index 34c790156ff..c1a3c6b8943 100644
--- a/src/js/utils/mixins.js
+++ b/src/js/utils/mixins.js
@@ -29,12 +29,23 @@ export const fontSize = (size, lineHeight) => css`
     }px`};
 `;
 
-export const breakpointStyle = (breakpoint, content) => css`
-  @media only screen ${breakpoint.value &&
-    `and (max-width: ${breakpoint.value}px)`} {
-    ${content};
-  }
-`;
+export const breakpointStyle = (breakpoint, content, responsive) => {
+  const st =
+    responsive === 'container'
+      ? css`
+          @container ${breakpoint.value &&
+          `(max-width: ${breakpoint.value}px)`} {
+            ${content};
+          }
+        `
+      : css`
+          @media only screen ${breakpoint.value &&
+            `and (max-width: ${breakpoint.value}px)`} {
+            ${content};
+          }
+        `;
+  return st;
+};
 
 export const findAllByType = (component, type) => {
   let matches = [];
diff --git a/src/js/utils/responsive.js b/src/js/utils/responsive.js
index 7ec59d8d593..5c4aef7f004 100644
--- a/src/js/utils/responsive.js
+++ b/src/js/utils/responsive.js
@@ -1,3 +1,5 @@
+import styled from 'styled-components';
+
 export const getBreakpoint = (viewportWidth, theme) => {
   const sortedBreakpoints = Object.keys(theme.global.breakpoints).sort(
     (a, b) => {
@@ -40,3 +42,34 @@ export const getBreakpointStyle = (theme, breakpointSize) => {
 // for checks that look for a small screen size, flag for xsmall
 // as well since we use xsmall in the hpe theme
 export const isSmall = (size) => ['xsmall', 'small'].includes(size);
+
+export const deviceResponsive = (userAgent, theme) => {
+  // log('--deviceResponsive', userAgent, theme);
+  /*
+   * Regexes provided for mobile and tablet detection are meant to replace
+   * a full-featured specific library due to contributing a considerable size
+   * into the bundle.
+   *
+   * User agents found https://deviceatlas.com/blog/list-of-user-agent-strings
+   */
+  if (userAgent) {
+    if (/(tablet|ipad|playbook|silk)|(android(?!.*mobile))/i.test(userAgent)) {
+      return getDeviceBreakpoint('tablet', theme);
+    }
+    if (/Mobile|iPhone|Android/.test(userAgent)) {
+      return getDeviceBreakpoint('phone', theme);
+    }
+    return getDeviceBreakpoint('computer', theme);
+  }
+  return undefined;
+};
+
+export const supportsContainerQueries = () => {
+  // styled-components v6 and later do not have a withComponent
+  // method. We need v6 or later to support container queries.
+  const comp = styled.div`
+    display: flex;
+  `;
+  const isPreV6 = typeof comp.withComponent === 'function';
+  return !isPreV6;
+};
diff --git a/src/js/utils/styles.js b/src/js/utils/styles.js
index 9c9fbbf5beb..dfbf2b8855f 100644
--- a/src/js/utils/styles.js
+++ b/src/js/utils/styles.js
@@ -59,6 +59,7 @@ export const edgeStyle = (
             `
         ${kind}: ${breakpoint.edgeSize[data] || data};
       `,
+            responsive,
           )
         : ''};
     `;
@@ -87,6 +88,7 @@ export const edgeStyle = (
             `
         ${kind}: ${breakpoint.edgeSize[value] || value};
       `,
+            responsive,
           )
         : ''};
     `;
@@ -103,6 +105,7 @@ export const edgeStyle = (
           ${kind}-left: ${breakpoint.edgeSize[horizontal] || horizontal};
           ${kind}-right: ${breakpoint.edgeSize[horizontal] || horizontal};
         `,
+            responsive,
           )
         : ''};
     `);
@@ -118,6 +121,7 @@ export const edgeStyle = (
           ${kind}-top: ${breakpoint.edgeSize[vertical] || vertical};
           ${kind}-bottom: ${breakpoint.edgeSize[vertical] || vertical};
         `,
+            responsive,
           )
         : ''};
     `);
@@ -131,6 +135,7 @@ export const edgeStyle = (
             `
           ${kind}-top: ${breakpoint.edgeSize[top] || top};
         `,
+            responsive,
           )
         : ''};
     `);
@@ -144,6 +149,7 @@ export const edgeStyle = (
             `
           ${kind}-bottom: ${breakpoint.edgeSize[bottom] || bottom};
         `,
+            responsive,
           )
         : ''};
     `);
@@ -157,6 +163,7 @@ export const edgeStyle = (
             `
           ${kind}-left: ${breakpoint.edgeSize[left] || left};
         `,
+            responsive,
           )
         : ''};
     `);
@@ -170,6 +177,7 @@ export const edgeStyle = (
             `
           ${kind}-right: ${breakpoint.edgeSize[right] || right};
         `,
+            responsive,
           )
         : ''};
     `);
@@ -185,6 +193,7 @@ export const edgeStyle = (
               breakpoint.edgeSize[data.start] || data.start
             };
         `,
+            responsive,
           )
         : ''};
     `);
@@ -198,6 +207,7 @@ export const edgeStyle = (
             `
           ${kind}-inline-end: ${breakpoint.edgeSize[data.end] || data.end};
         `,
+            responsive,
           )
         : ''};
     `);
@@ -727,6 +737,7 @@ export const roundStyle = (data, responsive, theme) => {
           border-top-left-radius: ${responsiveSize};
           border-top-right-radius: ${responsiveSize};
         `,
+            responsive,
           ),
         );
       }
@@ -743,6 +754,7 @@ export const roundStyle = (data, responsive, theme) => {
           border-bottom-left-radius: ${responsiveSize};
           border-bottom-right-radius: ${responsiveSize};
         `,
+            responsive,
           ),
         );
       }
@@ -759,6 +771,7 @@ export const roundStyle = (data, responsive, theme) => {
           border-top-left-radius: ${responsiveSize};
           border-bottom-left-radius: ${responsiveSize};
         `,
+            responsive,
           ),
         );
       }
@@ -775,6 +788,7 @@ export const roundStyle = (data, responsive, theme) => {
           border-top-right-radius: ${responsiveSize};
           border-bottom-right-radius: ${responsiveSize};
         `,
+            responsive,
           ),
         );
       }
@@ -789,6 +803,7 @@ export const roundStyle = (data, responsive, theme) => {
             `
           border-${data.corner}-radius: ${responsiveSize};
         `,
+            responsive,
           ),
         );
       }
@@ -803,6 +818,7 @@ export const roundStyle = (data, responsive, theme) => {
             `
           border-radius: ${responsiveSize};
         `,
+            responsive,
           ),
         );
       }
@@ -821,6 +837,7 @@ export const roundStyle = (data, responsive, theme) => {
           `
         border-radius: ${responsiveSize};
       `,
+          responsive,
         ),
       );
     }


Reviews:
user: login: taysea
body: Thanks for getting this started, Mike. I'm excited about it. A couple of initial questions about API/expected behaviors.

1. Do we want to introduce an entirely new component, or would it be nice to be able to turn any Box into a responsive container via a prop like `container`?
2. Do we expect responsive styling that typically steps down at window breakpoints (spacing, heading sizing, etc.) to apply to content in responsive containers? I suppose my initial thought was people might what to use ResponsiveContainerContext to drive layout things like Grid columns, etc. but I wasn't necessarily expecting the "sizing down" that happens at window breakpoints to apply to responsive containers.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T18:54:22Z
user: login: jcfilben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T21:43:43Z
user: login: MikeKingdom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T15:46:01Z
user: login: jcfilben
body: Originally you had built responsive container as a separate component. You mentioned the reasoning for that was: 

> Good question and I went back and forth on this. So far I landed on separate component because it adds a lot of special code and useEffects (which themselves can't be conditional, although their contents can be.) Seems like it was a lot of extra stuff in a rather large component. Plus it's something that really is likely only at the top level of an application so far. That said we can certainly discuss more what makes the most sense.

The PR has changed direction since then, I'm not opposed to it being built as a prop within Box but wanted to understand the reasoning for the change in direction.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T22:18:55Z
user: login: MikeKingdom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T22:58:30Z
user: login: MikeKingdom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T23:00:28Z
user: login: jcfilben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T23:10:39Z
user: login: jcfilben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T23:12:00Z
user: login: halocline
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T23:20:23Z
user: login: britt6612
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T16:51:30Z
user: login: britt6612
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-13T16:54:18Z
user: login: MikeKingdom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T16:00:58Z
user: login: MikeKingdom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T16:03:01Z
user: login: MikeKingdom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T16:04:23Z
user: login: MikeKingdom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T16:14:53Z
user: login: britt6612
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T18:49:02Z
user: login: britt6612
body: LGTM 

Thanks Mike looking forward to this!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-14T19:21:14Z
user: login: jcfilben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T20:27:28Z
user: login: MikeKingdom
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T21:41:39Z
user: login: jcfilben
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T15:21:38Z
user: login: jcfilben
body: Looks good!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-17T15:22:21Z

Review comments:
diff_hunk: @@ -29,12 +29,23 @@ export const fontSize = (size, lineHeight) => css`
     }px`};
 `;
 
-export const breakpointStyle = (breakpoint, content) => css`
-  @media only screen ${breakpoint.value &&
-    `and (max-width: ${breakpoint.value}px)`} {
-    ${content};
-  }
-`;
+export const breakpointStyle = (breakpoint, content, responsive) => {
+  const st = 
+  responsive === 'container'
+    ? css`
+        @container ${breakpoint.value && `(max-width: ${breakpoint.value}px)`} {
path: src/js/utils/mixins.js
user: login: jcfilben
body: Will using `@container` cause issues for anyone using v5 of styled-components with Grommet?
created_at: 2025-03-04T21:43:43Z
author_association: COLLABORATOR
diff_hunk: @@ -29,12 +29,23 @@ export const fontSize = (size, lineHeight) => css`
     }px`};
 `;
 
-export const breakpointStyle = (breakpoint, content) => css`
-  @media only screen ${breakpoint.value &&
-    `and (max-width: ${breakpoint.value}px)`} {
-    ${content};
-  }
-`;
+export const breakpointStyle = (breakpoint, content, responsive) => {
+  const st = 
+  responsive === 'container'
+    ? css`
+        @container ${breakpoint.value && `(max-width: ${breakpoint.value}px)`} {
path: src/js/utils/mixins.js
user: login: MikeKingdom
body: If I remember correctly it won't create useful css and simply won't work. Either way we'll have to document that this functionality requires styled-components v6+ if you want to use it
created_at: 2025-03-05T15:46:01Z
author_association: COLLABORATOR
diff_hunk: @@ -54,6 +56,10 @@ const Box = forwardRef(
 
     const skeleton = useSkeleton();
 
+    const responsiveContainer = useContext(ResponsiveContainerContext);
+    const responsive =
+      responsiveContainer && responsiveProp ? 'container' : responsiveProp;
path: src/js/components/Box/Box.js
user: login: jcfilben
body: I'm a little confused about this line, I would expect `responsive={true}` and `responsive='container'` to mean different things. It seems like this line would result in responsive being set to 'container' when the value is `true`.
created_at: 2025-03-10T22:08:19Z
author_association: COLLABORATOR
diff_hunk: @@ -29,12 +29,23 @@ export const fontSize = (size, lineHeight) => css`
     }px`};
 `;
 
-export const breakpointStyle = (breakpoint, content) => css`
-  @media only screen ${breakpoint.value &&
-    `and (max-width: ${breakpoint.value}px)`} {
-    ${content};
-  }
-`;
+export const breakpointStyle = (breakpoint, content, responsive) => {
+  const st =
+    responsive === 'container'
+      ? css`
+          @container ${breakpoint.value &&
path: src/js/utils/mixins.js
user: login: jcfilben
body: Could we add a console info message for people that try to use this feature with styled components v5?
created_at: 2025-03-10T22:15:07Z
author_association: COLLABORATOR
diff_hunk: @@ -54,6 +56,10 @@ const Box = forwardRef(
 
     const skeleton = useSkeleton();
 
+    const responsiveContainer = useContext(ResponsiveContainerContext);
+    const responsive =
+      responsiveContainer && responsiveProp ? 'container' : responsiveProp;
path: src/js/components/Box/Box.js
user: login: MikeKingdom
body: It would. This value is what is passed to the various apis used by the styled components involved (breakpoint css, edge, and round etc.) If `responsiveProp` is true or 'container' (basically not false) this Box needs to do responsive style stuff. If this Box is supposed to be responsive it needs to know whether it's inside a ResponsiveContainerContext (ie some Box way above it in the parentage has `responsive="container"`.) The breakpoint style apis need to know this but can't check a context. So this is how we tell them which type of responsive. 
created_at: 2025-03-10T22:58:30Z
author_association: COLLABORATOR
diff_hunk: @@ -29,12 +29,23 @@ export const fontSize = (size, lineHeight) => css`
     }px`};
 `;
 
-export const breakpointStyle = (breakpoint, content) => css`
-  @media only screen ${breakpoint.value &&
-    `and (max-width: ${breakpoint.value}px)`} {
-    ${content};
-  }
-`;
+export const breakpointStyle = (breakpoint, content, responsive) => {
+  const st =
+    responsive === 'container'
+      ? css`
+          @container ${breakpoint.value &&
path: src/js/utils/mixins.js
user: login: MikeKingdom
body: Any thoughts on how to detect styled-components v5 vs v6?
created_at: 2025-03-10T23:00:28Z
author_association: COLLABORATOR
diff_hunk: @@ -29,12 +29,23 @@ export const fontSize = (size, lineHeight) => css`
     }px`};
 `;
 
-export const breakpointStyle = (breakpoint, content) => css`
-  @media only screen ${breakpoint.value &&
-    `and (max-width: ${breakpoint.value}px)`} {
-    ${content};
-  }
-`;
+export const breakpointStyle = (breakpoint, content, responsive) => {
+  const st =
+    responsive === 'container'
+      ? css`
+          @container ${breakpoint.value &&
path: src/js/utils/mixins.js
user: login: jcfilben
body: I think we could do:
```
import styled from 'styled-components';
console.log(styled.version);
```
created_at: 2025-03-10T23:10:39Z
author_association: COLLABORATOR
diff_hunk: @@ -54,6 +56,10 @@ const Box = forwardRef(
 
     const skeleton = useSkeleton();
 
+    const responsiveContainer = useContext(ResponsiveContainerContext);
+    const responsive =
+      responsiveContainer && responsiveProp ? 'container' : responsiveProp;
path: src/js/components/Box/Box.js
user: login: jcfilben
body: Gotcha, thanks for the explanation
created_at: 2025-03-10T23:11:59Z
author_association: COLLABORATOR
diff_hunk: @@ -29,12 +29,23 @@ export const fontSize = (size, lineHeight) => css`
     }px`};
 `;
 
-export const breakpointStyle = (breakpoint, content) => css`
-  @media only screen ${breakpoint.value &&
-    `and (max-width: ${breakpoint.value}px)`} {
-    ${content};
-  }
-`;
+export const breakpointStyle = (breakpoint, content, responsive) => {
+  const st =
+    responsive === 'container'
+      ? css`
+          @container ${breakpoint.value &&
path: src/js/utils/mixins.js
user: login: halocline
body: I think checking the version directly can lead to build errors for folks who might be doing transforms in their bundling. As alternative to directly checking the version, feature detection is another potential pattern --> detect features that differ between v5 and v6. I've seen folks doing something like:

```
// tests `shouldForwardProp`'s behavior change from v5 to v6

export function detectStyledComponentsVersion() {
  const TestComponent = styled.div.attrs({ testProp: 'mytest' })`
   ...
  `;

  const testDiv = document.createElement('div');
  ReactDOM.render(<TestComponent ref={(el) => {
    if (el) {
      if (el.hasAttribute('testProp')) {
        return "v5 or earlier";
      } else {
        return "v6 or later";
      }
    }
  }} />, testDiv);
  return "";
}
```
created_at: 2025-03-10T23:20:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,63 @@
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import {
+  ResponsiveContainerContext,
+  ResponsiveContext,
+  ThemeContext,
+} from '../../contexts';
+import { StyledResponsiveContainer } from './StyledResponsiveContainer';
+import { deviceResponsive, getBreakpoint } from '../../utils';
+
+const responsiveContainerValue = true;
+
+export const ResponsiveContainer = ({ children }) => {
+  const theme = useContext(ThemeContext);
+  const ref = useRef(undefined);
+  const [stateResponsive, setResponsive] = useState();
path: src/js/components/Box/ResponsiveContainer.js
user: login: britt6612
body: ```suggestion
  const [stateResponsive, setStateResponsive] = useState();
```
created_at: 2025-03-13T16:19:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,63 @@
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import {
+  ResponsiveContainerContext,
+  ResponsiveContext,
+  ThemeContext,
+} from '../../contexts';
+import { StyledResponsiveContainer } from './StyledResponsiveContainer';
+import { deviceResponsive, getBreakpoint } from '../../utils';
+
+const responsiveContainerValue = true;
+
+export const ResponsiveContainer = ({ children }) => {
+  const theme = useContext(ThemeContext);
+  const ref = useRef(undefined);
+  const [stateResponsive, setResponsive] = useState();
path: src/js/components/Box/ResponsiveContainer.js
user: login: britt6612
body: if we change this then have to change below as well
created_at: 2025-03-13T16:19:27Z
author_association: COLLABORATOR
diff_hunk: @@ -634,12 +646,6 @@ exports[`Skeleton with styling props 1`] = `
   -webkit-font-smoothing: antialiased;
 }
 
-@media only screen and (max-width: 768px) {
path: src/js/components/Skeleton/__tests__/__snapshots__/Skeleton-test.tsx.snap
user: login: britt6612
body: this went away because of this change correct?

```
 ${(props) => props.round && roundStyle(props.round, true, props.theme)}
  ${(props) =>
    props.round && roundStyle(props.round, props.responsive, props.theme)}
```
created_at: 2025-03-13T16:47:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,118 @@
+import React, { useContext, useEffect } from 'react';
+
+import {
+  Box,
+  Button,
+  Heading,
+  ResponsiveContext,
+  Text,
+  ThemeContext,
+} from 'grommet';
+import { FormNext, FormPrevious } from 'grommet-icons';
+
+// Some content to show the effects of responsive layout.
+// The heading will change size at the small breakpoint
+// and the child Text items will switch from row to column
+// layout at the small breakpoint.
+const Content = ({ title, children }) => {
+  const size = useContext(ResponsiveContext);
+  const theme = useContext(ThemeContext);
+  const smallBreakpoint = theme.global.breakpoints.small.value;
+  return (
+    <Box pad="medium">
+      <Heading>{title}</Heading>
+      <Box direction="row-responsive" gap={{ column: 'medium', row: 'xsmall' }}>
+        <Text>I am row-responsive</Text>
+        <Text>Small breakpoint: {smallBreakpoint}</Text>
+        {children}
+        <Text>
+          Current size:&nbsp;<Text weight="bold">{size}</Text>{' '}
+        </Text>
+      </Box>
+    </Box>
+  );
+};
+
+const App = ({ title, responsive = true }) => {
+  const widthRef = React.useRef(undefined);
+  const containerRef = React.useRef(undefined);
+
+  // track the width of the container just so we can display it as it changes
+  useEffect(() => {
+    let resizeObserver;
+    const element = containerRef.current;
+    if (typeof window !== 'undefined' && window.ResizeObserver) {
+      resizeObserver = new window.ResizeObserver((entries) => {
+        const entry = entries[0].borderBoxSize[0];
+        const width = entry?.inlineSize;
+        if (widthRef.current) {
+          widthRef.current.innerText = `Container width: ${width.toFixed()}px`;
+        }
+      });
+      if (element) {
+        resizeObserver.observe(containerRef.current);
+      }
+    } else {
+      // fallback for server side rendering
+      const width = containerRef.current.getBoundingClientRect();
path: src/js/components/Box/stories/ResponsiveContainer.js
user: login: britt6612
body: ```suggestion
      const width = containerRef.current.getBoundingClientRect().width;
```

would they just want the width of the getBoundingClientRect()??
created_at: 2025-03-13T16:54:18Z
author_association: COLLABORATOR
diff_hunk: @@ -29,12 +29,23 @@ export const fontSize = (size, lineHeight) => css`
     }px`};
 `;
 
-export const breakpointStyle = (breakpoint, content) => css`
-  @media only screen ${breakpoint.value &&
-    `and (max-width: ${breakpoint.value}px)`} {
-    ${content};
-  }
-`;
+export const breakpointStyle = (breakpoint, content, responsive) => {
+  const st =
+    responsive === 'container'
+      ? css`
+          @container ${breakpoint.value &&
path: src/js/utils/mixins.js
user: login: MikeKingdom
body: The solution that seemed to work best was detect that it's pre-v6 by checking for the `withComponent` api that was removed in v6
created_at: 2025-03-14T16:00:58Z
author_association: COLLABORATOR
diff_hunk: @@ -634,12 +646,6 @@ exports[`Skeleton with styling props 1`] = `
   -webkit-font-smoothing: antialiased;
 }
 
-@media only screen and (max-width: 768px) {
path: src/js/components/Skeleton/__tests__/__snapshots__/Skeleton-test.tsx.snap
user: login: MikeKingdom
body: Yes. It wasn't passing along the responsive like it really should.
created_at: 2025-03-14T16:03:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,63 @@
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import {
+  ResponsiveContainerContext,
+  ResponsiveContext,
+  ThemeContext,
+} from '../../contexts';
+import { StyledResponsiveContainer } from './StyledResponsiveContainer';
+import { deviceResponsive, getBreakpoint } from '../../utils';
+
+const responsiveContainerValue = true;
+
+export const ResponsiveContainer = ({ children }) => {
+  const theme = useContext(ThemeContext);
+  const ref = useRef(undefined);
+  const [stateResponsive, setResponsive] = useState();
path: src/js/components/Box/ResponsiveContainer.js
user: login: MikeKingdom
body: Done

created_at: 2025-03-14T16:04:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,118 @@
+import React, { useContext, useEffect } from 'react';
+
+import {
+  Box,
+  Button,
+  Heading,
+  ResponsiveContext,
+  Text,
+  ThemeContext,
+} from 'grommet';
+import { FormNext, FormPrevious } from 'grommet-icons';
+
+// Some content to show the effects of responsive layout.
+// The heading will change size at the small breakpoint
+// and the child Text items will switch from row to column
+// layout at the small breakpoint.
+const Content = ({ title, children }) => {
+  const size = useContext(ResponsiveContext);
+  const theme = useContext(ThemeContext);
+  const smallBreakpoint = theme.global.breakpoints.small.value;
+  return (
+    <Box pad="medium">
+      <Heading>{title}</Heading>
+      <Box direction="row-responsive" gap={{ column: 'medium', row: 'xsmall' }}>
+        <Text>I am row-responsive</Text>
+        <Text>Small breakpoint: {smallBreakpoint}</Text>
+        {children}
+        <Text>
+          Current size:&nbsp;<Text weight="bold">{size}</Text>{' '}
+        </Text>
+      </Box>
+    </Box>
+  );
+};
+
+const App = ({ title, responsive = true }) => {
+  const widthRef = React.useRef(undefined);
+  const containerRef = React.useRef(undefined);
+
+  // track the width of the container just so we can display it as it changes
+  useEffect(() => {
+    let resizeObserver;
+    const element = containerRef.current;
+    if (typeof window !== 'undefined' && window.ResizeObserver) {
+      resizeObserver = new window.ResizeObserver((entries) => {
+        const entry = entries[0].borderBoxSize[0];
+        const width = entry?.inlineSize;
+        if (widthRef.current) {
+          widthRef.current.innerText = `Container width: ${width.toFixed()}px`;
+        }
+      });
+      if (element) {
+        resizeObserver.observe(containerRef.current);
+      }
+    } else {
+      // fallback for server side rendering
+      const width = containerRef.current.getBoundingClientRect();
path: src/js/components/Box/stories/ResponsiveContainer.js
user: login: MikeKingdom
body: Good point. Thanks. This is code a normal human can't hit but I was able to test the fix via the debugger.
created_at: 2025-03-14T16:14:53Z
author_association: COLLABORATOR
diff_hunk: @@ -249,6 +256,16 @@ const Box = forwardRef(
       </StyledBox>
     );
 
+    if (responsiveProp === 'container') {
path: src/js/components/Box/Box.js
user: login: britt6612
body: nice addition
created_at: 2025-03-14T18:49:02Z
author_association: COLLABORATOR
diff_hunk: @@ -222,22 +230,33 @@ const gapStyle = (directionProp, gap, responsive, wrap, theme) => {
       );
       if (responsiveMetric) {
         styles.push(
-          breakpointStyle(breakpoint, `column-gap: ${responsiveMetric};`),
+          breakpointStyle(
+            breakpoint,
+            `column-gap: ${responsiveMetric};`,
+            responsive,
+          ),
         );
       }
     }
   } else if (directionProp === 'column' || directionProp === 'column-reverse') {
     styles.push(`row-gap: ${metric};`);
     if (responsiveMetric) {
-      styles.push(breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`));
+      styles.push(
+        breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`),
+        responsive,
path: src/js/components/Box/StyledBox.js
user: login: jcfilben
body: Should this be 
```suggestion
        breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`, responsive),
```
created_at: 2025-03-14T20:24:44Z
author_association: COLLABORATOR
diff_hunk: @@ -222,22 +230,33 @@ const gapStyle = (directionProp, gap, responsive, wrap, theme) => {
       );
       if (responsiveMetric) {
         styles.push(
-          breakpointStyle(breakpoint, `column-gap: ${responsiveMetric};`),
+          breakpointStyle(
+            breakpoint,
+            `column-gap: ${responsiveMetric};`,
+            responsive,
+          ),
         );
       }
     }
   } else if (directionProp === 'column' || directionProp === 'column-reverse') {
     styles.push(`row-gap: ${metric};`);
     if (responsiveMetric) {
-      styles.push(breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`));
+      styles.push(
+        breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`),
+        responsive,
path: src/js/components/Box/StyledBox.js
user: login: MikeKingdom
body: Oooo great catch. And line 212 also. And there's also a pre-existing bug here we should fix, maybe as a separate PR? If gap is an object like `gap={{ column: "medium", row: "xsmall" }}` then`responsiveMetric` at line 200 is always `undefined`.
created_at: 2025-03-14T21:41:39Z
author_association: COLLABORATOR
diff_hunk: @@ -222,22 +230,33 @@ const gapStyle = (directionProp, gap, responsive, wrap, theme) => {
       );
       if (responsiveMetric) {
         styles.push(
-          breakpointStyle(breakpoint, `column-gap: ${responsiveMetric};`),
+          breakpointStyle(
+            breakpoint,
+            `column-gap: ${responsiveMetric};`,
+            responsive,
+          ),
         );
       }
     }
   } else if (directionProp === 'column' || directionProp === 'column-reverse') {
     styles.push(`row-gap: ${metric};`);
     if (responsiveMetric) {
-      styles.push(breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`));
+      styles.push(
+        breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`),
+        responsive,
path: src/js/components/Box/StyledBox.js
user: login: jcfilben
body: good to know, I feel like since it is a pre-existing issue it could be a separate PR
created_at: 2025-03-17T15:21:38Z
author_association: COLLABORATOR

PR comments:
user: login: MikeKingdom
created_at: 2025-03-04T00:39:26Z
body: > Thanks for getting this started, Mike. I'm excited about it. A couple of initial questions about API/expected behaviors.
> 
> 1. Do we want to introduce an entirely new component, or would it be nice to be able to turn any Box into a responsive container via a prop like `container`?

Good question and I went back and forth on this. So far I landed on separate component because it adds a lot of special code and useEffects (which themselves can't be conditional, although their contents can be.) Seems like it was a lot of extra stuff in a rather large component. Plus it's something that really is likely only at the top level of an application so far. That said we can certainly discuss more what makes the most sense.

> 2. Do we expect responsive styling that typically steps down at window breakpoints (spacing, heading sizing, etc.) to apply to content in responsive containers? I suppose my initial thought was people might what to use ResponsiveContainerContext to drive layout things like Grid columns, etc. but I wasn't necessarily expecting the "sizing down" that happens at window breakpoints to apply to responsive containers.

The primary use case is basically what you see in the storybook example: one container at the top level of a large piece of functionality so that it's normal window breakpoint behavior doesn't have to worry about something else getting added around it. This originally came about due to how the contextual help side panel totally invalidating the main GLP application's responsiveness. Trying to make all of GLP understand when the panel is visible and somehow make other responsive choices when it is was basically impossible. This is also exactly the use case COM wants it for. 

Of course Matt and I have been also thinking about other situations where this might be useful (and you'd likely want to wrap it in a ThemeContext.Extend that has breakpoints that are best suited for the situation.)
author_association: COLLABORATOR
user: login: MikeKingdom
created_at: 2025-03-10T22:49:44Z
body: 
> The PR has changed direction since then, I'm not opposed to it being built as a prop within Box but wanted to understand the reasoning for the change in direction.

Having it just be a simple prop on Box was an original goal but I was worried about the added complexity/bloat to Box. I figured out a decent way to avoid a lot of the complexity and given folks were commenting on avoiding a separate component it seemed preferable to just stick with Box. 
author_association: COLLABORATOR
user: login: britt6612
created_at: 2025-03-14T16:09:22Z
body: also one thing to increase bundle size 
author_association: COLLABORATOR