-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
feat(skills): Add migrate-container-queries skill #120459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
priscilawebdev
merged 4 commits into
master
from
priscila/migrate-container-queries-skill
Jul 27, 2026
+154
−0
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
5620ee0
feat(skills): Add migrate-container-queries skill
priscilawebdev d48c9fc
fix(skills): Correct Rung 3 container breakpoint example in migrate-c…
priscilawebdev 28c7e67
fix(skills): Use useResponsivePropValue for threshold boolean in Rung 3
priscilawebdev fd50567
fix(skills): Add Rung 3 useResponsivePropValue checklist item in migr…
priscilawebdev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| --- | ||
| name: migrate-container-queries | ||
| description: Guide for migrating viewport media queries (@media, useMedia) to container queries in Sentry's frontend. Use when migrating responsive layout to container queries, replacing @media/useMedia, refactoring styled responsive components to Container/Flex/Grid primitives, or working on the DE container-query migration. | ||
| --- | ||
|
|
||
| # Container Query Migration Guide | ||
|
|
||
| Migrate viewport-based responsive logic (`@media` + `useMedia`) to container queries so components respond to their own available space instead of the raw viewport. | ||
|
|
||
| > **Always do a visual check.** After every migration, resize the _element_ (not just the window) and confirm the layout is identical and flips at the intended width. A good way to narrow an element without touching the window is to open a resizable panel next to it — e.g. drag out the Seer explorer sidebar, which squeezes the middle content. The token scales differ, so a mechanical swap that compiles can still render wrong. | ||
|
|
||
| ## Approach: refactor first, swap second | ||
|
|
||
| Stop at the first rung that fits. Prefer replacing hand-rolled CSS with primitives over a mechanical token swap. | ||
|
|
||
| | Rung | When | Do | | ||
| | ---------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | 1. Primitive props | The `@media` only flips layout (`flex-direction`, `display`, `grid-template`, gap, visibility, width) | Delete the styled component; use `Container`/`Flex`/`Grid`/`Stack` responsive props (`direction={{xs: 'column', md: 'row'}}`) | | ||
| | 2. `@container` swap | CSS can't be a prop (descendant selectors, pseudo-elements, `font-size`, complex `grid-template-areas`) | Keep the styled component; swap `@media` → `@container`, `theme.breakpoints.*` → `theme.container.*` | | ||
| | 3. Container-scoped JS | Width is read in JS to branch rendering | Replace `useMedia(...)` with `useResponsivePropValue({...})` for a threshold boolean, or `useContainerBreakpoint()` to branch on the active key | | ||
| | 4. Leave as `useMedia` | Genuine media feature, not width | Do nothing — these do not migrate | | ||
|
|
||
| ## ⚠️ Convert to the nearest container scale | ||
|
|
||
| Breakpoint and container scales have **different keys and different pixel values** — this is not a rename. **MAP BY PIXEL VALUE, NOT BY KEY:** `breakpoints.sm` does NOT become `container.sm`. Reusing the same key is the #1 migration bug. | ||
|
|
||
| `theme.breakpoints` (viewport / `@media`), base `2xs`: | ||
|
|
||
| | `2xs` | `xs` | `sm` | `md` | `lg` | `xl` | `2xl` | | ||
| | ----- | ----- | ----- | ----- | ------ | ------ | ------ | | ||
| | 0px | 500px | 800px | 992px | 1200px | 1440px | 2560px | | ||
|
|
||
| `theme.container` (container / `@container`), base `zero`: | ||
|
|
||
| | `zero` | `3xs` | `2xs` | `xs` | `sm` | `md` | `lg` | `xl` | `2xl` | `3xl` | `4xl` | `5xl` | | ||
| | ------ | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ------ | ------ | ------ | | ||
| | 0px | 320px | 384px | 448px | 512px | 576px | 640px | 768px | 896px | 1024px | 1152px | 1280px | | ||
|
|
||
| **Rule:** take the old breakpoint's pixel value and pick the `container` token whose pixel value is _nearest_ to it — not the token with the same name. `breakpoints.sm` is 800px, so it maps to `container.xl` (768px), not `container.sm` (512px). Then confirm with a visual check: the container is often narrower than the viewport, so the nearest-px token is a starting point, not a guarantee. | ||
|
|
||
| ## Genuine viewport width → `screen:` keys, not `useMedia` | ||
|
|
||
| When layout truly must follow the _window_ (not the component's room), don't keep `useMedia` — use a `screen:`-prefixed responsive prop, which resolves against the viewport on the `theme.breakpoints` scale: `direction={{zero: 'column', 'screen:lg': 'row'}}`. Bare keys and `screen:` keys can mix on one prop. Prefer bare (container) keys; reach for `screen:` only when the viewport genuinely drives the layout. | ||
|
|
||
| ## Keep `useMedia` only for non-width media features | ||
|
|
||
| Width — container or viewport — has a prop/hook path above. Leave `useMedia` in place only for: | ||
| `prefers-color-scheme`, `prefers-reduced-motion`, `hover`, `pointer`, `max-height` / height-based, `resolution`, `print`. | ||
|
|
||
| ## container-type: only when no query container is in scope | ||
|
|
||
| **Default: don't add one.** Bare keys and `@container` already resolve against the nearest ancestor container, and product views have one: `ContentStack` (`#main`, `views/organizationLayout/index.tsx`) wraps the routed `<Outlet />` with `containerType="inline-size"`; `topBar` and `#modal-portal` cover their own subtrees. Add `container-type` only when a subtree must respond to _its own_ width rather than the page's — then: | ||
|
|
||
| - Use `inline-size` (width only). `size` also queries height, which collapses content unless height is set elsewhere. | ||
| - In a reusable component that may already sit inside a container, make it conditional to avoid a redundant one — `containerType={hasParentQueryContainer ? 'normal' : 'inline-size'}` via `useHasContainerQuery()` (see `components/core/breadcrumbList/breadcrumbList.tsx`). | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Rung 1 — styled `@media` → primitive props (preferred) | ||
|
|
||
| ```tsx | ||
| // Old — delete the styled component | ||
| const Row = styled('div')` | ||
| display: flex; | ||
| flex-direction: row; | ||
| gap: ${p => p.theme.space.md}; | ||
| @media (max-width: ${p => p.theme.breakpoints.sm}) { | ||
| flex-direction: column; | ||
| } | ||
| `; | ||
|
|
||
| // New | ||
| import {Flex} from '@sentry/scraps/layout'; | ||
| <Flex direction={{xs: 'column', sm: 'row'}} gap="md"> | ||
| ``` | ||
|
|
||
| ### Rung 2 — `@media` → `@container` (when it can't be a prop) | ||
|
|
||
| ```tsx | ||
| // Old | ||
| @media (max-width: ${p => p.theme.breakpoints.md}) { ... } | ||
|
sentry[bot] marked this conversation as resolved.
|
||
|
|
||
| // New — swap at-rule AND scale; md breakpoint (992px) → nearest container token by px | ||
| // is 3xl (1024px), NOT theme.container.md by matching key | ||
| @container (max-width: ${p => p.theme.container['3xl']}) { ... } | ||
| ``` | ||
|
|
||
| ### Rung 3 — `useMedia` (width) → container-scoped JS | ||
|
|
||
| Both helpers below read the nearest query container (call from a descendant of one) and re-render as it crosses a breakpoint. A single `max-width` boolean is cleanest as a responsive value; reach for the active key only when you branch on the key itself. | ||
|
|
||
| ```tsx | ||
| // Old | ||
| const isNarrow = useMedia(`(max-width: ${theme.breakpoints.sm})`); | ||
|
|
||
| // New — resolve a responsive boolean against the container, same mobile-first | ||
| // cascade as CSS. A max-width query is "on by default, off past the threshold", | ||
| // so name only the threshold key. Map by pixel value: breakpoints.sm (800px) → | ||
| // nearest container token is xl (768px). | ||
| import {useResponsivePropValue} from '@sentry/scraps/layout'; | ||
|
|
||
| const isNarrow = useResponsivePropValue({zero: true, xl: false}); | ||
| // below xl → true, at/above xl → false — one key on each side, nothing to enumerate. | ||
| ``` | ||
|
|
||
| Reach for `useContainerBreakpoint()` instead only when you branch on the key | ||
| itself (e.g. picking one of several layouts), not a single threshold. It returns | ||
| the container's active key (`'zero'` … `'5xl'`) — don't compare it with | ||
| `=== 'zero'` for a max-width case: that fires only below 320px and drops the | ||
| 320–768px range the original query treated as narrow. | ||
|
|
||
| ## Migration Checklist | ||
|
|
||
| Took the lowest rung that fits (above). Then verify the gotchas: | ||
|
|
||
| - [ ] Mapped to the `container` token with the nearest pixel value, not the same name — e.g. `breakpoints.sm` → `container.xl`, not `container.sm` | ||
| - [ ] For width read in JS, used `useResponsivePropValue({...})` for a threshold boolean; reserved `useContainerBreakpoint()` for branching on the key — never `=== 'zero'` to mean "narrow" (that's only <320px) | ||
| - [ ] Routed genuine viewport-width cases to `screen:` keys; kept `useMedia` only for non-width media features | ||
| - [ ] Added `container-type` only when a subtree needs its own; used `inline-size` | ||
| - [ ] Confirmed a query-container ancestor exists (`@container` silently no-ops without one) | ||
| - [ ] **Visual check:** resized the element and confirmed identical output flipping at the intended width | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Container Query Migration Guide Specification | ||
|
|
||
| ## Intent | ||
|
|
||
| Provide a reliable, low-risk path for converting viewport-based responsive logic (`@media`, `useMedia`) to container queries, so components respond to their own available width instead of the raw viewport. The dominant failure mode this skill guards against is mapping a breakpoint token to the container token of the same name: the two scales share names but not pixel values. | ||
|
|
||
| ## Scope | ||
|
|
||
| In scope: migrating `@media`/`useMedia` width logic to primitive props, `@container` CSS, `useContainerBreakpoint()`, or `screen:` keys, and deciding when to add `container-type`. | ||
|
|
||
| Out of scope: non-width media features (`prefers-*`, `hover`, `pointer`, `resolution`, height-based, `print`) stay on `useMedia`; building the primitives or tokens themselves. | ||
|
|
||
| ## Non-negotiable Constraints | ||
|
|
||
| - Map to the `container` token with the pixel value _nearest_ the old breakpoint's — never the same-named token. | ||
| - Always visually verify by resizing the element (the container is often narrower than the viewport, so the nearest-px token is a starting point). | ||
| - Route genuine viewport-width cases to `screen:` keys; keep `useMedia` only for non-width media features. | ||
|
|
||
| ## Sources | ||
|
|
||
| - Scraps `Container` story, "Container Queries" section — authoritative for container vs. `screen:` keys, both token scales, `useContainerBreakpoint`, and `container-type` guidance. | ||
| - Reference migration PR getsentry/sentry#120315 (trace-view). | ||
| - `components/core/breadcrumbList/breadcrumbList.tsx` — conditional `container-type` pattern. | ||
|
|
||
| ## Known Limitations | ||
|
|
||
| - The nearest-px token is only a starting point; the true reflow width needs a browser visual check the skill cannot perform. | ||
| - `@container` silently no-ops without a query-container ancestor; the skill flags this but cannot detect it statically. | ||
|
|
||
| ## Maintenance | ||
|
|
||
| - Update `SKILL.md` when token scales change, primitives gain/lose props, or a rung is added. | ||
| - Update `SPEC.md` when intent, scope, the non-negotiable constraints, or the sources change. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.