[Web dev for beginners] CSS layout: flexbox, grid, media queries and container queries

[2025-10-22] dev, learning web dev, css
(Ad, please don’t block)

This blog post is part of the series “Web development for beginners” – which teaches people who have never programmed how to create web apps with JavaScript.

To download the projects, go to the GitHub repository learning-web-dev-code and follow the instructions there.

I’m interested in feedback! If there is something you don’t understand, please write a comment at the end of this page.


CSS provides a variety of services for web content:

  • In the previous chapter, we used it to format content: to change colors, typefaces, etc.
  • In this chapter, we will use it to lay out content: to place HTML elements on a page.

Almost all of the diagrams in this blog post were created via HTML and CSS. You can check out the originals here: html/css-layout.html

What is CSS layout?  

What does “laying out content” actually mean? Let’s look at two examples where want HTML elements to be placed on a page in particular fashion.

The first example is a horizontal list of links with gaps between them. The dashed gray border shows the (invisible) boundary of the <div> that contains the links:

This is how we’d like to tell CSS what to do: The links should be placed in a horizontal row, with gaps between them.

CSS flexible box layout helps us with that: It arranges HTML elements in rows and columns.

CSS layout example: content plus sidebar  

The second example is a sidebar that occupies a fixed narrow space on the left of the page. The rest is taken up by the actual content:

This is how we’d like to tell CSS what to do: There is a grid with two columns and a single row. The first column has a fixed narrow width. The second column is as wide as fits the page. The row is as tall as is necessary for the content to fit.

CSS grid layout helps us with that: It lets us place HTML elements on a two-dimensional grid.

CSS layout terminology  

Layout container vs. layout items  

CSS layout works like this: An outer HTML element contains children that we want to lay out.

<div class="container">
  <div class="item">A</div>
  <div class="item">B</div>
  <div class="item">C</div>
</div>

The outer HTML element is called the container. Its children are called items. We activate a layout via property display – e.g.:

.container {
  display: flex;
}

That changes how elements are arranged inside .container.

CSS axes: inline (row) vs. block (column)  

We distinguish:

  • A dimension is one extension of space (in two directions) – e.g.:
    • the horizontal dimension
    • the vertical dimension
  • An axis exists within a specific dimension but additionally has a direction – e.g.:
    • the x-axis exists within the horizontal dimension.
    • the y-axis exists within the vertical dimension.

Axes matter for operations such as aligning.

CSS takes into consideration that not all writing systems go from left to right and from top to bottom. It distinguishes:

  • Inline axis: In which direction do inline HTML elements (such as <strong>) flow? In English, they flow from left to right. The term row is a synonym for inline axis.

  • Block axis: In which direction do block HTML elements (such as <p>) flow? In English, they flow from top to bottom. The term column is is a synonym for block axis.

The following table summarizes these terms:

Axis Synonym Dimension
inline axis row horizontal
block axis column vertical

CSS flexible box (flexbox) layout  

CSS flexible box layout (short: flexbox layout) is a one-dimensional layout. Its most common use cases are:

  • Arranging a sequence of HTML elements in a row (horizontally), with gaps.
  • Arranging a sequence of HTML elements in a column (vertically), with gaps.

Basic flexbox usage  

Flexbox CSS properties  

We need the following CSS properties to use flexbox layout:

  • display: flex activates flexbox layout.
  • flex-direction specifies in which direction the layout items flow inside the layout container. The corresponding axis is called the main axis. The other axis is called the cross-axis. It is perpendicular to the main axis.
  • justify-content distributes the items along the main axis.
  • align-items and align-self align the items along the cross-axis.
  • gap inserts fixed gaps between items.

content means that a property affects groups of items. items and self means that a property affects items individually.

Note that all properties except display are optional. If we don’t specify them, default values are used.

The following diagram visualizes some flexbox terms:

Note that this diagram only applies if the writing mode is (like) the one used by Latin-based languages and the flex direction is row (which is the default).

display and flex-direction  

Consider the following HTML:

<div class="vertical">
  Text without span
  <span>Text inside span</span>
  <a href="https://example.com">Link</a>
</div>

We are going to use flexbox to arrange the inline HTML elements inside .vertical vertically:

.vertical {
  display: flex;
  flex-direction: column;
}

The result looks like this:

What do the two CSS properties do?

  • display activates flexbox layout for the <div> (because it has the class vertical).
  • flex-direction tells CSS that the main axis is the block axis (vertical in this case).

It’s interesting that flexbox doesn’t care if a child of .vertical is an inline element or a block element. Even sequences of text are considered to be layout items (which are called invisible items because they are not HTML elements). However, if a sequence of text consists only of whitespace characters then it does not count as an item and is ignored. One example of that is the text between </span> and <a>.

flex-direction can have the following values:

  • row (default): The main axis is the inline axis.
  • row-reverse: The main axis is the inline axis, but with its direction reversed.
  • column: The main axis is the block axis.
  • column-reverse: The main axis is the block axis, but with its direction reversed.

If there is no flex-direction then row is used as a default.

The following diagrams visualize how these values work:

justify-content distributes items along main axis  

Let’s change the layout:

.vertical {
  height: 7rem;
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
}

Previously, the height of the container was derived from its items. Now we set a fixed height. With that height, there is extra vertical space left beyond what is needed for the items. We use justify-content to tell CSS how to distribute that extra space along the main axis: We want it to be inserted before, after and between the items. The result looks like this:

justify-content can have the following values:

  • flex-start (default): The items are all put at the start of the main axis. They are followed by all extra space.
  • flex-end: The items are all put at the end of the main axis. They are preceded by all extra space.
  • center: The items are all put at the center of the main axis. They are preceded by half of any extra space and followed the other half.
  • space-between: Any extra space is distributed equally between the items.
  • space-around: Each item gets its own share of the space, before and after it. Therefore, there are spaces at the start and at the end – which are half as wide as the spaces between the items.
  • space-evenly: There are spaces at the start, between the items and at the end. They all have the same size.

The following diagrams visualize how these values work:

gap inserts fixed gaps between items  

Let’s use gap to insert fixed spaces between the items:

.vertical {
  display: flex;
  flex-direction: column;
  gap: 0.5em;
}

The height of the container is once again derived from the heights of its items. However, now there are vertical gaps between items that are 0.5em high. Each gap increases the height of the container. The container now looks like this:

align-items aligns all items along cross-axis  

Let’s center items horizontally (without gaps):

.vertical {
  display: flex;
  flex-direction: column;
  align-items: center;
}

We use align-items to center the items along the cross-axis. The result looks like this:

align-items can have the following values:

  • stretch (default) makes each item as long along the cross-axis as possible.
  • center centers each item along the cross-axis.
  • flex-start moves each item to the start of the cross-axis.
  • flex-end moves each item to the end of the cross-axis.
  • baseline makes sure that the baselines of all items line up.

The following diagrams visualize how these values work:

align-self aligns one item along cross-axis  

If present, align-items provides a default value for the property align-self of each item. We can override the former via the latter:

.vertical {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
a {
  align-self: flex-end;
}

Now the HTML is displayed like this:

Wrapping items  

flex-wrap wraps items  

By default, a flexbox consists of a single line and does not wrap its items. We can change that via property flex-wrap. This property changes the cross-axis which determines the position and order of the lines. It does not affect the main axis (which determines the order of items within a line). Its values are:

  • nowrap (default): No wrapping happens.
  • wrap: Items are wrapped. The cross-axis is the block axis. It determines the position and order of the lines. t does not change the axis.
  • wrap-reverse: Items are wrapped. The cross-axis is the block axis, but with its direction reversed.

These diagrams visualize the values:

Note that with a fixed width, nowrap leads to the fixed widths of the items being compressed so that they fit. CSS layouts normally don’t do that.

align-content distributes wrapped lines along the cross-axis  

If a flexbox wraps then we can use align-content to distribute the lines along the cross-axis. It can have these values:

  • stretch (default)
  • flex-start
  • flex-end
  • center
  • space-between
  • space-around
  • space-evenly

The following diagrams illustrate how these values work:

Gaps when wrapping  

Flexbox has two more properties for gaps:

  • column-gap is for specifying gaps along the inline axis (regardless of the main axis)
  • row-gap is for specifying gaps along the block axis (regardless of the main axis)

Property gap is actually a shorthand for these two properties:

gap: 1rem;
  /* column-gap: 1rem; row-gap: 1rem; */

gap: 1rem 2rem;
  /* column-gap: 1rem; row-gap: 2rem; */

The following diagrams illustrate how these properties work:

Notes:

  • The row diagrams use different items than the column diagrams.
  • Gaps along the cross-axis are only used if wrapping is active. Therefore, without wrapping, a gap with a single value can be a good choice because then we don’t have to think about axes and “column” vs. “row”.

The names of properties for alignment  

For alignment properties, flexbox layout uses the following terminology:

  • Along which axis is the property operating?
    • justify-* indicates that the property operates along the main axis.
    • align-* indicates that the property operates along the cross-axis.
  • What entities are affected by the property?
    • *-content indicates that a property affects groups of interdependent items.
    • *-self indicates that a property affects individual items. It is therefore an item property – while all other properties are container properties.
    • *-items provides a default value for the *-self properties of all items.

The following table shows all alignment properties supported by flexbox layout:

content (C) items (C) self (I)
justify (main axis) justify-content
align(cross-axis) align-content align-items align-self
(requires flex-wrap)

Notes:

  • (C) Container properties
  • (I) Item properties
  • Property align-content only has an effect if flex-wrap is active.

How to make sense of “justify” vs. “align”  

Flexbox layout and grid layout handle dimensions differently:

  • Flexbox operates along a single main axis – which is either the inline axis or the block axis.
  • Grid operates along two axes: the inline axis and the block axis.

For alignment, the authors of the standards wanted to share as many property names as possible between flexbox and grid, therefore, they did not use the terms “inline” and “block” (which would have worked well for grid), but went with more abstract terms that were inspired by the names of two text alignment properties:

  • “justify” was inspired by text-justify. That property operates along the inline axis (which is the default main axis of flexbox).
  • “align” was inspired by vertical-align. This property operates along an axis that is perpendicular to the axis of text-justify:
    • For flexbox, that means the cross-axis.
    • For grid, that means the block axis.

Exercises: flexbox layout  

Exercise: Modify the initial example  

Turn the initial example into a horizontal list of links with gaps that wraps:

  • Create a new HTML document. You can use the HTML skeleton from the chapter on HTML.
  • Go to the initial flexbox example on the HTML page and copy the HTML inside the <section> and the CSS inside the section rule.
  • As content, use three or more links that you like.
  • Add CSS for gaps and for wrapping the flexbox.
  • Open the document in a web browser and check that it does indeed wrap if you change the size of the viewport.

Exercise: Play the game “Flexbox Froggy”  

The game “Flexbox Froggy” by Thomas Park may be too difficult for beginners, but you can check it out and see if you can make sense of it.

Summary: flexbox properties  

The following table summarizes the flexbox properties:

CSS property Default Element Axis Affects
display container
flex-direction row container
justify-content flex-start container main groups of items
align-items stretch container cross single items
align-self value of item cross single items
align-items
gap 0px container main

Material on flexbox layout  

CSS grid layout  

Where flexbox is mostly used for one-dimensional layouts, grid is for two-dimensional layouts: The container defines a grid and the items are placed on it. That involves the following steps:

  • First we activate grid layout via display: grid
  • Next, we define a grid:
    • We define how wide each column is.
    • We define how high each row is.
  • Then we place the items on the grid: Per child, we specify where it starts in the grid and how much it extends along the inline axis and the block axis.
  • Lastly, we configure how empty space is distributed inside cells and between cells.

The running grid layout example  

To explore how grid layout works, we’ll create the following layout:

If we look at this layout, we can already see a grid with cells: Some HTML elements (header, left bar, content) occupy single cells, others (right bar, footer) more than one.

Activating grid layout and defining the grid  

The following CSS sets up the grid:

.container {
  display: grid;
  grid-template-columns: 3.5rem 1fr 3.5rem;
  grid-template-rows: 1.5rem 1fr 1.5rem;
  /* ... */
}

We have defined the following columns and rows:

Columns and rows are called tracks. Each track is delimited by two lines. These are some of the values we can use to define the track sizes:

  • Tracks can have fixed sizes such as 3.5 rem and 50px.

  • 1fr stands for one fraction of the space that remains after all fixed tracks were allocated their shares. The number is called the flex factor. How many fractions there are, is determined by adding all flex factors. As an example: If there are only two tracks with flexible sizes, one with 1fr and another one with 2fr, then there are three fractions in total: The first track gets one third of the remaining space and the second track gets two thirds of it.

  • Length percentages such as 50% are relative to the the inline dimension (columns) or the block dimension (rows) of the content box of the container.

  • min-content tries to make cells as thin (narrow or short) as possible while the content still fits. It uses the size of its thickest cell as its size.

  • max-content tries to make cells as thick (wide or tall) as possible while the content still fits. It uses the size of its thickest cell as its size.

  • minmax(min, max) ensures that the size of a track is at least min and at most max. That has many uses. One example is dealing with fr which can become arbitrarily thin and arbitrarily thick. minmax() lets us constrain that:

    • minmax(3rem, 1fr) takes up one fraction of the remaining space, but never less than 3rem.
    • minmax(1fr, 50px) takes up one fraction of the remaining space, but never more than 50px.
  • repeat(n, track-def) creates n tracks, each of which has the definition track-def – e.g.:

    • repeat(3, 2rem) creates three tracks, each 2rem thin.

The following diagrams illustrate how min-content and max-content work. Notably, min-content does not make a track narrower than the content allows.

Placing items on the grid  

Next, we have to tell grid layout where on the grid to place the grid items (lines have numbers):

We’ll place the following HTML elements:

<div class="container">
  <div class="header">
    header
  </div>
  <div class="leftbar">
    left bar
  </div>
  <div class="content">
    content
  </div>
  <div class="rightbar">
    right bar
  </div>
  <div class="footer">
    footer
  </div>
</div>

Let’s look at three ways in which items can be placed:

  • Per axis, we can specify at which line an item starts and ends.
  • We can define named grid areas and tell grid layout per item which area it occupies.
  • If we don’t explicitly place an item, then grid layout automatically places it for us.

Placing items via lines  

The following CSS places the header:

.header {
  grid-column-start: 2;
  grid-column-end: 3;
  grid-row-start: 1;
  grid-row-end: 2;
}

We tell CSS grid layout that the header:

  • Extends from line 2 to line 3 along the column axis.
  • Extends from line 1 to line 2 along the row axis.

We could also have written the CSS like this:

.header {
  grid-column: 2 / 3;
  grid-row: 1 / 2;
}

Another option is to specify these properties via the HTML attribute style:

<div style="grid-column: 2 / 3; grid-row: 1 / 2">
  header
</div>

If we only mention one number then an item spans one cell – e.g., the following two declarations are equivalent:

grid-column: 4;
grid-column: 4 / 5;

Placing items via grid areas  

A grid area is a region of the grid that is rectangular (delimited by two lines per axis). One way of defining regions is by writing words in a textual grid:

.container {
  /* ... */
  grid-template-areas: 
    ".       header  rightbar"
    "leftbar content rightbar"
    "footer  footer  footer"
  ;
}

Notes:

  • If a template area spans multiple grid layout cells, we put its name in all corresponding text grid cells.
  • Putting a dot (.) in a text grid cell means that the corresponding grid layout cell must remain empty.
  • We don’t have to align the words and we don’t have to write each row of the textual grid in a separate line, but both make the grid easier to understand for humans.

Now that we have defined the grid areas, we can use them to place items:

.header {
  grid-area: header;
}

We can also specify the CSS property grid-area via the HTML style attribute:

<div class="header" style="grid-area: header">
  header
</div>

Placing items automatically  

If we don’t place items explicitly, grid layout places them for us – by default, one item per cell. It starts at the beginnings of the inline axis and the block axis. We can use property grid-auto-flow to configure how it continues. These are two values (among others) that we can use:

  • row (default) places along the inline axis first and the block axis second.
  • column (default) places along the block axis first and the inline axis second.

The following diagrams show how that works:

Alignment  

Alignment: inline axis vs. block axis  

For alignment properties, grid layout uses the following terminology:

  • Along which axis is the property operating?
    • justify-* indicates that the property operates along the inline axis. This term was inspired by the text property text-justify.
    • align-* indicates that the property operates along the block axis. This term was inspired by the text property vertical-align.
  • What entities are affected by the property?
    • *-content indicates that a property affects tracks (entities created by the container).
    • *-self indicates that a property affects individual items. It is therefore an item property – while all other properties are container properties.
    • *-items provides a default value for the *-self properties of all items.

The following table shows all alignment properties supported by grid layout:

content (C) items (C) self (I)
justify (inline axis) justify-content justify-items justify-self
align (block axis) align-content align-items align-self

Notes:

  • (C) Container properties
  • (I) Item properties

Arranging items along the inline axis: justify-items and justify-self  

Property justify-items arranges items along the inline axis. It can have the following values:

justify-items provides a default value for the justify-self property of each item. Therefore, the latter can override the former.

Arranging items along the block axis: align-items and align-self  

Property align-items arranges items along the block axis. It can have the following values:

align-items provides a default value for the align-self property of each item. Therefore, the latter can override the former.

Aligning columns: justify-content  

If there is a resizable column such as 1fr then a grid layout can always fill the complete inline axis of the container. If not then we can use property justify-content to tell it how to distribute unused space:

Aligning rows: align-content  

align-content specifies how to distribute unused space between rows:

Adding fixed spaces between items: column-gap, row-gap and gap  

Grid layout supports two properties for adding gaps between items:

  • column-gap is for specifying gaps along the inline axis
  • row-gap is for specifying gaps along the block axis

Additionally we can use property gap as a shorthand:

gap: 1rem;
  /* column-gap: 1rem; row-gap: 1rem; */

gap: 1rem 2rem;
  /* column-gap: 1rem; row-gap: 2rem; */

The following diagrams illustrate how these properties work:

Exercise: grid layout  

Mostly follow the steps described in the first exercise and play with the grid layout example:

  • Change the grid-template-areas.
  • Change aligments, gaps, padding, etc.

Material on grid layout  

Choosing between flexbox layout and grid layout  

How can we decide which CSS layout mechanism to use?

  • For simple spacing, simple properties such as margin, padding and text-align may already be enough.
  • Flexbox has a few obvious use cases:
    • A single row of HTML elements with gaps between them
    • A row with gaps that wraps into multiple lines if necessary
    • A column with gaps
  • For most other things, grid works well.

Often, we are not faced with a single layout use case but with multiple, nested ones – e.g., the top-level layout may be a grid but that grid may contain a column of elements – which are laid out via a nested flexbox. We’ll see an example of that later.

Layout examples  

In this section we apply what we have learned to examples that are small but often occur in real-world projects.

Centering content horizontally and vertically  

Centering text horizontally is easy to do in CSS. As an example, take the following HTML:

<div class="container">
  Centered
</div>

We can center the text horizontally like this:

.container {
  text-align: center;
}

If the height of the <div> is greater than the height of the text, we may additionally want to center the text vertically. That is more complicated. One elegant solution is to use flexbox:

.container {
  display: flex;
  justify-content: center;
  align-items: center;
}
  • justify-content centers along the inline axis.
  • align-items centers along the block axis.

The result looks like this:

A row of inlines with gaps  

Given the following HTML:

<div class="row-of-inlines">
  <span>Home</span>
  |
  <span>About</span>
  |
  <span>Follow</span>
  |
  <span>Archive</span>
</div>

In real projects, those spans are often links. We’d like the HTML to look like this:

To achieve that, we use a flexbox to insert gaps between the spans:

.row-of-inlines {
  display: flex;
  gap: 1rem;
}

A column of blocks with gaps  

This is a sequence of block elements:

<div class="column-of-blocks">
  <div>A</div>
  <div>B</div>
  <div>C</div>
</div>

We’d like to arrange them like this:

We can use flexbox to do that:

.column-of-blocks {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

A row of blocks that wraps  

Consider this sequence of blocks:

<div class="row-of-blocks">
  <div>A</div>
  <div>B</div>
  <div>C</div>
  <div>D</div>
  <div>E</div>
</div>

We want to lay them out as follows:

To do that, we need this CSS:

.row-of-blocks {
  flex-wrap: wrap;
  gap: 1rem;
}

Aligning labels and text fields  

Consider the following user interface:

We can use <label> and <input type="text"> to create it. However, if we want to use grid to align a label with its text field then we can’t nest the latter inside the former. Instead, we have to connect them via an ID and the HTML attribute for:

<div class="container">
  <label for="celsius">Celsius:</label>
  <input type="text" id="celsius" size="5" value="100">

  <label for="fahrenheit">Fahrenheit:</label>
  <input type="text" id="fahrenheit" size="5" value="212">
</div>

The grid layout for this HTML looks like this:

.container {
  display: grid;
  grid-template-columns: min-content min-content;
  grid-template-rows: min-content;
  gap: 0.5rem;
}

We did not place the layout items outselves, we let grid layout place them automatically.

If we wanted to align the baselines of the smaller font used for the label and the larger font used for the text field, we could do that like this:

.container {
  /* ... */
  align-items: baseline;
}

Responsive design via CSS queries  

During the early mobile web (in the late 2000s), there were often two versions of a web app:

  • A desktop version that was optimized for large screens
  • A mobile version that was optimized for small screens

The idea behind responsive design is to use a single design for both that flexibly adapts to screen sizes. In this section, we’ll look at two CSS features that help us with that:

  • Media queries activate different CSS declarations depending on the screen size and other criteria.
  • Container queries activate different CSS declarations depending on the size of a container (think wrapper element).

Arithmetic with CSS units: calc()  

In this section, we’ll need to perform some simple arithmetic with CSS lengths and the CSS function calc() lets us do that. As an example, let’s assume we have:

article {
  box-sizing: border-box;
  padding-left: 1rem;
  padding-right: 1rem;
  /* ... */
}

And we want the content to take up 50% of its parent. Then we can use calc() like this:

article {
  /* ... */
  width: calc(1rem + 50% + 1rem);
}

calc() lets us use arithmetic operations such as addition (+), subtraction (-), multiplication (*) and division (/). And we can freely mix various CSS units.

Note that it’s often better to switch to content box sizing than to use calc() but that’s not always possible.

Media queries: adapting to viewport sizes  

The following user interface changes in response to how wide the viewport is:

Left-aligning the links in the sidebar was a deliberate decision; I find that more pleasant to read.

How can we switch between layouts in CSS? Via media queries:

@media «condition» {
  /* Conditional declarations go here */
}

A media query contains declarations that are only active if the condition is true. The condition checks the medium the HTML is displayed on. Examples include:

/* Is the HTML displayed on a printed page? */
@media print { /* ... */ }

/* Is the HTML displayed on a screen? */
@media screen { /* ... */ }

/* Is the viewport less than 1024px wide? */
@media (width < 1024px) { /* ... */ }

/* Displayed on screen and viewport less than 1024px wide? */
@media screen and (width < 1024px) { /* ... */ }

Often the declarations of the media query override defaults that were set up earlier. As an example, the following CSS responsively changes between the two layouts shown above:

body {
  display: grid;
  grid-template-columns: 5rem 1fr;
  grid-template-rows: min-content;
  gap: 0.5rem;
  grid-template-areas: 
    "sidebar content"
  ;

  @media (width < calc(5rem + 0.5rem + 10rem)) {
    grid-template-columns: 1fr;
    grid-template-rows: min-content min-content;
    grid-template-areas: 
      "content"
      "sidebar"
    ;
  }
}

Notes:

  • By default, we have two columns and the sidebar is to the left of the content.

  • The condition of the @media at-rule only lets us refer to the total width of the viewport. We’d like the layout to change if the content is narrower than 10rem. Therefore, we use calc() to compute the sum of the width of the sidebar, the width of the gap and 10rem. If the width of the viewport is less than that sum, we change the grid layout: There is a single column and the sidebar is below the content.

It would be nice if we could create a CSS variable for the calculated width. Alas, browsers currently don’t support that – even though the standard allows it.

Breakpoints  

The points at which the layout changes are called breakpoints. In the previous CSS, the result of calc() is a breakpoint. This term was first used in 2010 (source, first appearance in the jQuery Mobile codebase). It’s not completely clear what it inspired that name. Two explanations seem plausible:

  • Mathematical functions can have breakpoints where they are not continuous.

  • Most non-responsive layouts “break” at a certain point when we make the viewport smaller. Then it’s time to switch to a different layout.

A more complicated sidebar layout  

The following layout is similar to the previous one (the elements of the sidebar are also deliberately left-aligned):

Note the additional features:

  • In the first “screenshot”, we can see that the content has a maximum width. As a consequence, text lines won’t get too wide even if the viewport is very wide. We additionally center it horizontally, which also helps with wide viewports.

  • In the second “screenshot”, we can see that the flow of the elements of the sidebar changes if it is underneath the content.

  • In the third “screenshot”, we can see that the elements of the of the sidebar wrap if there is not enough horizontal space for them. We can also see that the content is padded – which prevents text from touching the edges of the viewport.

<div class="layout">
  <div class="sidebar" style="grid-area: sidebar">
    <div>1</div>
    <div>2</div>
    <!-- ... -->
  </div>
  <div class="content" style="grid-area: content">
    <p>This is the content.</p>
    <p>It contains multiple paragraphs.</p>
    <p>They wrap automatically.</p>
  </div>
</div>

The following CSS is basically the same as in the previous example:

.layout {
  display: grid;
  grid-template-columns: 2rem 1fr;
  grid-template-rows: min-content;
  grid-template-areas: 
    "sidebar content"
  ;
  gap: 0.25rem;

  @media (width < calc(2rem + 0.25rem + 10rem)) {
    grid-template-columns: 1fr;
    grid-template-rows: min-content min-content;
    grid-template-areas: 
      "content"
      "sidebar"
    ;
  }
}

.sidebar uses the same media query as .layout in order to go from column flex direction to row flex direction. In the former mode, we don’t wrap because we want the elements to determine the height of the sidebar. In the latter mode, we enable flex-wrapping.

.sidebar {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  @media (width < calc(2rem + 0.25rem + 10rem)) {
    flex-direction: row;
    flex-wrap: wrap;
  }
}

This is the CSS for the content:

.content {
  min-width: 5rem;
  max-width: 14rem;
  padding: 0 0.5rem;
  justify-self: center;
}
  • The content has a minimum width and a maximum width.
  • Because the paragraphs have vertical margins, there is vertical padding. A horizontal padding of 0.5rem prevents the content from ever touching the edges of the viewport.
  • We use justify-self to center the content horizontally.

Container queries  

Where media queries react viewport size changes, container queries react to the size changes of an HTML element. That means that a single HTML element can become responsive. With container queries, two roles are important:

<div class="wrapper">
  <div class="layout">
    <!-- ... -->
  </div>
</div>

.wrapper is the container. We can query it as if it were a viewport. However, we can’t use queries to change the layout of .wrapper, we can only do so in a nested HTML element such as .layout.

Note that CSS re-uses the term container for this context:

  • .wrapper is a query container.
  • .layout is a layout container.

The following CSS turns .wrapper into a query container:

.wrapper {
  container-type: size;
  container-name: wrapper;
}

Property container-type can have these values:

  • normal: The HTML element is not a query container.
  • size: Enables container size queries for both the inline axis and the block axis.
  • inline-size: Only enables container size queries for the inline axis.

Now we can use a container query for .layout:


.layout {
  /* Default declarations go here */

  @container wrapper «condition» {
    /* Conditional declarations go here */
  }
}

As you can see, the syntax of container queries is very similar to the one of media queries. We can omit the name wrapper after @container. Then the query refers to the most recently declared container.

Example: responsive contact card  

The following image shows a contact card at three different sizes:

The contact card adapts in two ways:

  • If it’s not wide enough, it switches to a vertical layout.
  • If it’s additionally not high enough, it hides the description.

This is the HTML behind the contact card:

<div class="wrapper">
  <div class="contact-card">
    <img class="headshot"
         src="css-layout/person.svg"
         width="110" height="110"
    >
    <div class="name">
      E. Scrooge
    </div>
    <div class="description">
      The hard-working boss of our company. Profits matter!
    </div>
  </div>
</div>

Let’s look at the CSS that implements this behavior.

First, we enable container queries for .wrapper:

.wrapper {
  container-type: size;
  container-name: wrapper;
}

Second, we set up the default layout:

.contact-card {
  display: grid;
  grid-template-columns: min-content 1fr;
  grid-template-rows: min-content 1fr;
  grid-template-areas: 
    "headshot name"
    "headshot description"
  ;
  gap: 0.5rem;
  padding: 0.5rem;
  /* ... */
}

Third, we use a container query to set up the layout that is used when we are inside the width breakpoint but not inside the height breakpoint.

.contact-card {
  /* ... */
  @container wrapper
    (width < calc(0.5rem + 110px + 0.5rem + 5.5rem + 0.5rem)) and
    (height >= calc(0.5rem + 110px + 0.5rem + 1.2rem + 0.5rem + 5rem + 0.5rem))
  {
    grid-template-columns: 1fr;
    grid-template-rows: min-content min-content min-content;
    grid-template-areas: 
      "headshot"
      "name"
      "description"
    ;
  }
  /* ... */
}

Explanation of the calc() uses:

  • 110px is the fixed width of the image. 5.5rem is the minimum width of name and description. The rest is padding or a gap.
  • 110px is the fixed height of the image. 1.2rem is the minimum height of the name. 5rem is the minimum height of the description. The rest is padding or gaps.

How does the layout change? The layout is now vertical and consists of a single column. The headshot comes first and is followed by the name and the description.

Fourth, we set up the layout that is used when we are inside the width breakpoint and inside the height breakpoint.

.contact-card {
  /* ... */
  @container wrapper
    (width < calc(0.5rem + 110px + 0.5rem + 5.5rem + 0.5rem)) and
    (height < calc(110px + 1rem))
  {
    grid-template-columns: 1fr;
    grid-template-rows: min-content min-content;
    grid-template-areas: 
      "headshot"
      "name"
    ;
    .description {
      display: none;
    }
  }
}

How does the layout change? The description is not a template area anymore and hidden via the display property.

The remaining CSS assigns grid areas to HTML elements. These remain the same regardless of what breakpoints were triggered.

.headshot {
  grid-area: headshot;
}

.name {
  grid-area: name;
  font-weight: bold;
}

.description {
  grid-area: description;
}

Exercise: CSS queries  

Mostly follow the steps described in the first exercise and play with the contact card example:

  • Go from contact card to image plus caption:
    • There is no name, only a description.
    • The smallest size only shows the image.
  • You can also play with gaps, padding, alignments and text styles.

Material on CSS queries  

More CSS layout features  

We have taken a comprehensive look at CSS layout mechanism but there is more – these are a few interesting examples:

  • Flexbox layout has more features – e.g.:
  • Grid layout has more features – e.g.:
    • CSS subgrid lets us put grids into grid cells whose cells line up.
  • Viewport units let us specify widths and heights as percentages of the current viewport. That is different from normal percentages which are relative to the content size of the surrounding element (at the top level, that’s the page). Especially on mobile devices, the viewport can change temporarily – e.g. when a keyboard comes up. There are also viewport units that let us dynamically adapt to such changes.

Tip against feeling overwhelmed by the sheer amount of CSS features: For most of those features, it’s enough to be aware what a feature does. Then, when you actually need it, you can learn the details on the spot. That’s usually better than “learning ahead”.

More resources on CSS layout