Container Queries in a CMS: How We Write Specs So AI Agents Build Responsive Components Correctly
Nov 10, 2026
In our previous article, we walked through the process of modernizing a 10-year-old monolithic Sitecore front end. In this post, we’ll cover one part of that work: the CSS. Over the years, the codebase had collected 11 different breakpoints, the screen widths where a layout changes, such as a three-column section collapsing to one column on a phone.
Most of them added one bug at a time by resizing the browser, finding where the layout broke, and patching it

On a CMS-driven site, that approach produces layouts that look right in design review but then break once content authors start placing components on real pages. AI tools that generate code from design files tend to repeat the same problem, which adds rework after launch. Modern CSS gives components better options, since a component can now adjust to the space it actually has, often without any breakpoints.
In this article, we compare today's CSS tools and show why container queries matter more on a CMS-driven site. We also share the component specs we give to AI agents, so their code holds up wherever authors place it.
Key Takeaways
-
On CMS-driven sites, components built with viewport media queries often break when content authors place them in slots the design never accounted for, leading to production bugs and post-launch rework. AI agents working from Figma frames alone repeat the problem by copying pixel values and guessing where breakpoints belong.
-
A component spec for AI-assisted development should define the CMS slots the component can appear in, its layout at each container breakpoint, its variants, and edge cases that match content-model limits. Pairing that spec with a written CSS constitution keeps the agent within agreed rules and makes its output reviewable.
-
Oshyn builds with spec-driven agentic development, so AI-generated components are planned against written specs and reviewed by architects before they reach production.
Which CSS Tool Should You Use for Each Layout Job?
Start with layouts that adapt on their own, and add breakpoints only when a component needs a real structural change. For most components, Grid or Flexbox handles the layout, and clamp() handles sizes that should scale. Container queries come in when a component needs a different layout in different CMS slots. Media queries are mostly left for the overall page layout and user preferences such as reduced motion.
Teams still default to breakpoints because when Ethan Marcotte coined responsive web design in 2010, media queries were the only real tool available. There was no Flexbox, no Grid, no clamp(), and no way for a component to know how much space it had.
| Tools | Responds to | Best for | Avoid for | Support (Oct 2026) |
|---|---|---|---|---|
Grid / Flexbox with minmax(), flex-basis |
Available space | Card grids, clusters, sidebars, any "as many as fit" layout | Exact column counts at exact widths | Universal |
clamp(), min(), max() |
Viewport or container (vw, cqi) | Type scales, spacing, gutters, max widths | Structural changes | Universal |
| Container queries | Container size | Components that change layout in different CMS slots | Simple resizing; page shell | All major browsers |
Style queries / if() |
Custom property values | Section themes, component variants, density modes | Critical layout without a fallback | Chrome, Safari; Firefox in progress |
| Media queries | Viewport, device, user preferences | Page shell, reduced motion, color scheme, pointer type, print | Reusable components | Universal |
The decision order we use on every component:
-
Intrinsic layout (Grid or Flexbox)
-
Fluid values (
clamp()and tokens) -
Container queries
-
Style queries
-
Media queries, only for the page shell and user preferences
Grid and Flexbox: Intrinsic Layouts Without Breakpoints
With minmax(), auto-fit, and flex-basis, a layout determines its own column count based on the available space, so many breakpoints simply disappear. This is where we start every component.
Grid: equal columns that reflow
.card-grid {
display: grid;
gap: var(--space-m);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
}
Columns are at least 18rem wide and share the leftover space. Wrapping the minimum in min(100%, 18rem) stops the grid from overflowing on screens narrower than 18rem. auto-fit stretches items to fill an incomplete row; auto-fill keeps the empty tracks so items hold their size.
Flexbox: content-driven wrapping
.cluster { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.cluster > * { flex: 1 1 14rem; }
.with-sidebar { display: flex; flex-wrap: wrap; gap: 2rem; }
.with-sidebar > :first-child { flex: 1 1 15rem; }
.with-sidebar > :last-child { flex: 999 1 0; min-inline-size: 60%; }
Each cluster item aims for 14rem, grows to fill space, and wraps when it can't fit. In the sidebar pattern, the sidebar wraps above the main column as soon as the main column drops to 60% below the main column. No media query is involved. Both patterns come from Heydon Pickering and Andy Bell's Every Layout, which is still the best reference on intrinsic CSS.
The tradeoff you have to negotiate with design: You give up control over exactly when the layout changes. A designer who expects "three columns at 1024px" has to agree to "as many 18rem columns as fit." In our experience, that conversation is the real work; the CSS is three lines.
A few things still catch people:
-
Flexbox stretches orphaned items across the last row. If items must be equal width, use Grid.
-
Reordering or hiding elements still needs a container or media query; intrinsic layout only changes the column count.
-
Flex items default to
min-width: auto, so a long URL or product code can push a column wider than you planned. Settingmin-inline-size: 0on the item usually fixes it. -
subgridkeeps titles, prices, and buttons aligned across a row of cards. Designers notice immediately when that alignment is missing.
Rule of thumb: reach for intrinsic Grid or Flexbox first. Add a query only when the design needs a structural change that the algorithm can't produce.
clamp(), min() and max(): Fluid Values With Guardrails
Math functions replace entire families of breakpoints by allowing a value to scale smoothly between a floor and a ceiling.
-
min(a, b)picks the smaller value, so it sets a ceiling: width:min(100%, 70ch). -
max(a, b)picks the larger value, so it sets a floor: padding:max(1rem, 3vw). -
clamp(min, preferred, max)does both, with a fluid value in the middle.
:root {
--step-0: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--step-2: clamp(1.5rem, 1.2rem + 1.5vw, 2.25rem);
--step-4: clamp(2.25rem, 1.5rem + 3.5vw, 4rem);
--space-m: clamp(1rem, 0.75rem + 1.25vw, 1.75rem);
--gutter: clamp(1rem, 4vw, 3rem);
}
h1 { font-size: var(--step-4); }
.wrapper { width: min(100% - 2 * var(--gutter), 75rem); margin-inline: auto; }

Always mix a rem value into the preferred term (1.2rem + 1.5vw, not 5vw). Pure viewport units ignore the user's browser font size and break text zoom, which fails WCAG 1.4.4 (Resize Text). It's easy to miss in review because nothing looks broken until someone zooms. AI agents will also happily write 5vw-style values unless a rule tells them not to.
The preferred value is hard to calculate by hand. Use a generator such as Utopia, or have your token pipeline compute it from the min and max sizes in Figma. Swap vw for cqi, and the value scales with the container instead of the screen.
The catch is the review. Designers usually hand over two static frames, so nobody sees the sizes in between unless they go looking. Keep all formulas in tokens rather than scattered across components, so there is one place to check.
Rule of thumb: use clamp() for anything that should scale continuously, and keep the formulas in tokens.
Container Queries: Components That Know Their Space
Container queries let a component respond to the width of its container instead of the viewport. That is what makes a responsive component truly reusable.
.product-slot {
container: product / inline-size;
}
.product-card {
display: grid;
gap: 1rem;
}
@container product (inline-size >= 30rem) {
.product-card { grid-template-columns: 10rem 1fr; }
}
The same card stacks in a narrow sidebar and goes horizontal in a wide column, without any knowledge of the page. Container query units (cqi, cqb, cqmin) let sizes follow the container too, as in font-size: clamp(1rem, 4cqi, 1.5rem). Size queries have worked in every major browser since early 2023.
Three gotchas worth knowing before you ship:
-
An element can't query itself. You need a wrapper or parent to act as the container.
-
container-type: inline-size applies sizecontainment. The container can no longer size itself based on its children's widths, so within a flex row it can collapse to zero. If a component suddenly disappears after you add a container, check this first. -
Unnamed containers get confusing fast. A query matches the nearest ancestor container, which may not be the one you meant once components nest. Name every container.
Container queries are still breakpoints, just better-placed ones. If a component only needs to get bigger or smaller, use clamp() or intrinsic layout for less code.
Rule of thumb: use container queries when a component needs a different layout at different sizes, not just a different size.
Why Do Container Queries Matter More in a CMS?
On a CMS driven site, the developer doesn't decide where a component goes. A content author does, and authors put components in places nobody designed for.
Think about how pages get built in Sitecore, AEM, or Optimizely. A promo card designed for the full-width main column gets dropped into a one-third column on a two-column template. A personalization rule swaps a wide hero variant into a narrow rail for one audience segment. A page-builder block in Contentful or Contentstack gets nested inside another block. In every case, the viewport is the same, but the space the component has is completely different.
If that component uses viewport media queries, it is a production bug waiting for an author to trigger it. The page passes design review, then breaks the first time someone uses the CMS flexibly, which is what the CMS is for.
Two practical habits come out of this:
-
List the slots in the component spec. If a component is allowed in the main column, a sidebar placeholder, and a three-up grid, write that down and test all three. The CMS placeholder settings should match that list. These are the allowed controls in Sitecore, the allowed components in AEM, and the allowed types in an Optimizely content area.
-
Make the container part of the placeholder markup, not the component. The wrapper the CMS renders around a placeholder is the natural place to declare a
container-type. Every component inside it then gets a container for free, and you avoid the "element can't query itself" problem.
Style Queries and if(): Context-Aware Variants
Style queries let descendants change based on a custom property set on an ancestor, which turns CSS variables into a lightweight theming and variant system.
Every element is a style container by default, so there's no setup. In practice, browsers only support querying custom properties:
.section--dark { --surface: dark; }
@container style(--surface: dark) {
.card__body { background: #1b1b1f; color: #f2f2f2; }
.card__link { color: #8ab4ff; }
}
Any card placed inside a dark section adapts on its own. The same idea works for variants like --variant: danger, without a growing list of modifier classes. For a single property, the newer if() function is more compact:
.button {
background: if(style(--variant: danger): crimson; else: steelblue);
}
This maps neatly to how content teams work. A Figma component property becomes a custom property in code. That custom property can be driven by a CMS rendering parameter or by a section-level "theme" field the author picks. One field on the section changes every component inside it, with no class passed to each child.
The limits are real, though:
-
Only custom properties can be queried, not standard properties like
display. -
Like size queries, they style descendants only, so you often need an inner wrapper to style the surface itself.
-
Values compare as text unless registered with
@property, so#fffandwhitedon't match. -
Support is incomplete. Chrome and Safari ship
@container style(), and Chromium added range comparisons such asstyle(--level > 3)in Chrome 142 (web-features). Firefox had it only in Nightly as of March 2026 (MDN browser-compat-data). It's an Interop 2026 focus area, so check current support before relying on it. [AUTHOR: verify Firefox status before publishing.]
Rule of thumb: treat style queries as progressive enhancement. Ship sensible defaults, then layer variants on top.
What Are Media Queries Still Good For?
Media queries still matter, but their job has narrowed to two things: the page shell and the person using it.
/* Page shell */
@media (width >= 64rem) {
.page { grid-template-columns: 16rem 1fr; }
}
/* User preferences */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
@media (prefers-color-scheme: dark) {
:root { --surface: #121212; --text: #eee; }
}
/* Input type */
@media (pointer: coarse) {
.button { min-block-size: 44px; }
}
A few notes on that snippet:
-
The range syntax
(width >= 64rem)reads better thanmin-widthand avoids off-by-one gaps between breakpoints. -
The reduced-motion block shortens transitions instead of removing them. Some JavaScript waits for a
transitionendevent, which never arrives if the transition is removed. -
The 44px target meets WCAG 2.5.5 (AAA). The AA requirement under 2.5.8 is 24px, so 44px is a deliberate choice for touch, not a legal minimum.
Outside those uses, viewport media queries cause most of the problems this article is about. They only know the screen, so a card in a narrow sidebar and the same card in a wide column get the same styles. Their breakpoints are usually magic numbers found by dragging a browser window, and they multiply. The legacy codebase we started from had 11 different breakpoints, most of them added one bug at a time."
Rule of thumb: use media queries for the page shell and for user preferences. Keep them out of reusable components.
How Should You Architect CSS for Container-First Components?
Push responsiveness down into tokens, layout primitives, and components, and leave only a thin page layer that knows about the viewport. That is the structure we moved the legacy Sitecore front end toward.
1. Order the cascade with layers.
@layer reset, tokens, base, layout, components, utilities, overrides;
Layers make specificity predictable: for normal declarations, a utility beats a component regardless of selector weight. One catch trips up experienced developers: !important reverses layer order. An !important rule in reset beats an !important rule in overrides. Keep !important for user-preference resets like reduced motion, and remember that its behavior reverses inside layers.
2. Tokens are the fluid foundation.
Every size, space, color, and radius comes from a custom property, including the clamp() type scale. Tokens mirror Figma variables one-to-one, with the same names. Dark mode, high contrast, and density switches change token values, never component CSS.
3. Layout primitives are intrinsic and context-free.
A small set of classes each solves one pattern with no breakpoints: stack, cluster, grid, with sidebar, switcher, and center. Components compose these instead of reinventing the layout.
4. Components own their containers.
@layer components {
.media-card-slot { container: media-card / inline-size; }
.media-card { display: grid; gap: var(--space-s); }
@container media-card (inline-size >= 28rem) {
.media-card { grid-template-columns: 1fr 2fr; }
}
@container style(--tone: inverted) {
.media-card { background: var(--color-inverse); color: var(--color-on-inverse); }
}
}
5. Responsive images belong here, too.
Every content image needs srcset and accurate sizes. For lazy-loaded images, sizes="auto, ..." lets the browser measure the real slot. Keep a fallback list after auto, since support is not universal.
For above the fold heroes, write simple sizes such as 100vw. Without sizes, many framework image components serve oversized files, even on mobile devices. This problem is covered in Henrique Yuji Rossetti Inonhe's Your Images Are (Probably) Oversized and Mat Marquis's The End of Responsive Images.
Suggested folder structure
styles/
layers.css /* @layer order */
tokens/ /* generated from Figma variables */
base/ /* reset, typography, forms */
layout/ /* stack, cluster, grid, sidebar, center */
components/ /* one file per component, owns its container */
pages/ /* page shells + viewport media queries */
utilities/
How Do You Get AI Agents to Build Responsive Components From Figma?
Give an AI agent two Figma frames, and it does exactly what you'd expect: it copies the pixel values and adds a breakpoint between them. Figma shows a 375px frame and a 1440px frame. The site, though, lives at every width in between and inside every CMS slot an author can reach. How a card should behave between those frames usually lives only in the designer's head. Spec-driven development (SDD) documents requirements before any code is generated.
The workflow, mapped to SDD phases
-
Constitution: encode the CSS architecture as rules. This is where the previous section becomes enforceable. An example follows below.
-
Specify: describe behavior, not pixels. For each component, capture content priority, the CMS slots it can appear in, its variants, and its edge cases.
-
Plan: connect Figma to the codebase. Map Figma variables to tokens, Figma components to code components, and Figma variants to custom properties. Decide how the agent reads the design. One option is Figma's MCP server for Dev Mode, which exposes components, variables, and layout data to coding agents.
-
Tasks: one component per task, with testable criteria. Each task names the component, its container breakpoints, and the widths it will be checked at.
-
Implement and verify. The agent writes code, and people and tooling check it against the spec. Wherever something was missing, the spec gets refined.
An example CSS constitution
This is the file the agent reads before every component task. Copy it and adjust the token names to your system.
# CSS constitution
1. Cascade layer order: reset, tokens, base, layout, components, utilities, overrides.
2. Use tokens for every size, space, color and radius. No raw px, rem or hex values in components.
3. No viewport media queries inside components. Viewport queries live only in styles/pages/.
4. Every component that changes layout declares a NAMED container on its slot wrapper.
5. Every container breakpoint must cite the spec line that requires it.
6. Start from a layout primitive (stack, cluster, grid, with-sidebar, switcher, center) before writing custom layout.
7. clamp() preferred values always include a rem term. Never pure vw or cqi.
8. Style queries are progressive enhancement only. The default state must be complete without them.
9. Every content image has srcset and sizes; lazy images use sizes="auto, <fallback>".
10. Wrap reduced-motion and color-scheme handling in the page layer, not per component.
11. No !important outside the reset and user-preference rules.
12. If the spec doesn't cover a case, stop and ask. Do not invent breakpoints.
</fallback>
What a responsive component spec looks like
### ProductCard
- Slots: product grid (main column), sidebar "related" placeholder, search results list
- Content priority: image, title, price, CTA; rating is optional
- Layout:
- Container < 28rem:="" stacked,="" image="" on="" top,="" aspect-ratio="" 4/3="" -="" container="" />= 28rem: image left (1/3), content right
- Sizing: title uses --step-1; padding uses --space-s
- Variants: --tone: default | inverted (style query, fallback = default)
- Edge cases: title over 3 lines truncates with ellipsis; no image shows placeholder
- Content model: title field max 90 characters; image field optional
- Acceptance: Storybook baselines approved by design at container widths
280, 448 and 720px; no horizontal overflow at a 320px viewport and 200% zoom
Two details matter here. First, the breakpoints refer to the container, not the screen. That single shift is what lets the agent produce components that work in every slot.
Second, the edge cases and the content model are the same decision. If the spec says titles truncate after three lines, the CMS field should carry a character limit that makes three lines the worst case. Likewise, the image field's "optional" status should match the placeholder behavior. When one spec drives both the CSS and the content type definition, content operations, and front-end work stop drifting apart.
Regarding acceptance criteria: designers rarely draw components at 280, 448, and 720px, so "match Figma at those widths" isn't testable. Instead, we have designers approve Storybook baselines at the agreed container widths once. Every later change is then compared against those baselines.
Prompting the agent well
-
Give it the constitution and the component spec, not a screenshot.
-
Ask it to read tokens and component structure from Figma instead of eyeballing values.
-
Ask which layout primitive fits before it writes the component.
-
Ask it to explain every breakpoint it adds and name the spec line that justifies it. Unjustified breakpoints are a smell.
Verification is the human part
As Daniel Sogl notes in his review of GitHub Spec Kit, the bottleneck in SDD is not AI but how precisely people specify. Treat every generated component as a draft. Automate what you can:
-
Storybook stories at each container width
-
Playwright visual snapshots against the approved baselines
-
Axe accessibility checks
-
A 200% zoom test
Every bug you find becomes a new spec line or constitution rule, so the next component comes out right the first time.
Example: A Production Component Spec
# Component Specification: Side by Side (`<sidebyside>`)
> **Classification**: Organism Component
> **Target Directory**: `src/components/organisms/side-by-side/`
> **Status**: Ready for Implementation
> **Design System Compliance**: Strict 3-Tier Tokens • Responsive 2-Column Grid • Atomic Design • WCAG 2.1 AA Compliance
---
## 1. Overview & Architectural Intent
The **Side by Side** component is a reusable, two-column promotional banner designed to spotlight featured offers, products, or member services.
The design provides **two visual layout variants** of the same underlying component:
- **Variant A (Dark)**: Navy background (`--color-navy-900`), text content on the left, full-bleed landscape image on the right.
- **Variant B (Light)**: White surface background (`--color-white`), text content on the right, full-bleed landscape image on the left.
*(Note: The dotted "mirror" indicator connecting both variants in design deliverables is an editorial design annotation indicating the alternating layout pattern, not an interactive or rendered UI element).*
```
Variant A -- Dark (Desktop 50/50 Split):
┌──────────────────────────────────────┬──────────────────────────────────────┐
│ [Content Column - Navy Background] │ [Image Column - Flush Edge-to-Edge] │
│ │ │
│ EYEBROW LABEL │ ┌──────────────────────────────────┐ │
│ Headline with Key Rate or Value │ │ │ │
│ Body description paragraph. │ │ 800x600 Cover Image │ │
│ [ Primary CTA Button ] │ │ Outer Corners: 24px Radius │ │
│ │ │ Inner Edge: Square (0px) │ │
│ Padding: 40px vert / 48px horiz │ └──────────────────────────────────┘ │
└──────────────────────────────────────┴──────────────────────────────────────┘
(Outer card radius: 24px on all 4 corners)
Variant B -- Light (Desktop 50/50 Split - Mirrored):
┌──────────────────────────────────────┬──────────────────────────────────────┐
│ [Image Column - Flush Edge-to-Edge] │ [Content Column - White Background] │
│ │ │
│ ┌──────────────────────────────────┐ │ EYEBROW LABEL │
│ │ │ │ Headline with Key Rate or Value │
│ │ 800x600 Cover Image │ │ Body description paragraph. │
│ │ Outer Corners: 24px Radius │ │ [ Primary CTA Button ] │
│ │ Inner Edge: Square (0px) │ │ │
│ └──────────────────────────────────┘ │ Padding: 40px vert / 48px horiz │
└──────────────────────────────────────┴──────────────────────────────────────┘
(Outer card radius: 24px on all 4 corners)
```
---
## 2. Layout & Responsive Structure
### 2.1 Section Container
- **Outer Wrapper**: `<section classname="{styles.section}">` with strict **88px vertical padding block** (`padding-block: 5.5rem;` / `88px`).
- **Inner Constraint**: Standard responsive `.container` (`max-width: 1200px; margin-inline: auto; padding-inline: 15px;`).
### 2.2 Card Shell & Grid
- **Card Wrapper**: `<article classname="{styles.card}">` with outer border radius: **24px** (`var(--radius-xl)` / `1.5rem`).
- **Layout Model**: 2-column CSS Grid (`grid-template-columns: 1fr 1fr;` at desktop `width >= 1024px`).
- **Stacking Behavior (Mobile)**:
- Stacks into a single column (`grid-template-columns: 1fr;`).
- Mobile display order: Content column appears above or below according to content hierarchy (by default: image top, content bottom for visual engagement, or natural DOM order).
- **Corner Radii Integrity**:
- Desktop: Outer card corners have `border-radius: 24px`.
- Image Column: Outer corners match the card (24px); the inner vertical edge meeting the content column is square (`0px`).
- *Variant A (Image Right)*: `border-radius: 0 24px 24px 0;`
- *Variant B (Image Left)*: `border-radius: 24px 0 0 24px;`
- Mobile: Image outer top corners: `border-radius: 24px 24px 0 0;`, content bottom corners: `border-radius: 0 0 24px 24px;`.
- `overflow: hidden;` is applied to the card to guarantee clean clipping.
### 2.3 Content Column
- **Alignment**: Vertically centered (`display: flex; flex-direction: column; justify-content: center;`).
- **Internal Padding**:
- Desktop: `padding: 40px 48px;` (`2.5rem 3rem`).
- Mobile: `padding: 32px 24px;` (`2rem 1.5rem`).
- **Text Alignment**: Left-aligned (`text-align: left;`).
- **Vertical Rhythm Gaps**:
- Eyebrow → Headline: **12px-16px** (`var(--space-sm)` / `0.75rem`-`1rem`).
- Headline → Body: **16px** (`var(--space-md)` / `1rem`).
- Body → CTA: **24px** (`var(--space-lg)` / `1.5rem`).
### 2.4 Image Column
- **Dimensions**: Edge-to-edge flush within its column slot (`width: 100%; height: 100%; min-height: 320px;`).
- **Aspect Ratio & Fit**: Next.js `<img alt="" priority="{false}" sizes="(min-width: 1024px) 50vw, 100vw" />` with `object-fit: cover;`.
- **Recommended Asset**: Landscape orientation, minimum 800×600 resolution.
---
## 3. Variant Matrix
| Variant | Card Background | Text Eyebrow | Headline Color | Body Color | CTA Button Variant | Text Col Order | Image Col Order |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| **A (Dark)** | `var(--color-navy-900)` (`#00395D`) | `vibrant` (Turquoise `#68E0CC`) | `inverted` (`#FFFFFF`) | `inverted-muted` (White 80%) | `primary` or `secondary` | Col 1 (Left) | Col 2 (Right) |
| **B (Light)** | `var(--color-white)` (`#FFFFFF`) | `accent` (Teal `#00A3A3`) | `primary` (`#00395D`) | `secondary` (`#4E5059`) | `primary` | Col 2 (Right) | Col 1 (Left) |
---
## 4. Content Model & TypeScript Contracts
### 4.1 Domain Schema (`src/domain/models/SideBySide.ts`)
```typescript
import { z } from "zod";
export const SideBySideVariantSchema = z.enum(["dark", "light"]);
export const SideBySideCtaSchema = z.object({
label: z.string().min(1, "CTA label is required"),
url: z.string().min(1, "CTA URL is required"),
external: z.boolean().optional().default(false),
target: z.enum(["_blank", "_self"]).optional(),
rel: z.string().optional(),
});
export const SideBySideImageSchema = z.object({
src: z.string().min(1, "Image source URL is required"),
alt: z.string().default(""),
width: z.number().optional().default(800),
height: z.number().optional().default(600),
});
export const SideBySideSchema = z.object({
id: z.string(),
variant: SideBySideVariantSchema.default("dark"),
eyebrow: z.string().min(1, "Eyebrow is required"),
headline: z.string().min(1, "Headline is required"),
body: z.string().max(300, "Recommended body length 140-160 characters"),
cta: SideBySideCtaSchema,
image: SideBySideImageSchema,
});
export type SideBySideVariant = z.infer<typeof>;
export type SideBySideCtaModel = z.infer<typeof>;
export type SideBySideImageModel = z.infer<typeof>;
export type SideBySideModel = z.infer<typeof>;
```
### 4.2 Component Props (`SideBySide.tsx`)
```typescript
export interface SideBySideProps {
id?: string;
variant?: "dark" | "light";
headingLevel?: "h1" | "h2" | "h3" | "h4";
eyebrow: string;
headline: React.ReactNode;
body: string | React.ReactNode;
ctaLabel: string;
ctaUrl: string;
ctaExternal?: boolean;
imageSrc: string;
imageAlt: string;
className?: string;
}
```
---
## 5. Composition Contract (Atomic Integration)
The organism strictly composes existing atom components:
1. **Eyebrow**: `<eyebrow color="{isDark">{eyebrow}</eyebrow>`
- Automatically handles uppercase transform, tracking (`2.2px`), and Albert Sans bold typography.
2. **Headline**: `<heading level="{headingLevel}" color="{isDark">{headline}</heading>`
- Defaults to semantic `h3` heading hierarchy with fluid typography scale.
3. **Body**: `<text as="p" variant="body" color="{isDark">{body}</text>`
- 16px font size with 24px line-height.
4. **CTA Button**: `<button href="{ctaUrl}" variant="{isDark">{ctaLabel}</button>`
- Fully accessible interactive action with hover transitions and focus ring.
5. **Image**: `<img src="{imageSrc}" alt="{imageAlt}" classname="{styles.image}" />`
---
## 6. Accessibility & Compliance (WCAG 2.1 AA)
- **Semantic Landmark**: Uses `<section aria-labelledby="{headingId}">` and `<article>`.
- **Heading Hierarchy**: Headline rendered with semantic `<heading level="h3">` (or configurable tag level).
- **Color Contrast**:
- Dark variant: Turquoise on Navy (`#68E0CC` on `#00395D`) exceeds **4.5:1** contrast ratio; White text (`#FFFFFF` on `#00395D`) reaches **10.5:1**.
- Light variant: Navy on White (`#00395D` on `#FFFFFF`) reaches **10.5:1** contrast ratio.
- **Screen Reader Image Handling**: `imageAlt` is strictly enforced. If left empty, `aria-hidden="true"` is applied for decorative treatment.
- **Focus Management**: Focus outlines on CTA button respect `:focus-visible` with `2px solid var(--color-turquoise-400)` and `2px` offset.
---
## 7. Test Scenarios (Vitest & Testing Library)
```typescript
describe("SideBySide Organism", () => {
it("renders Variant A (Dark) with correct visual hierarchy and class modifiers", () => {
// Verifies dark card class, text on left, image on right
});
it("renders Variant B (Light) with mirrored layout and correct typography tokens", () => {
// Verifies light card class, text on right, image on left
});
it("renders CTA button with correct link href and accessibility labels", () => {
// Verifies Button link target and text
});
it("renders cover image with alt text and responsive fill attributes", () => {
// Verifies Next.js Image properties and container boundaries
});
});
```
</heading></article></section></typeof></typeof></typeof></typeof></article></section></sidebyside>
Common Pitfalls and a Component Review Checklist
Most responsive bugs, AI-generated or hand-written, come from the same short list of mistakes. A checklist catches them before they reach an author's page.
Common pitfalls
-
Copying Figma pixel values directly instead of mapping them to tokens.
-
Viewport media queries inside components, which break the moment an author places the component in a narrower placeholder.
-
clamp()with purevwvalues, which ignores user font settings and breaks zoom. -
container-type: inline-sizeon an element that needs to size itself from its content, causing it to collapse. -
Style queries used for essential layout with no fallback.
-
Images without
sizes, so every device downloads the largest file. -
!importantinside cascade layers, which silently reverses which layer wins. -
Component styles are injected one at a time during render, which forces repeated style recalculation.
Review checklist for each component
-
Uses only tokens for sizes, spacing, and color
-
Starts from a layout primitive or intrinsic Grid/Flexbox
-
Container queries are named, and each breakpoint is justified by a spec line
-
Works in every CMS slot listed in the spec
-
CMS field limits match the spec's edge cases
-
No horizontal overflow at a 320px viewport
-
Readable and usable at 200% zoom (WCAG 1.4.4)
-
Respects
prefers-reduced-motionandprefers-color-scheme -
Has complete default styling when style queries aren't supported
-
Images have
srcsetand accuratesizes(orautoplus a fallback when lazy-loaded) -
Visual snapshots match the approved baselines at the agreed container widths
Written Specs Make Responsive Components Reliable for Developers and AI Agents
Modern CSS handles most reflow on its own through intrinsic Grid and Flexbox, clamp() tokens, container queries, and style queries. The breakage that remains comes from undocumented behavior, such as how a card renders in a placeholder the designer never drew or what an author’s 90-character headline does to the layout.
On the legacy Sitecore front end, we moved away from 11 ad hoc viewport breakpoints by putting the rules into tokens, layout primitives, and components that own their containers. Then we wrote a CSS constitution and a spec for each component, so developers and AI agents worked from the same rules. That combination makes agent-generated components reviewable rather than something the team has to rewrite, and it accounts for much of the way agentic development shortens delivery timelines.
At Oshyn, we run agentic CMS and DXP implementations on Sitecore, AEM, Optimizely, and Contentstack using spec-driven development.
Contact us to discuss how this applies to your design system.
Frequently Asked Questions
No. Container queries replace media queries inside components. Media queries remain the right tool for the page shell and for user preferences like reduced motion, color scheme and pointer type, which only the media query API can read.
It should include:
- The CMS slots where the component can appear in
- Its content priority
- Its layout at each container breakpoint
- The tokens it uses
- Its variants
- Its edge cases and matching content-model limits
- Acceptance criteria tied to specific container widths
Related Insights
-
BLOG
Fernando Torres
How We Modernized a 10-Year-Old Front End Without a Rewrite
-
BLOG
Patrick Wirz
AI Agents See Your Brand First
Webinar: Machines Are the New Persona
-
BLOG
Christian Burne
Connect Your AI Assistant to Oshyn's Free DXP Tools via MCP
A step-by-step guide to adding Oshyn's MCP server to Claude, ChatGPT, Cursor, and more.
-
BLOG
Fernando Torres
AI-Assisted Development
Figma to Code