HTML and CSS course Β· Module 9: Modern CSS

Container Queries - Container-Based Responsiveness

14 min read
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 @container query,
  • 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:

UnitDescription
cqw1% of container width (container query width)
cqh1% of container height (container query height)
cqi1% of container inline size (container query inline)
cqb1% 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. 1. What are Container Queries in CSS?

  2. 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!

Useful articles