HTML and CSS course Β· Module 9: Modern CSS
Container Queries - Container-Based Responsiveness
In this lesson6
Your artifact card looks great in a wide gallery column, but in a narrow sidebar it falls apart, and a media query notices nothing, because the browser window has not changed. The card does not know which chamber it stands in. Container Queries are a CSS feature that enables creating responsive components based on the size of their container, rather than the browser window. This is a breakthrough that allows for truly modular layouts.
What you will learn
- turn an element into a container and write a
@containerquery, - name containers and combine conditions with operators,
- tell the jobs for Container Queries apart from the jobs for media queries,
- use container units such as
cqw.
Container Queries Basics
What Are Container Queries?
Container Queries let you apply styles depending on the size of the parent container, not the viewport, so components react to their local context. Compare both queries:
1/* Traditional Media Queries - based on viewport */
2@media (max-width: 768px) {
3 .card {
4 flex-direction: column;
5 }
6}
7
8/* Container Queries - based on container size */
9@container (max-width: 400px) {
10 .card {
11 flex-direction: column;
12 }
13}Both rules switch the card to a column. The syntax of the conditions stays the same, only the point of reference changes: the window or the container.
Establishing a Container Context
To use Container Queries, we first need to define an element as a container with the container-type property:
1.container {
2 container-type: inline-size; /* Monitors width */
3}
4
5.container {
6 container-type: size; /* Monitors width and height */
7}
8
9.container {
10 container-type: normal; /* Default value - no monitoring */
11}
12
13/* Shorthand with container name */
14.sidebar {
15 container: sidebar / inline-size;
16}inline-size tracks the width and is almost always enough. size also tracks the height, but the element then stops taking its height from its content. The most important rule: an element cannot query itself, so the rules inside @container style the container's descendants. The workflow: container-type on the parent, an optional name in container-name, the @container query, styles for the children and a test at different widths.
Practical Applications of Container Queries
1. Responsive Product Card
Here the container is the whole product grid, and the cards are its descendants, changing their layout together with it:
1.product-grid {
2 display: grid;
3 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
4 gap: 1rem;
5 container-type: inline-size;
6}
7
8.product-card {
9 background: white;
10 border-radius: 8px;
11 padding: 1rem;
12 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
13}
14
15/* When container is narrow - vertical layout */
16@container (max-width: 300px) {
17 .product-card {
18 text-align: center;
19 }
20
21 .product-card .image {
22 width: 100%;
23 margin-bottom: 1rem;
24 }
25
26 .product-card .title {
27 font-size: 1.1rem;
28 margin-bottom: 0.5rem;
29 }
30
31 .product-card .price {
32 font-size: 1.2rem;
33 color: #e74c3c;
34 }
35}
36
37/* When container is wider - horizontal layout */
38@container (min-width: 400px) {
39 .product-card {
40 display: flex;
41 align-items: center;
42 gap: 1rem;
43 }
44
45 .product-card .image {
46 width: 120px;
47 height: 120px;
48 flex-shrink: 0;
49 }
50
51 .product-card .content {
52 flex: 1;
53 }
54
55 .product-card .title {
56 font-size: 1.3rem;
57 margin-bottom: 0.5rem;
58 }
59
60 .product-card .description {
61 color: #666;
62 margin-bottom: 1rem;
63 }
64
65 .product-card .price {
66 font-size: 1.4rem;
67 font-weight: bold;
68 color: #27ae60;
69 }
70}We ask about the width of the whole grid, not of a single card. If each card should react on its own, make its wrapper the container.
2. Adaptive Navigation
You can name a container, and then the @container nav query checks only that container, even if another one stands closer:
1.navigation {
2 container-name: nav;
3 container-type: inline-size;
4}
5
6.nav-list {
7 display: flex;
8 list-style: none;
9 margin: 0;
10 padding: 0;
11 gap: 1rem;
12}
13
14/* Compact navigation for small containers */
15@container nav (max-width: 600px) {
16 .nav-list {
17 flex-direction: column;
18 gap: 0;
19 }
20
21 .nav-item {
22 border-bottom: 1px solid #eee;
23 }
24
25 .nav-link {
26 display: block;
27 padding: 1rem;
28 text-decoration: none;
29 transition: background-color 0.2s;
30 }
31
32 .nav-link:hover {
33 background-color: #f8f9fa;
34 }
35
36 /* Hide text and show only icons */
37 .nav-text {
38 display: none;
39 }
40
41 .nav-icon {
42 display: block;
43 font-size: 1.5rem;
44 }
45}
46
47/* Full navigation for larger containers */
48@container nav (min-width: 601px) {
49 .nav-list {
50 justify-content: space-between;
51 align-items: center;
52 }
53
54 .nav-link {
55 display: flex;
56 align-items: center;
57 gap: 0.5rem;
58 padding: 0.5rem 1rem;
59 text-decoration: none;
60 border-radius: 4px;
61 transition: all 0.2s;
62 }
63
64 .nav-link:hover {
65 background-color: #007bff;
66 color: white;
67 }
68
69 .nav-text {
70 display: inline;
71 }
72
73 .nav-icon {
74 font-size: 1.2rem;
75 }
76}Up to 600px the navigation becomes a vertical list of icons, and from 601px it shows full links with text.
3. Dashboard with Flexible Widgets
In the dashboard every widget is a container itself, so in a narrow and in a wide column it shows a different amount of detail:
1.dashboard {
2 display: grid;
3 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
4 gap: 1rem;
5 padding: 1rem;
6}
7
8.widget {
9 background: white;
10 border-radius: 8px;
11 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
12 container-type: inline-size;
13 overflow: hidden;
14}
15
16.widget-header {
17 padding: 1rem;
18 background: #f8f9fa;
19 border-bottom: 1px solid #dee2e6;
20}
21
22.widget-content {
23 padding: 1rem;
24}
25
26/* Small widget - basic information */
27@container (max-width: 350px) {
28 .widget-header h3 {
29 font-size: 1rem;
30 margin: 0;
31 }
32
33 .widget-stats {
34 display: block;
35 }
36
37 .stat-item {
38 margin-bottom: 0.5rem;
39 text-align: center;
40 }
41
42 .stat-value {
43 font-size: 1.5rem;
44 font-weight: bold;
45 display: block;
46 }
47
48 .stat-label {
49 font-size: 0.8rem;
50 color: #666;
51 }
52
53 .chart {
54 height: 150px;
55 }
56}
57
58/* Medium widget - more details */
59@container (min-width: 351px) and (max-width: 500px) {
60 .widget-header h3 {
61 font-size: 1.2rem;
62 margin: 0;
63 }
64
65 .widget-stats {
66 display: flex;
67 justify-content: space-around;
68 margin-bottom: 1rem;
69 }
70
71 .stat-item {
72 text-align: center;
73 }
74
75 .stat-value {
76 font-size: 1.8rem;
77 font-weight: bold;
78 display: block;
79 color: #007bff;
80 }
81
82 .stat-label {
83 font-size: 0.9rem;
84 color: #666;
85 }
86
87 .chart {
88 height: 200px;
89 }
90}
91
92/* Large widget - full details */
93@container (min-width: 501px) {
94 .widget-header {
95 display: flex;
96 justify-content: space-between;
97 align-items: center;
98 }
99
100 .widget-header h3 {
101 font-size: 1.4rem;
102 margin: 0;
103 }
104
105 .widget-actions {
106 display: flex;
107 gap: 0.5rem;
108 }
109
110 .widget-stats {
111 display: grid;
112 grid-template-columns: repeat(3, 1fr);
113 gap: 1rem;
114 margin-bottom: 1.5rem;
115 }
116
117 .stat-item {
118 text-align: center;
119 padding: 1rem;
120 background: #f8f9fa;
121 border-radius: 4px;
122 }
123
124 .stat-value {
125 font-size: 2rem;
126 font-weight: bold;
127 display: block;
128 color: #28a745;
129 }
130
131 .stat-label {
132 font-size: 1rem;
133 color: #666;
134 margin-top: 0.5rem;
135 }
136
137 .chart {
138 height: 250px;
139 }
140
141 .widget-details {
142 display: block;
143 margin-top: 1rem;
144 padding-top: 1rem;
145 border-top: 1px solid #dee2e6;
146 }
147}The three ranges do not overlap, which is why exactly one set of styles always applies.
Advanced Container Query Techniques
1. Logical Operators
You combine conditions with the words and, or and not, just as in media queries. The container here is the component's parent:
1.component-wrapper {
2 container-type: inline-size; /* the parent is the container */
3}
4
5/* Combining conditions */
6@container (min-width: 300px) and (max-width: 600px) {
7 .component {
8 background: lightblue;
9 }
10}
11
12/* Alternative */
13@container (max-width: 300px) or (min-width: 800px) {
14 .component {
15 border: 2px solid red;
16 }
17}
18
19/* Negation */
20@container not (min-width: 400px) {
21 .component {
22 font-size: 0.9rem;
23 }
24}The wrapper is the container, because .component could not query itself. The third rule applies whenever the container is narrower than 400px.
2. Nested Containers
Many containers can work on a page at once, one inside another, and names point to the one you are asking about:
1.main-container {
2 container-name: main;
3 container-type: inline-size;
4}
5
6.sidebar {
7 container-name: sidebar;
8 container-type: inline-size;
9}
10
11.article {
12 container-name: article;
13 container-type: inline-size;
14}
15
16/* Different styles depending on context */
17@container main (min-width: 1200px) {
18 .layout {
19 display: grid;
20 grid-template-columns: 300px 1fr;
21 gap: 2rem;
22 }
23}
24
25@container sidebar (max-width: 250px) {
26 .sidebar-item {
27 padding: 0.5rem;
28 }
29
30 .sidebar-text {
31 display: none;
32 }
33}
34
35@container article (min-width: 600px) {
36 .article-content {
37 columns: 2;
38 column-gap: 2rem;
39 }
40}A named query looks for the nearest ancestor with that name, while an unnamed query takes the nearest container of any kind.
3. Container Queries with CSS Grid
Container Queries combine very well with Grid. The gallery has from one to four columns, depending on the width of its wrapper:
1.gallery-wrapper {
2 container-type: inline-size; /* the gallery's wrapper is the container */
3}
4
5/* Responsive gallery based on container size */
6@container (max-width: 400px) {
7 .gallery {
8 display: grid;
9 grid-template-columns: 1fr;
10 gap: 1rem;
11 }
12}
13
14@container (min-width: 401px) and (max-width: 800px) {
15 .gallery {
16 display: grid;
17 grid-template-columns: repeat(2, 1fr);
18 gap: 1rem;
19 }
20}
21
22@container (min-width: 801px) and (max-width: 1200px) {
23 .gallery {
24 display: grid;
25 grid-template-columns: repeat(3, 1fr);
26 gap: 1.5rem;
27 }
28}
29
30@container (min-width: 1201px) {
31 .gallery {
32 display: grid;
33 grid-template-columns: repeat(4, 1fr);
34 gap: 2rem;
35 }
36}
37
38/* Adaptive images */
39.gallery-item {
40 position: relative;
41 overflow: hidden;
42 border-radius: 8px;
43}
44
45@container (max-width: 400px) {
46 .gallery-item {
47 aspect-ratio: 16/9;
48 }
49
50 .gallery-item img {
51 width: 100%;
52 height: 100%;
53 object-fit: cover;
54 }
55
56 .gallery-caption {
57 position: absolute;
58 bottom: 0;
59 left: 0;
60 right: 0;
61 background: linear-gradient(transparent, rgba(0,0,0,0.8));
62 color: white;
63 padding: 1rem;
64 font-size: 0.9rem;
65 }
66}
67
68@container (min-width: 801px) {
69 .gallery-item {
70 aspect-ratio: 1;
71 transition: transform 0.3s ease;
72 }
73
74 .gallery-item:hover {
75 transform: scale(1.05);
76 }
77
78 .gallery-caption {
79 position: absolute;
80 bottom: 0;
81 left: 0;
82 right: 0;
83 background: rgba(0,0,0,0.9);
84 color: white;
85 padding: 1rem;
86 transform: translateY(100%);
87 transition: transform 0.3s ease;
88 }
89
90 .gallery-item:hover .gallery-caption {
91 transform: translateY(0);
92 }
93}The container is .gallery-wrapper, because the gallery cannot ask about its own width. In a wide container the images become squares with a caption that slides in on hover.
Container Queries vs Media Queries
When to Use Container Queries?
The two techniques complement each other: Container Queries serve components, and media queries serve changes to the whole page:
1/* Good use of Container Queries */
2.component {
3 container-type: inline-size;
4}
5
6/* Components that can be used in different contexts */
7@container (max-width: 300px) {
8 .card {
9 /* Styles for narrow container */
10 flex-direction: column;
11 }
12}
13
14/* Media Queries for global changes */
15@media (max-width: 768px) {
16 body {
17 font-size: 14px;
18 }
19
20 .global-header {
21 padding: 1rem;
22 }
23}
24
25/* Don't use Container Queries for global changes */
26@container (max-width: 768px) {
27 body { /* This won't work as expected */
28 font-size: 14px;
29 }
30}The last rule will not work, because body has no container above it. Leave global changes to media queries.
Practical Example - Modular Card
The same profile card HTML stands once in the wide main column and once in the narrow sidebar:
1<!-- HTML -->
2<div class="page-layout">
3 <main class="main-content">
4 <div class="card-container">
5 <div class="profile-card">
6 <img src="avatar.jpg" alt="Profile" class="avatar">
7 <div class="info">
8 <h3 class="name">John Smith</h3>
9 <p class="role">Frontend Developer</p>
10 <p class="description">Passionate about creating amazing web experiences</p>
11 <div class="actions">
12 <button class="btn primary">Follow</button>
13 <button class="btn secondary">Message</button>
14 </div>
15 </div>
16 </div>
17 </div>
18 </main>
19
20 <aside class="sidebar">
21 <div class="card-container">
22 <div class="profile-card">
23 <!-- Same card, but in a narrower container -->
24 <img src="avatar.jpg" alt="Profile" class="avatar">
25 <div class="info">
26 <h3 class="name">John Smith</h3>
27 <p class="role">Frontend Developer</p>
28 <p class="description">Passionate about creating amazing web experiences</p>
29 <div class="actions">
30 <button class="btn primary">Follow</button>
31 <button class="btn secondary">Message</button>
32 </div>
33 </div>
34 </div>
35 </div>
36 </aside>
37</div>Both copies have identical code. The styles below make each card adapt to its own .card-container:
1/* CSS with Container Queries */
2.card-container {
3 container-type: inline-size;
4}
5
6.profile-card {
7 background: white;
8 border-radius: 12px;
9 padding: 1.5rem;
10 box-shadow: 0 4px 6px rgba(0,0,0,0.1);
11 transition: all 0.3s ease;
12}
13
14/* Narrow container - vertical layout */
15@container (max-width: 350px) {
16 .profile-card {
17 text-align: center;
18 }
19
20 .avatar {
21 width: 80px;
22 height: 80px;
23 border-radius: 50%;
24 margin: 0 auto 1rem;
25 display: block;
26 }
27
28 .name {
29 font-size: 1.2rem;
30 margin-bottom: 0.5rem;
31 }
32
33 .role {
34 color: #666;
35 font-size: 0.9rem;
36 margin-bottom: 1rem;
37 }
38
39 .description {
40 display: none; /* Hide in narrow container */
41 }
42
43 .actions {
44 display: flex;
45 flex-direction: column;
46 gap: 0.5rem;
47 }
48
49 .btn {
50 padding: 0.75rem;
51 border: none;
52 border-radius: 6px;
53 font-size: 0.9rem;
54 cursor: pointer;
55 transition: all 0.2s;
56 }
57
58 .btn.primary {
59 background: #007bff;
60 color: white;
61 }
62
63 .btn.secondary {
64 background: #f8f9fa;
65 color: #666;
66 }
67}
68
69/* Medium container - mixed layout */
70@container (min-width: 351px) and (max-width: 500px) {
71 .profile-card {
72 display: flex;
73 align-items: center;
74 gap: 1rem;
75 text-align: left;
76 }
77
78 .avatar {
79 width: 100px;
80 height: 100px;
81 border-radius: 50%;
82 flex-shrink: 0;
83 }
84
85 .info {
86 flex: 1;
87 }
88
89 .name {
90 font-size: 1.3rem;
91 margin-bottom: 0.25rem;
92 }
93
94 .role {
95 color: #666;
96 font-size: 1rem;
97 margin-bottom: 0.5rem;
98 }
99
100 .description {
101 font-size: 0.9rem;
102 color: #888;
103 margin-bottom: 1rem;
104 display: -webkit-box;
105 -webkit-line-clamp: 2;
106 -webkit-box-orient: vertical;
107 overflow: hidden;
108 }
109
110 .actions {
111 display: flex;
112 gap: 0.5rem;
113 }
114
115 .btn {
116 padding: 0.5rem 1rem;
117 border: none;
118 border-radius: 6px;
119 font-size: 0.9rem;
120 cursor: pointer;
121 transition: all 0.2s;
122 }
123
124 .btn.primary {
125 background: #007bff;
126 color: white;
127 }
128
129 .btn.secondary {
130 background: #f8f9fa;
131 color: #666;
132 border: 1px solid #dee2e6;
133 }
134}
135
136/* Wide container - full layout */
137@container (min-width: 501px) {
138 .profile-card {
139 display: flex;
140 align-items: flex-start;
141 gap: 1.5rem;
142 }
143
144 .avatar {
145 width: 120px;
146 height: 120px;
147 border-radius: 50%;
148 flex-shrink: 0;
149 }
150
151 .info {
152 flex: 1;
153 }
154
155 .name {
156 font-size: 1.5rem;
157 margin-bottom: 0.5rem;
158 font-weight: 600;
159 }
160
161 .role {
162 color: #007bff;
163 font-size: 1.1rem;
164 margin-bottom: 1rem;
165 font-weight: 500;
166 }
167
168 .description {
169 font-size: 1rem;
170 color: #555;
171 line-height: 1.6;
172 margin-bottom: 1.5rem;
173 }
174
175 .actions {
176 display: flex;
177 gap: 1rem;
178 }
179
180 .btn {
181 padding: 0.75rem 1.5rem;
182 border: none;
183 border-radius: 8px;
184 font-size: 1rem;
185 font-weight: 500;
186 cursor: pointer;
187 transition: all 0.2s;
188 }
189
190 .btn.primary {
191 background: #007bff;
192 color: white;
193 }
194
195 .btn.primary:hover {
196 background: #0056b3;
197 }
198
199 .btn.secondary {
200 background: white;
201 color: #666;
202 border: 2px solid #dee2e6;
203 }
204
205 .btn.secondary:hover {
206 border-color: #007bff;
207 color: #007bff;
208 }
209}In the sidebar the card stands vertically and hides the description, and in the main column it shows everything, without a single media query.
Container Queries Best Practices
1. Performance and Optimization
Choose the narrowest container type you need, group rules with the same breakpoints and avoid nesting queries:
1/* Use only the needed container type */
2.width-only {
3 container-type: inline-size; /* Better for performance */
4}
5
6.both-dimensions {
7 container-type: size; /* Only when you need height */
8}
9
10/* Group similar breakpoints */
11@container (max-width: 400px) {
12 .component-a { /* styles */ }
13 .component-b { /* styles */ }
14 .component-c { /* styles */ }
15}
16
17/* Avoid excessive nesting */
18@container (min-width: 300px) {
19 @container (max-width: 600px) { /* Hard to manage */
20 .component { /* styles */ }
21 }
22}A nested @container is valid but hard to read, so combine conditions with the and operator.
2. Fallback for Older Browsers
Container Queries have worked in all major browsers since 2023, and @supports lets you add them as an enhancement:
1/* Fallback without Container Queries */
2.responsive-component {
3 padding: 1rem;
4 background: #f8f9fa;
5}
6
7.responsive-component .title {
8 font-size: 1.2rem;
9 margin-bottom: 1rem;
10}
11
12.responsive-component .content {
13 display: block;
14}
15
16/* Container Queries as enhancement */
17@supports (container-type: inline-size) {
18 .responsive-container {
19 container-type: inline-size;
20 }
21
22 @container (min-width: 400px) {
23 .responsive-component {
24 padding: 2rem;
25 }
26
27 .responsive-component .title {
28 font-size: 1.5rem;
29 }
30
31 .responsive-component .content {
32 display: flex;
33 gap: 1rem;
34 }
35 }
36}Every browser gets the base styles, and newer ones also get the container-based layout. Nothing breaks.
Container Query Units
Container Queries introduce special units that refer to the container size:
| Unit | Description |
|---|---|
cqw | 1% of container width (container query width) |
cqh | 1% of container height (container query height) |
cqi | 1% of container inline size (container query inline) |
cqb | 1% of container block size (container query block) |
Let's use the cqw unit so that the card title grows together with its container:
1@container (min-width: 400px) {
2 .card-title {
3 font-size: 5cqw; /* 5% of container width */
4 }
5}These units work like vw and vh, only relative to the container. When an element has no container above it, they are calculated relative to the window.
My advice: leave media queries for the page layout, and build components with Container Queries. Later in this module you will meet the :has() selector, which styles a parent based on its children and, together with Container Queries, forms a duo of modern responsiveness. In the sandbox below, add container-type: inline-size to .card-container and watch the card.
Remember: a media query asks about the size of the whole palace, and a container query asks about the size of the chamber your component stands in.
Code for this lesson: index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <title>Container Queries</title>
6 <link rel="stylesheet" href="style.css">
7</head>
8<body>
9 <h1>Container Queries</h1>
10 <div class="wide-wrapper">
11 <div class="card-container">
12 <div class="card">
13 <div class="card-img">IMG</div>
14 <div class="card-body">
15 <h2>Scarab</h2>
16 <p>A symbol of rebirth in ancient Egypt.</p>
17 </div>
18 </div>
19 </div>
20 </div>
21 <div class="narrow-wrapper">
22 <div class="card-container">
23 <div class="card">
24 <div class="card-img">IMG</div>
25 <div class="card-body">
26 <h2>Ankh</h2>
27 <p>The key of life - a symbol of eternity.</p>
28 </div>
29 </div>
30 </div>
31 </div>
32</body>
33</html>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What are Container Queries in CSS?
2. How do you define a container for Container Queries?
Hands-on tasks in the game
- Code editor
Create .card-container { container-type: inline-size }. Inside, add a .card that changes flex-direction from column to row at @container (min-width: 300px). Place it at different widths - the card will adapt!