Container Queries in Pure CSS: The Raven Technique
CSS container queries remain one of the most requested features for responsive component design. When you build a component, you rarely control its final width—it might span the full viewport, sit beside another element, or live inside a narrow sidebar. The component's width doesn't always track the browser window.
JavaScript solutions for element queries exist, but they come with baggage: extra dependencies, style application tied to script execution, and logic that mixes application state with presentation concerns. Layout belongs squarely in CSS. This is what makes the Raven Technique so appealing: it uses existing CSS math functions to approximate container queries today.
The approach builds on Heydon Pickering's Flexbox Holy Albatross, which uses clamp() to switch between row and column layouts, but goes well beyond rows-to-columns switches. A raven, after all, can learn more tricks than an albatross.
How the Math Works
The technique leans on calc(), min(), max(), and clamp(). The clamp(a, x, b) function returns a when x is smaller, b when x is larger, and x otherwise—essentially, min(max(a,x),b). CSS custom properties keep the configuration clean.
The starting point is a base measurement against which "container width" is evaluated:
--base_size: 100%;
Using 100% rather than 100vw makes the technique truly container-based. The next step defines the breakpoint variables that mark the boundaries between layout intervals.
--breakpoint_wide: 1500px;
/* Wider than 1500px will be considered wide */
--breakpoint_medium: 800px;
/* From 801px to 1500px will be considered medium */
/* Smaller than or exact 800px will be small */
Any number of breakpoints is possible—the example here uses three intervals, with one value defined for each:
--length_4_small: calc((100% / 1) - 10px); /* Change to your needs */
--length_4_medium: calc((100% / 2) - 10px); /* Change to your needs */
--length_4_wide: calc((100% / 3) - 10px); /* Change to your needs */
Building Indicator Variables
The core trick is creating indicator variables that behave like booleans: 0px for false, 1px for true. Clamping the difference between the base size and a breakpoint achieves this distinction cleanly.
For the "wide" interval, the indicator should return 1px only when the base size exceeds the widest breakpoint:
--is_wide: clamp(0px,
var(--base_size) - var(--breakpoint_wide),
1px
);
When the subtraction yields a negative value (the container is narrower than the breakpoint), clamp() returns 0px. When the container is wider, the result is at least 1px, so the clamp returns 1px.
For the medium interval, a simple clamp would return true for both medium and wide containers. Subtracting the wide indicator fixes that: in wide containers 1px - 1px = 0px, while medium containers get 1px - 0px = 1px.
--is_medium: calc(
clamp(0px,
var(--base_size) - var(--breakpoint_medium),
1px)
- var(--is_wide)
);
The small interval needs no clamp at all. Since the lower breakpoint is zero, the difference is always positive, so the calculation reduces to:
--is_small: calc(1px - (var(--is_medium) + var(--is_wide)));
Converting Indicators to Usable Values
The indicators become useful when multiplied by target values. With pixel-based layouts this is direct arithmetic. The expression below returns 100px in the small interval and 0px otherwise:
calc(var(--is_small) * 100);
Summing several such terms yields different lengths for different intervals. If --is_small is true and --is_medium is false, for example, the result is simply 100px. In the medium interval, the reverse produces 200px.
width: calc(
(var(--is_small) * 100)
+ (var(--is_medium) * 200)
+ (var(--is_wide) * 500)
);
Most layouts, however, need values in other units—2rem, 65ch, percentages. This is where the technique pivots to min() and a very large integer. Define a helper variable that dwarfs any real viewport:
--very_big_int: 9999;
/* Pure, unitless number. Must be bigger than any length appearing elsewhere. */
Multiplying this by an indicator variable yields either 0px or 9999px. (The value 9999 works across browsers; Chrome tolerates larger numbers than Firefox.) When this product is passed to min() along with a real target length, the function returns 0px when the indicator is false, and the target value when it is true, because the large integer always exceeds the target:
min(
var(--length_4_small),
var(--is_small) * var(--very_big_int)
);
Each line in the final calculation uses this pattern, and all lines are summed:
--dyn_length: calc(
min(var(--is_wide) * var(--very_big_int), var(--length_4_wide))
+ min(var(--is_medium) * var(--very_big_int), var(--length_4_medium))
+ min(var(--is_small) * var(--very_big_int), var(--length_4_small))
);
Flexibility Beyond Fixed Breakpoints
Breakpoints do not have to be static pixel constants. Responsive breakpoints—like half the viewport minus 10 pixels—work naturally because calculations evaluate everywhere within the formula:
--breakpoint_wide: calc(50vw - 10px);
The key constraint is ordering: calculations assume breakpoints are maintained in ascending order, so wrap them with min() or max() to keep them consistent.
Heights, Visibility, and Aspect Ratios
Because CSS evaluates these formulas lazily, the same architecture works with heights when the base size is set to 100%. Height based on the width of a container isn't directly possible, but padding-top with 100% resolves to the width and offers a workaround.
Showing and hiding elements with the Raven means collapsing their width to zero in specific intervals:
.show_if_small {
width: calc(var(--is_small) * 100);
}
.show_if_medium {
width: calc(var(--is_medium) * 100);
}
.show_if_wide {
width: calc(var(--is_wide) * 100);
}
Setting:
overflow: hidden;
display: inline-block; /* to avoid ugly empty lines */
…plus zeroing out margin, padding, and border-width hides the box completely. Alternatively, position: absolute with left: calc(var(--is_???) * 9999) shifts the element far off-screen.
Bonus: Boolean Logic on Intervals
The indicator values lend themselves to logical operations. An OR is just a max() over indicators:
--a_OR_b: max( var(--indicator_a) , var(--indicator_b) );
NOT is a simple subtraction:
--NOT_a: calc(1px - var(--indicator_a));
AND uses min():
--a_AND_b: min(var(--indicator_a), var(--indicator_b));
XOR compares differences:
--a_XOR_b: max(
var(--indicator_a) - var(--indicator_b),
var(--indicator_b) - var(--indicator_a)
);
Equality—which works for length variables generally—can be clamped into utility:
--a_EQUALS_b_general: calc(
1px -
clamp(0px,
max(
var(--var_a) - var(--var_b),
var(--var_b) - var(--var_a)
),
1px)
);
Bonus: Responsive Grid Columns
Grid columns are unit-less values, which the Raven cannot return directly. But there's a workaround. Configure each interval's column count, compute the ideal column width for each interval, then let the Raven pick a width that causes repeat() and auto-fit to land on the intended count:
.grid_container{
display: grid;
grid-template-columns: repeat(auto-fit, var(--raven_grid_columns_width));
gap: var(--grid_gap)
};
The difference from auto-fit with minmax() is precisely controlled: the Raven approach never produces intermediate column counts, and column count need not correlate with container width. Feel free to try it in a simulator:
--number_of_cols_4_wide: 1;
--number_of_cols_4_medium: 2;
--number_of_cols_4_small: 4;
Bonus: Background Colors via Gradients
The Raven deals in lengths, not colors. But linear gradients intersect the two. By doubling color stops—making the actual gradient span zero pixels—length values control where colors begin. This makes color selection a width-driven affair:
background-image:linear-gradient(
to right,
red 0%,
red 50%,
blue 50%,
blue 100%
);
Define variables for the color stops, compute them from interval indicators, and compose the final gradient. A boolean OR picks correct stop positions when intervals overlap:
max(--is_small, --is_medium)
Nesting Depth Gotchas
There's a real constraint on how deeply nested variables can go inside calc(). The definition of --is_medium references --is_wide, and that formula gets pasted into the expression—multiplying the risk of hitting that limit as breakpoints multiply.
A flatter formulation avoids the problem entirely. Instead of subtracting indicators for larger intervals, express each indicator as the logical AND of two comparisons—the base size above the lower breakpoint and at or below the upper one:
--is_medium:
min(
clamp(0px, var(--base_size) - var(--breakpoint_medium), 1px),
clamp(0px, 1px + var(--breakpoint_wide) - var(--base_size), 1px)
);
The + 1px in the second clamp converts "smaller than" into "smaller or equal than," which works with integer pixel values. This keeps every indicator independent:
--is_wide: clamp(0px, var(--base_size) - var(--breakpoint_wide), 1px);
--is_medium: min(clamp(0px, var(--base_size) - var(--breakpoint_medium), 1px),
clamp(0px, 1px + var(--breakpoint_wide) - var(--base_size), 1px)
);
--is_small: clamp(0px,1px + var(--breakpoint_medium) - var(--base_size), 1px);
What the Raven Can Do—and Cannot
The Raven does not replace media queries, which still matter for viewport-scale changes like sidebar positions and menu layouts. But those queries address the whole browser window, not the arbitrary widths a component may encounter. Photos of responsive patterns in component design make this mismatch painfully clear:

By keeping this logic in CSS rather than JavaScript, layouts avoid bottlenecks: no script downloads to await, no main thread blocking, no intertwining design decisions with application code. The Raven may be a simulation of container queries, but it works today—and proves much of what designers have been told needs JavaScript can be done in stylesheets alone.



