Tag: Layout

Using The New Constrained Layout In WordPress Block Themes

One of the main goals of the WordPress Site Editor (and, yes, that is now the “official” name) is to move basic block styling from CSS to structured JSON. JSON files are machine-readable, which makes it consumable by the JavaScript-based Site Editor for configuring a theme’s global styles directly in WordPress.

It’s not all the way there yet! If we look at the Twenty Twenty-Two (TT2) default theme, there were two main unresolved issues: styling interactions (like :hover, :active, :focus), and the margins and padding of layout containers. You can see how those were temporarily fixed in the TT2 style.css file rather than making it into the theme.json file.

WordPress 6.1 fixed those issues and what I want to do is look specifically at the latter. Now that we have JSON-ified styles for the margins and padding of layout containers, that opens us up to more flexible and robust ways to define spacing in our theme layouts.

What kind of spacing are we talking about?

First off, we already have root-level padding which is a fancy way of describing padding on the <body> element. That’s nice because it ensures consistent spacing on an element that is shared on all pages and posts.

But there’s more to it because now we have a way for blocks to bypass that padding and align themselves full-width. That’s thanks to padding-aware alignments which is a new opt-in feature in theme.json. So, even if you have root-level padding, you can still allow, say, an image (or some other block) to break out and go full-width.

That gets us to another thing we get: constrained layouts. The idea here is that any blocks nested in the layout respect the layout’s content width — which is a global setting — and do not flow outside of it. We can override that behavior on a block-by-block basis with alignments, but we’ll get to that.

Let’s start with…

Root-level padding

Again, this isn’t new. We’ve had the ability to set padding on the <body> element in theme.json since the experimental Gutenberg plugin introduced it in version 11.7. We set it on the styles.spacing object, where we have margin and padding objects to define the top, right, bottom, and left spacing on the body:

{   "version": 2,   "styles": {     "spacing": {       "margin": {         "top": "60px",         "right": "30px",         "bottom": "60px",         "left": "30px"       },       "padding": {         "top": "30px",         "right": "30px",         "bottom": "30px",         "left": "30px"       }     }   } }

This is a global setting. So, if we were to crack open DevTools and inspect the <body> element, we would see these CSS styles:

body {   margin-top: 60px;   margin-right: 30px;   margin-bottom: 60px;   margin-left: 30px;   padding-top: 30px;   padding-right: 30px;   padding-bottom: 30px;   padding-left: 30px; }

Cool. But herein lies the issue of how in the world we can allow some blocks to break out of that spacing to fill the full screen, edge-to-edge. That’s why the spacing is there, right? It helps prevent that from happening!

But there are indeed plenty of cases where you might want to break out of that spacing on a one-off instance when working in the Block Editor. Say we plop an Image block on a page and we want it to go full-width while the rest of the content respects the root-level padding?

Enter…

Padding-aware alignments

While attempting to create the first default WordPress theme that defines all styles in the theme.json file, lead designer Kjell Reigstad illustrates the challenging aspects of breaking out of root-level padding in this GitHub issue.

Root-level padding prevents blocks from taking up the full viewport width (left). But with padding-aware alignments, some blocks can “opt-out” of that spacing and take up the full viewport width (right). (Image credit: Kjell Reigstad)

New features in WordPress 6.1 were created to address this issue. Let’s dig into those next.

useRootPaddingAwareAlignments

A new useRootPaddingAwareAlignments property was created to address the problem. It was actually first introduced in the Gutenberg plugin v13.8. The original pull request is a nice primer on how it works.

{   "version": 2,   "settings": {     "appearanceTools": true,     "useRootPaddingAwareAlignments": true,     // etc.   },

Right off the bat, notice that this is a feature we have to opt into. The property is set to false by default and we have to explicitly set it to true in order to enable it. Also notice that we have appearanceTools set to true as well. That opts us into UI controls in the Site Editor for styling borders, link colors, typography, and, yes, spacing which includes margin and padding.

Setting appearanceTools set to true automatically opts blocks into margin and padding without having to set either settings.spacing.padding or setting.spacing.margin to true.

When we do enable useRootPaddingAwareAlignments, we are provided with custom properties with root padding values that are set on the <body> element on the front end. Interestingly, it also applies the padding to the .editor-styles-wrapper class so the spacing is displayed when working in the back-end Block Editor. Pretty cool!

I was able to confirm those CSS custom properties in DevTools while digging around.

Enabling useRootPaddingAwareAlignments also applies left and right padding to any block that supports the “content” width and “wide” width values in the Global Styles image above. We can also define those values in theme.json:

{   "version": 2,   "settings": {     "layout": {       "contentSize": "640px",       "wideSize": "1000px"     }   } }

If the Global Styles settings are different than what is defined in theme.json, then the Global Styles take precedence. You can learn all about managing block theme styles in my last article.

  • contentSize is the default width for blocks.
  • wideSize provides a “wide” layout option and establishes a wider column for blocks to stretch out.

So, that last code example will give us the following CSS:

/* The default content container */ .wp-container-[id] > * {   max-width: 640px;   margin-left: auto !important;   margin-right: auto !important; }  /* The wider content container */ .wp-container-[id] > .alignwide {   max-width: 1000px; }

[id] indicates a unique number automatically generated by WordPress.

But guess what else we get? Full alignment as well!

.wp-container-[id] .alignfull {   max-width: none; }

See that? By enabling useRootPaddingAwareAlignments and defining contentSize and wideSize, we also get a full alignment CSS class for a total of three container configurations for controlling the width of blocks that are added to pages and posts.

This applies to the following layout-specific blocks: Columns, Group, Post Content, and Query Loop.

Block layout controls

Let’s say we add any of those aforementioned layout-specific blocks to a page. When we select the block, the block settings UI offers us new layout settings based on the settings.layout values we defined in theme.json (or the Global Styles UI).

We’re dealing with very specific blocks here — ones that can have other blocks nested inside. So, these Layout settings are really about controlling the width and alignment of those nested blocks. The “Inner blocks use content width” setting is enabled by default. If we toggle it off, then we have no max-width on the container and the blocks inside it go edge-to-edge.

If we leave the toggle on, then nested blocks will adhere to either the contentWidth or wideWidth values (more on that in a bit). Or we can use the numeric inputs to define custom contentWidth and wideWidth values in this one-off instance. That’s great flexibility!

Wide blocks

The settings we just looked are set on the parent block. Once we’ve nested a block inside and select it, we have additional options in that block to use the contentWidth, wideWidth, or go full-width.

This Image block is set to respect the contentWidth setting, labeled “None” in the contextual menu, but can also be set to wideWidth (labeled “Wide width”) or “Full width”.

Notice how WordPress multiplies the root-level padding CSS custom properties by -1 to create negative margins when selecting the “Full width” option.

The .alignfull class sets negative margins on a nested block to ensure it takes up the full viewport width without conflicting with the root-level padding settings.

Using a constrained layout

We just covered the new spacing and alignments we get with WordPress 6.1. Those are specific to blocks and any nested blocks within blocks. But WordPress 6.1 also introduces new layout features for even more flexibility and consistency in a theme’s templates.

Case in point: WordPress has completely restructured its Flex and Flow layout types and gave us a constrained layout type that makes it easier to align block layouts in themes using the content width settings in the Site Editor’s Global Styles UI.

Flex, Flow, and Constrained layouts

The difference between these three layout types is the styles that they output. Isabel Brison has an excellent write-up that nicely outlines the differences, but let’s paraphrase them here for reference:

  • Flow layout: Adds vertical spacing between nested blocks in the margin-block direction. Those nested blocks can also be aligned to the left, right, or center.
  • Constrained layout: Same exact deal as a Flow layout, but with width constraints on nested blocks that are based on the contentWidth and wideWidth settings (either in theme.json or Global Styles).
  • Flex layout: This was unchanged in WordPress 6.1. It uses CSS Flexbox to create a layout that flows horizontally (in a row) by default, but can flow vertically as well so blocks stack one on top of another. Spacing is applied using the CSS gap property.

This new slate of layout types creates semantic class names for each layout:

Semantic layout class Layout type Supported blocks
.is-layout-flow Flow layout Columns, Group, Post Content, and Query Loop.
.is-layout-constrained Constrained layout Columns, Group, Post Content, and Query Loop.
.is-layout-flex Flex layout Columns, Buttons, Social Icons

Justin Tadlock has an extensive write-up on the different layout types and semantic classes, including use cases and examples.

Updating your theme to support constrained layouts

If you’re already using a block theme of your own making, you’re going to want to update it to support constrained layouts. All it takes is swapping out a couple of things in theme.json:

{   "version": 2,   "settings": {     "layout": {       "type": "constrained", // replaces `"inherit": true`       "type": "default", // replaces `"inherit": false`     }   } }

These are recently released block themes that have enabled spacing settings with useRootPaddingAwareAlignments and have an updated theme.json file that defines a constrained layout:

Theme Root-level padding Constrained layout features
TT3 Source code Source codeTemplates
ProWP Source code Source codeTemplates
Triangulate Source code Source codeTemplates
Oaknut Source code Source codeTemplates
Loudness Source code Source codeTemplates
Pixl Source code Source codeTemplates
Block Canvas Source code Source code, Templates
Rainfall Source code Source codeTemplates

Disabling layout styles

The base layout styles are default features that ship in WordPress 6.1 Core. In other words, they’re enabled right out of the box. But we can disable them if we need to with this little snippet in functions.php:

// Remove layout styles. add_theme_support( 'disable-layout-styles' );

Big warning here: disabling support for the default layout types also removes all of the base styling for those layouts. That means you’ll need to roll your own styles for spacing, alignments, and anything else needed to display content in different template and block contexts.

Wrapping up

As a great fan of full-width images, the new contained WordPress 6.1 layout and padding aware alignment features are two of my most favorites yet. Taken together with other tools including, better margin and padding control, fluid typography, and updated List and Quote blocks, among others, is solid proof that WordPress is moving towards a better content creation experience.

Now, we have to wait and look at how the imagination and creativity of ordinary designers and content creators use these incredible tools and take it to a new level.

Because of the site editor development iterations in progress, we should always anticipate a difficult path ahead. However, as an optimist, I am eager to see what will happen in the upcoming version of WordPress 6.2. Some of the thing, that I am keeping a close eye on are things like features being considered for inclusion, support for sticky positioning, new layout class names for inner block wrappers, updated footer alignment options, and adding constrained and flow layout options to Cover blocks.

This GitHub issues #44720 lists the layout related discussions slated for WordPress 6.2.

Additional resources

I consulted and referenced a lot of sources while digging into all of this. Here’s a big ol’ list of things I found helpful and think you might enjoy as well.

Tutorials

WordPress posts

GitHub pull requests and issues


Using The New Constrained Layout In WordPress Block Themes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , , , ,

Using Grid Named Areas to Visualize (and Reference) Your Layout

Whenever we build simple or complex layouts using CSS Grid, we’re usually positioning items with line numbers. Grid layouts contain grid lines that are automatically indexed with positive and negative line numbers (that is unless we explicitly name them). Positioning items with line numbers is a fine way to lay things out, though CSS Grid has numerous ways to accomplish the same with an undersized cognitive encumbrance. One of those ways is something I like to think of as the “ASCII” method.

The ASCII method in a nutshell

The method boils down to using grid-template-areas to position grid items using custom-named areas at the grid container level rather than line numbers.

When we declare an element as a grid container using display: grid, the grid container, by default, generates a single-column track and rows that sufficiently hold the grid items. The container’s child elements that participate in the grid layout are converted to grid items, irrespective of their display property.

For instance, let’s create a grid by explicitly defining columns and rows using the grid-template-columns and grid-template-rows properties.

.grid {   display: grid;   grid-template-columns: 1fr 1fr;   grid-template-rows: repeat(3, 200px); }

This little snippet of CSS creates a 3×2 grid where the grid items take up equal space in the columns, and where the grid contains three rows with a track size of 200px.

We can define the entire layout with named grid areas using the grid-template-areas property. According to the spec, the initial value of grid-template-areas is none.

grid-template-areas = none | <string>+

<string>+ is listing the group of strings enclosed with a quote. Each string is represented as a cell, and each quoted string is represented as a row. Like this:

grid-template-areas: "head head" "nav main" "foot foot";

The value of grid-template-areas describes the layout as having four grid areas. They are,

  • head
  • nav
  • main
  • foot

head and foot span two column tracks and one row track. The remaining nav and main each span one column track and one row track. The value of grid-template-areas is a lot like arranging ASCII characters, and as Chris suggested a while back, we can get a visualization of the overall structure of the layout from the CSS itself which is the most trouble-free way to understand it.

(Full size GIF)

OK, so we created our layout with four named grid areas: head, nav, main, foot.

Now, let’s start to position the grid items against named grid areas instead of line numbers. Specifically, let’s place a header element into the named grid area head and specify the named grid area head in the header element using the grid-area property.

Named grid areas in a grid layout are called idents. So, what we just did was create a custom ident named head that we can use to place items into certain grid tracks.

header { grid-area: head; }

We can other HTML elements using other custom idents:

nav { grid-area: nav; } main { grid-area: main; } footer { grid-area: foot; }

Writing named area values

According to CSS Grid Layout Module Level 1, all strings must be defined under the following tokens:

  • Named cell token: This represents the named grid area in the grid. For instance, head is a named cell token.
  • Null cell token: This represents the unnamed grid area in the grid container. For instance, an empty cell in the grid is a null cell token.
  • Trash token: This is a syntax error, such as an invalid declaration. For instance, a disparate number of cells and rows compared to the number of grid items would make a declaration invalid.

In grid-template-area, every quoted string (the rows) must have the same number of cells and define the complete grid without ignoring any cell.

We can ignore a cell or leave it as an empty cell using the full-stop character (.)

.grid {    display: grid;   grid-template-areas:     "head head"     "nav main"     "foot ."; }

If that feels visually awkward or imbalanced to you, we can use multiple full-stop characters without any whitespaces separating them:

.grid {   display: grid;   grid-template-areas:     "head head"     "nav main"     "foot ...."; }

A named cell token can span multiple grid cells, But those cells must form a rectangular layout. In other words, we’re unable to create “L” or “T”-shaped layouts, although the spec does hint at support for non-rectangular layouts with disconnected regions in the future.

ASCII is better than line-based placement

Line-based placement is where we use the grid-column and grid-row properties to position an element on the grid using grid line numbers that are automatically indexed by a number:

.grid-item {   grid-column: 1 / 3; /* start at grid column line 1 and span to line 3 */ }

But grid item line numbers can change if our layout changes at a breakpoint. In those cases, it’s not like we can rely on the same line numbers we used at a specific breakpoint. This is where it takes extra cognitive encumbrance to understand the code.

That’s why I think an ASCII-based approach works best. We can redefine the layout for each breakpoint using grid-template-areas within the grid container, which offers a convenient visual for how the layout will look directly in the CSS — it’s like self-documented code!

.grid {   grid-template-areas:     "head head"     "nav main"     "foot ...."; /* much easier way to see the grid! */ }  .grid-item {   grid-area: foot; /* much easier to place the item! */ }

We can actually see a grid’s line numbers and grid areas in DevTools. In Firefox, for example, go to the Layout panel. Then, under the Grid tab, locate the “Grid display settings” and enable the “Display line number” and “Display area names” options.

Enabling grid settings.

This ASCII approach using named areas requires a lot less effort to visualize and easily find the placement of elements.

Line-based placement versus ASCII Art placement.

Let’s look at the “universal” use case

Whenever I see a tutorial on named grid areas, the common example is generally some layout pattern containing header, main, sidebar, and footer areas. I like to think of this as the “universal” use case since it casts such a wide net.

The Holy Grail layout in rectangles.

It’s a great example to illustrate how grid-template-areas works, but a real-life implementation usually involves media queries set to change the layout at certain viewport widths. Rather than having to re-declare grid-area on each grid item at each breakpoint to re-position everything, we can use grid-template-areas to “respond” to the breakpoint instead — and get a nice visual of the layout at each breakpoint in the process!

Before defining the layout, let’s assign an ident to each element using the grid-area property as a base style.

header {   grid-area: head; }  .left-side {   grid-area: left; }  main {   grid-area: main; }  .right-side {   grid-area: right; }  footer {   grid-area: foot; }

Now, let’s define the layout again as a base style. We’re going with a mobile-first approach so that things will stack by default:

.grid-container {   display: grid;   grid-template-areas:     "head"     "left"     "main"     "right"     "foot"; }

Each grid item is auto-sized in this configuration — which seems a little bit weird — so we can set min-height: 100vh on the grid container to give us more room to work with:

.grid-container {   display: grid;   grid-template-areas:     "head"     "left"     "main"     "right"     "foot";   min-height: 100vh; }

Now let’s say we want the main element to sit to the right of the stacked left and right sidebars when we get to a slightly wider viewport width. We re-declare grid-template-areas with an updated ASCII layout to get that:

@media (min-width: 800px) {   .parent {     grid-template-columns: 0.5fr 1fr;     grid-template-rows: 100px 1fr 1fr 100px;     grid-template-areas:       "head head"       "left main"       "right main"       "foot foot";   } }

I tossed some column and row sizing in there purely for aesthetics.

As the browser gets even wider, we may want to change the layout again, so that main is sandwiched between the left and right sidebars. Let’s write the layout visually!

.grid-container {   grid-template-columns: 200px 1fr 200px; /* again, just for sizing */   grid-template-areas:     "head head head"     "left main right"     "left main right"     "foot foot foot"; }

Leveraging implicit line names for flexibility

According to the spec, grid-template-areas automatically generates names for the grid lines created by named grid areas. We call these implicitly-named grid lines because they are named for us for free without any additional work.

Every named grid area gets four implicitly-named grid lines, two in the column direction and two in the row direction, where -start and -end are appended to the ident. For example, a grid area named head gets head-start and head-end lines in both directions for a total of four implicitly-named grid lines.

Implicitly assigned line names.

We can use these lines to our advantage! For instance, if we want an element to overlay the main, left, and right areas of our grid. Earlier, we talked about how layouts have to be rectangular — no “T” and “L” shaped layouts allowed. Consequently, we’re unable to use the ASCII visual layout method to place the overlay. We can, however, use our implicit line names using the same grid-area property on the overlay that we use to position the other elements.

Did you know that grid-area is a shorthand property, sort of the same way that margin and padding are shorthand properties? It takes multiple values the same way, but instead of following a “clockwise” direction like, margin — which goes in order of margin-block-start, margin-inline-end, margin-block-end, and margin-inline-startgrid-area goes like this:

grid-area: block-start / inline-start / block-end / inline-end;
Showing the block and inline flow directions in a left-to-right writing mode.

But we’re talking about rows and columns, not block and inline directions, right? Well, they correspond to one another. The row axis corresponds to the block direction, and the column axis corresponds to the inline direction:

grid-area: grid-row-start / grid-column-start / grid-row-end / grid-column-end;
Block and inline axis.

Back to positioning that overlay element as a grid item in our layout. The grid-area property will be helpful to position the element using our implicitly-named grid lines:

.overlay {   grid-area: left-start / left-start / right-end / main-end; }

Creating a minimal grid system

When we focus on layouts like the “universal” use case we just saw, it’s tempting to think of grid areas in terms of one area per element. But it doesn’t have to work like that. We can repeat idents to reserve more space for them in the layout. We saw that when we repeated the head and foot idents in the last example:

.grid-container {   grid-template-areas:     "head head head"     "left main right"     "left main right"     "foot foot foot"; }

Notice that main, left, and right are also repeated but in the block direction.

Let’s forget about full page layouts and use named grid areas on a component. Grid is just as good for component layouts as full pages!

Here’s a pretty standard hero component that sports a row of images followed by different blocks of text:

A row of weightlifting photos above a heading, blurb, then a row of three links.

The HTML is pretty simple:

<div class="hero">   <div class="image">     <img src="..." alt="" />   </div>   <div class="text">     <!-- ... -->   </div> </div>

We could do this for a real fast stacked layout:

.hero {   grid-template-areas:     "image"     "text"; }

But then we have to reach for some padding, max-width or whatever to get the text area narrower than the row of images. How about we expand our ASCII layout into a four-column grid instead by repeating our idents on both rows:

.hero {   display: grid;   grid-template-columns: repeat(4, 1fr); /* maintain equal sizing */   grid-template-areas:     "image image image image"     "text  text  text  text"; }

Alright, now we can place our grid items into those named areas:

.hero .image {   grid-area: image; }  .hero .text {   grid-area: text; }

So far, so good — both rows take up the entire width. We can use that as our base layout for small screens.

Showing grid lines on the stacked mobile version of the page.

But maybe we want to introduce the narrower text when the viewport reaches a larger width. We can use what we know about the full-stop character to “skip” columns. Let’s have the text ident skip the first and last columns in this case.

@media (min-width: 800px) {   main {     grid-template-columns: repeat(6, 1fr); /* increase to six columns */     grid-template-areas:       "image image image image image image"       "..... text  text  text  text  .....";   } }

Now we have the spacing we want:

Showing grid lines for a table-sized layout of the page.

If the layout needs additional tweaking at even larger breakpoints, we can add more columns and go from there:

.hero {   grid-template-columns: repeat(8, 1fr);   grid-template-areas:     "image image image image image image image image"     "..... text  text  text  text  text  text  ....."; }

Dev tool visualization:

Showing grid lines for a large table sized layout of the page.

Remember when 12-column and 16-column layouts were the big things in CSS frameworks? We can quickly scale up to that and maintain a nice visual ASCII layout in the code:

main {   grid-template-columns: repeat(12, 1fr);   grid-template-areas:     "image image image image image image image image image image image image"     "..... text  text  text  text  text  text  text  text  text  text  ....."; }

Let’s look at something more complex

We’ve looked at one fairly generic example and one relatively straightforward example. We can still get nice ASCII layout visualizations with more complex layouts.

Let’s work up to this:

Three images positioned around a fancy heading.

I’ve split this up into two elements in the HTML, a header and a main:

<header>   <div class="logo"> ... </div>   <div class="menu"> ... </div> </header> <main>   <div class="image"> ... </div>   <h2> ... </h2>   <div class="image"> ... </div>   <div class="image"> ... </div> </main>

I think flexbox is more appropriate for the header since we can space its child elements out easily that way. So, no grid there:

header {   display: flex;   justify-content: space-between;   /* etc. */ }

But grid is well-suited for the main element’s layout. Let’s define the layout and assign the idents to the corresponding elements that we need to position the .text and three .image elements. We’ll start with this as our baseline for small screens:

.grid {   display: grid;   grid-template-columns: repeat(4, 1fr);   grid-template-areas:     "image1 image1 .....  image2"     "texts  texts  texts  texts"     ".....  image3 image3 ....."; }

You can already see where we’re going with this, right? The layout is visualized for us, and we can drop the grid items into place with the custom idents:

.image:nth-child(1) {   grid-area: image1; }  .image:nth-last-child(2) {   grid-area: image2; }  .image:nth-last-child(1) {   grid-area: image3; }  h2 {   grid-area: texts; }
Showing grid lines on a mobile layout of the page.

That’s our base layout, so let’s venture into a wider breakpoint:

@media (min-width: 800px) {   .grid {     grid-template-columns: repeat(8, 1fr);     grid-template-areas:       ". image1 image1 ...... ......  ...... image2 ."       ". texts  texts  texts  texts   texts  image2 ."       ". .....  image3 image3 image3  image3 ...... .";   } }

I bet you know exactly how that will look because the layout is right there in the code!

Showing grid lines for a table-sized layout of the page.

Same deal if we decide to scale up even further:

.grid {   grid-template-columns: repeat(12, 1fr);   grid-template-areas:     ". image1 image1 .....  .....   .....  .....  .....  .....  .....  .....  ."     ". texts  texts  texts  texts   texts  texts  texts  texts  texts  image2 ."     ". .....  image3 image3 image3  image3 .....  .....  .....  .....  .....  ."; }
Showing grid lines for a desktop-sized layout of the page.

Here’s the full demo:

I’m using the “negative margin hack” to get the first image to overlap the heading.

Wrapping up

I’m curious if anyone else is using grid-template-areas to create named areas for the benefit of having an ASCII visual of the grid layout. Having that as a reference in my CSS code has helped de-mystify some otherwise complex designs that may have been even more complex when dealing with line numbers.

But if nothing else, defining grid layouts this way teaches us some interesting things about CSS Grid that we saw throughout this post:

  • The grid-template-areas property allows us to create custom idents — or “named areas” — and use them to position grid items using the grid-area property.
  • There are three types of “tokens” that grid-template-areas accepts as values, including named cell tokens, null cell tokens, and trash cell tokens.
  • Each row that is defined in grid-template-areas needs the same number of cells. Ignoring a single cell doesn’t create a layout; it is considered a trash token.
  • We can get a visual ASCII-like diagram of the grid layout in the grid-template-areas property value by using required whitespaces between named cell tokens while defining the grid layout.
  • Make sure there is no whitespace inside a null cell token (e.g. .....). Otherwise, a single whitespace between null cell tokens creates unnecessary empty cells, resulting in an invalid layout.
  • We can redefine the layout at various breakpoints by re-positioning the grid items using grid-area, then re-declaring the layout with grid-template-areas on the grid container to update the track listing, if needed. There’s no need to touch the grid items.
  • Custom named grid areas automatically get four implicitly assigned line names — <custom-ident>-start and <custom-ident>-end in both the column and row directions.

Using Grid Named Areas to Visualize (and Reference) Your Layout originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , , , , ,
[Top]

Zooming Images in a Grid Layout

Creating a grid of images is easy, thanks to CSS Grid. But making the grid do fancy things after the images have been placed can be tricky to pull off.

Say you want to add some fancy hover effect to the images where they grow and zoom beyond the rows and columns where they sit? We can do that!

Cool, right? If you check the code, you won’t find any JavaScript, complex selectors, or even magic numbers. And this is only one example among many we will explore!

Building the grid

The HTML code to create the grid is as simple as a list of images within a container. We don’t need more than that.

<div class="gallery">   <img>   <img>   <img>   <!-- etc. --> </div>

For the CSS, we first start by setting the grid using the following:

.gallery {   --s: 150px; /* controls the size */   --g: 10px;  /* controls the gap */    display: grid;   gap: var(--g);   width: calc(3*var(--s) + 2*var(--g)); /* 3 times the size plus 2 times the gap */   aspect-ratio: 1;   grid-template-columns: repeat(3, auto); }

In short, we have two variables, one that controls the size of the images and one that sets the size of the gap between images. aspect-ratio helps keep things in proportion.

You might be wondering why we are only defining three columns but no rows. No, I didn’t forget the rows — we just don’t need to explicitly set them. CSS Grid is capable of automatically placing items on implicit rows and columns, meaning we get as many rows as needed to any number of images we throw at it. We can explicitly define the rows instead but we need to add grid-auto-flow: column to make sure the browser will create the needed columns for us.

Here is an example to illustrate both cases. The difference is that one flows in a row direction an the other in a column direction.

Check out this other article I wrote for more about the implicit grids and the auto-placement algorithm.

Now that we have our grid, it’s time to style the images:

.gallery > img {   width: 0;   height: 0;   min-height: 100%;   min-width: 100%;   object-fit: cover; }

The hover effect we’re making relies on this CSS. It probably looks weird to you that we’re making images that have both no width or height but have a minimum width and height of 100%. But you will see that it’s a pretty neat trick for what we are trying to achieve.

What I’m doing here is telling the browser that the images need to have 0 width and height but also need to have a minimum height equal to 100%… but 100% of what? When using percentages, the value is relative to something else. In this case, our image is placed inside a grid cell and we need to know that size to know what’s 100% is relative to.

The browser will first ignore min-height: 100% to calculate the size of the grid cells, but it will use the height: 0 in its calculation. That means our images will not contribute to the size of the grid cells… because they technically have no physical size. This will result in three equal columns and rows that are based on the size of the grid (which we defined on the .gallery’s width and aspect-ratio). The height of each grid cell is nothing but the variable --s we defined (same for the width).

Now that we have the dimensions of our grid’s cells, the browser will use it with min-height: 100% (and min-width: 100%) which will force the images to completely fill the space of each grid cell. The whole thing may look a bit confusing but the main idea is to make sure that the grid defines the size of the images rather than the other way around. I don’t want the image to define the size of the grid and you will understand why after adding the hover effect.

Creating the hover effect

What we need to do is increase the scale of the images when they’re hovered. We can do that by adjusting an image’s width and height on :hover:

.gallery {   --f: 1.5; /* controls the scale factor */ }  .gallery img:hover{   width:  calc(var(--s) * var(--f));   height: calc(var(--s) * var(--f)); }

I added a new custom variable, --f, to the mix as a scale factor to control the size on hover. Notice how I’m multiplying the size variable, --s, by it to calculate the new image size.

But you said that the image size needs to be 0. What is going on? I am lost…

What I said is still true but I am making an exception for the hovered image. I am telling the browser that only one image will have a size that’s not equal to zero — so it will contribute to the dimension of the grid — while all the others remain equal to 0.

The left side shows the grid in its natural state without any hovered images, which is what the right side is showing. All the grid cells on the left side are equal in size since all the images have no physical dimensions.

On the right side, the second image in the first row is hovered, which gives it dimensions that affect the grid cell’s size. The browser will make that specific grid cell bigger on hover, which contributes to the overall size. And since the size of the whole grid is set (because we set a fixed width on the .gallery), the other grid cells will logically respond by becoming smaller in order to keep the .gallery‘s overall size in tact.

That’s our zoom effect in action! By increasing the size of only one image we affect the whole grid configuration, and we said before that the grid defines the size of the images so that each image stretches inside its grid cell to fill all the space.

To this, we add a touch of transition and use object-fit to avoid image distortion and the illusion is perfect!

I know that the logic behind the trick is not easy to grasp. Don’t worry if you don’t fully understand it. The most important is to understand the structure of the code used and how to modify it to get more variations. That’s what we will do next!

Adding more images

We created a 3×3 grid to explain the main trick, but you have probably guessed that we there’d no need to stop there. We can make the number of columns and rows variables and add as many images as we want.

.gallery {   --n: 3; /* number of rows*/   --m: 4; /* number of columns */   --s: 150px; /* control the size */   --g: 10px;  /* control the gap */   --f: 1.5;   /* control the scale factor */    display: grid;   gap: var(--g);   width:  calc(var(--m)*var(--s) + (var(--m) - 1)*var(--g));   height: calc(var(--n)*var(--s) + (var(--n) - 1)*var(--g));   grid-template-columns: repeat(var(--m),auto); }

We have two new variables for the number of rows and columns. Then we simply define the width and height of our grid using them. Same for grid-template-columns which uses the --m variable. And just like before, we don’t need to explicitly define the rows since the CSS Grid’s auto-placement feature will do the job for us no matter how many image elements we’re using.

Why not different values for the width and height? We can do that:

.gallery {   --n: 3; /* number of rows*/   --m: 4; /* number of columns */   --h: 120px; /* control the height */   --w: 150px; /* control the width */   --g: 10px;  /* control the gap */   --f: 1.5;   /* control the scale factor */    display: grid;   gap: var(--g);   width:  calc(var(--m)*var(--w) + (var(--m) - 1)*var(--g));   height: calc(var(--n)*var(--h) + (var(--n) - 1)*var(--g));   grid-template-columns: repeat(var(--m),auto); }  .gallery img:hover{   width:  calc(var(--w)*var(--f));   height: calc(var(--h)*var(--f)); }

We replace --s with two variables, one for the width, --w, and another one for the height, --h. Then we adjust everything else accordingly.

So, we started with a grid with a fixed size and number of elements, but then we made a new set of variables to get any configuration we want. All we have to do is to add as many images as we want and adjust the CSS variables accordingly. The combinations are limitless!

What about a full-screen version? Yes, that’s also possible. All we need is to know what values we need to assign to our variables. If we want N rows of images and we want our grid to be full screen, we first need to solve for a height of 100vh:

var(--n) * var(--h) + (var(--n) - 1) * var(--g) = 100vh

Same logic for the width, but using vw instead of vh:

var(--m) * var(--w) + (var(--m) - 1) * var(--g) = 100vw

We do the math to get:

--w: (100vw - (var(--m) - 1) * var(--g)) / var(--m) --h: (100vh - (var(--n) - 1) * var(--g)) / var(--n)

Done!

It’s the same exact HTML but with some updated variables that change the grid’s sizing and behavior.

Note that I have omitted the formula we previously set on the .gallery‘s width and height and replaced them with 100vw and 100vh, respectively. The formula will give us the same result but since we know what value we want, we can ditch all that added complexity.

We can also simplify the --h and --w by removing the gap from the equation in favor of this:

--h: calc(100vh / var(--n)); /* Viewport height divided by number of rows */ --w: calc(100vw / var(--m)); /* Viewport width divided by number of columns */

This will make the hovered image grow a bit more than the previous example, but it is no big deal since we can control the scale with the --f variable we’re using as a multiplier.

And since the variables are used in one place we can still simplify the code by removing them altogether:

It’s important to note this optimization applies only to the full-screen example and not to the examples we’ve covered. This example is a particular case where we can make the code lighter by removing some of the complex calculation work we needed in the other examples.

We actually have everything we need to create the popular pattern of expanding panels:

Let’s dig even deeper

Did you notice that our scale factor can be less than 1? We can define the size of the hovered image to be smaller than --h or --w but the image gets bigger on hover.

The initial grid cell size is equal to --w and --h, so why do a smaller values make the grid cell bigger? Shouldn’t the cell get smaller, or at least maintain its initial size? And what is the final size of the grid cell?

We need to dig deeper into how the CSS Grid algorithm calculates the size of the grid cells. And this is involves understanding CSS Grid’s default stretch alignment.

Here’s an example to understand the logic.

On the left side of the demo, I defined a two-column with auto width. We get the intuitive result: two equal columns (and two equal grid cells). But the grid I set up on the right side of the demo, where I am updating the alignment using place-content: start, appears to have nothing.

DevTools helps show us what’s really happening in both cases:

In the second grid, we have two columns, but their widths equal zero, so we get two grid cells that are collapsed at the top-left corner of the grid container. This is not a bug but the logical result of the grid’s alignment. When we size a column (or row) with auto, it means that its content dictates its size — but we have an empty div with no content to make room for.

But since stretch is the default alignment and we have enough space inside our grid, the browser will stretch both grid cells equally to cover all that area. That’s how the grid on the left winds up with two equal columns.

From the specification:

Note that certain values of justify-content and align-content can cause the tracks to be spaced apart (space-around, space-between, space-evenly) or to be resized (stretch).

Note the “to be resized” which is the key here. In the last example, I used place-content which is the shorthand for justify-content and align-content

And this is buried somewhere in the Grid Sizing algorithm specs:

This step expands tracks that have an auto max track sizing function by dividing any remaining positive, definite free space equally amongst them. If the free space is indefinite, but the grid container has a definite min-width/height, use that size to calculate the free space for this step instead.

“Equally” explains why we wind up with equal grid cells, but it applies to “the free space” which is very important.

Let’s take the previous example and add content to one of the divs:

We added a square 50px image. Here’s an illustration of how each grid in our example responds to that image:

In the first case, we can see that the first cell (in red) is bigger than the second one (in blue). In the second case, the size of the first cell changes to fit the physical size of the image while the second cell remains with no dimensions. The free space is divided equally, but the first cell has more content inside which makes it bigger.

This is the math to figure out our free space:

(grid width) - (gap) - (image width) = (free space) 200px - 5px - 50px = 145px 

Divided by two — the number of columns — we get a width of 72.5px for each column. But we add the size of the image, 50px, to the first column which leaves us with one column at 122.5px and the second one equal to 72.5px.

The same logic applies to our grid of images. All the images have a size equal to 0 (no content) while the hovered image contributes to size — even if it’s just 1px — making its grid cell bigger than the others. For this reason, the scale factor can be any value bigger than 0 even decimals between 0 and 1.

To get the final width of the grid cells, we do the same calculation to get the following:

(container width) - (sum of all gaps) - (hovered image width) = (free space)

The width of container is defined by:

var(--m)*var(--w) + (var(--m) - 1)*var(--g)

…and all the gaps are equal to:

(var(--m) - 1)*var(--g)

…and for the hovered image we have:

var(--w)*var(--f)

We can calculate all of that with our variables:

var(--m)*var(--w) - var(--w)*var(--f) = var(--w)*(var(--m) - var(--f))

The number of columns is defined by --m ,so we divide that free space equally to get:

var(--w)*(var(--m) - var(--f))/var(--m)

…which gives us the size of the non-hovered images. For hovered images, we have this:

var(--w)*(var(--m) - var(--f))/var(--m) + var(--w)*var(--f) var(--w)*((var(--m) - var(--f))/var(--m) + var(--f))

If we want to control the final size of the hovered image, we consider the above formula to get the exact size we want. If, for example, we want the image to be twice as big:

(var(--m) - var(--f))/var(--m) + var(--f) = 2

So, the value of our scale multiplier, --f, needs to be equal to:

var(--m)/(var(--m) - 1)

For three columns we will have 3/2 = 1.5 and that’s the scale factor I used in the first demo of this article because I wanted to make the image twice as big on hover!

The same logic applies to the height calculation and in case we want to control both of them independently we will need to consider two scale factors to make sure we have a specific width and height on hover.

.gallery {   /* same as before */    --fw: 1.5; /* controls the scale factor for the width */    --fh: 1.2; /* controls the scale factor for the height */    /* same as before */ }  .gallery img:hover{   width:  calc(var(--w)*var(--fw));   height: calc(var(--h)*var(--fh)); }

Now, you know all the secrets to create any kind of image grid with a cool hover effect while also having control of the sizing you want using the math we just covered.

Wrapping up

In my last article, we created a complex-looking grid with a few lines of CSS that put CSS Grid’s implicit grid and auto-placement features to use. In this article, we relied on some CSS Grid sizing trickery to create a fancy grid of images that zoom on hover and cause the grid to adjust accordingly. All of this with a simplified code that is easy to adjust using CSS variables!

In the next article, we will play with shapes! We will combine CSS grid with mask and clip-path to get fancy grid of images.


Zooming Images in a Grid Layout originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSS-Tricks

, , ,
[Top]

Implicit Grids, Repeatable Layout Patterns, and Danglers

Dave Rupert with some modern CSS magic that tackles one of those classic conundrums: what happens when the CSS for component is unable to handle the content we throw at it?

The specific situation is when a layout grid expects an even number of items, but is supplied with an odd number instead. We’re left with a “dangling” element at the end that throws off the layout. Sounds like what’s needed is some Defensive CSS and Dave accomplishes it.

He reaches for :has() to write a nifty selector that sniffs out the last item in a grid that contains an odd number of items:

.items:has(.item:last-of-type:nth-of-type(odd)) .item:first-of-type { }

Breaking that down:

  • We have a parent container of .items.
  • If the container :has() an .item child that is the last of its type,
  • …and that .item happens to be an odd-numbered instance,
  • …then select the first .item element of that type and style it!

In this case, that last .item can be set to go full-width to prevent holes in the layout.

If… then… CSS has conditional logic powers! We’re only talking about support for Safari TP and Edge/Chrome Canary at the moment, but that’s pretty awesome.

As chance has it, Temani Afif recently shared tricks he learned while experimenting with implicit grids. By taking advantage of CSS Grid’s auto-placement algorithm, we don’t even have to explicitly declare a fixed number of columns and rows for a grid — CSS will create them for us if they’re needed!

No, Temani’s techniques aren’t alternative solutions to Dave’s “dangler” dilemma. But combining Temani’s approach to repeatable grid layout patterns with Dave’s defensive CSS use of :has(), we get a pretty powerful and complex-looking grid that’s lightweight and capable of handling any number of items while maintaining a balanced, repeatable pattern.


Implicit Grids, Repeatable Layout Patterns, and Danglers originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,
[Top]

Control Layout in a Multi-Directional Website

Many business websites need a multilingual setup. As with anything development-related, implementing one in an easy, efficient, and maintainable way is desirable. Designing and developing to be ready for multiple languages, whether it happens right at launch or is expected to happen at any point in the future, is smart.

Changing the language and content is the easy part. But when you do that, sometimes the language you are changing to has a different direction. For example, text (and thus layout) in English flows left-to-right while text (and thus layout) in Arabic goes right-to-left.

In this article, I want to build a multilingual landing page and share some CSS techniques that make this process easier. Hopefully the next time you’ll need to do the same thing, you’ll have some implementation techniques to draw from.

We’ll cover six major points. I believe that the first five are straightforward. The sixth includes multiple options that you need to think about first.

1. Start with the HTML markup

The lang and dir attributes will define the page’s language and direction.

<!-- English, left-to-right --> <html lang="en" dir="ltr">  <!-- Arabic, right-to-left --> <html lang="ar" dir="rtl">

Then we can use these attributes in selectors to do the the styling. lang and dir attributes are on the HTML tag or a specific element in which the language varies from the rest of the page. Those attributes help improve the website’s SEO by showing the website in the right language for users who search for it in case that each language has a separate HTML document.

Also, we need to ensure that the charset meta tag is included and its value is UTF-8 since it’s the only valid encoding for HTML documents which also supports all languages.

<meta charset="utf-8">

I’ve prepared a landing page in three different languages for demonstration purposes. It includes the HTML, CSS, and JavaScript we need.

2. CSS Custom Properties are your friend

Changing the direction may lead to inverting some properties. So, if you used the CSS property left in a left-to-right layout, you probably need right in the right-to-left layout, and so on. And changing the language may lead to changing font families, font sizes, etc.

These multiple changes may cause unclean and difficult to maintain code. Instead, we can assign the value to a custom property, then change the value when needed. This is also great for responsiveness and other things that might need a toggle, like dark mode. We can change the font-size, margin, padding, colors, etc., in the blink of an eye, where the values then cascade to wherever needed.

Here are some of the CSS custom properties that we are using in this example:

html {   /* colors */   --dark-color: #161616;   --light-color: #eee;   --primary-text-color: var(--dark-color);   --primary-bg-color: #fff;   --shadow-color: var(--light-color);   --hero-bg-gradient: linear-gradient(90deg, #30333f, #161616, #161616);    /* font sizes */   --logo-font-size: 2rem;   --lang-switcher-font-size: 1.02em;   --offers-item-after-font-size: 1.5rem;    /* margin and padding */   --btn-padding: 7px;   --sec-padding-block: 120px;    /* height and width */   --hero-height: 500px;   --cta-img-width: 45.75%; }

While styling our page, we may add/change some of these custom properties, and that is entirely natural. Although this article is about multi-directional websites, here’s a quick example that shows how we can re-assign custom property values by having one set of values on the <body>, then another set when the <body> contains a .dark class:

body {   background-color: var(--primary-bg-color);   color: var(--primary-text-color); } body.dark {   --primary-bg-color: #0f0f0f;   --primary-text-color: var(--light-color);    /* other changes */   --shadow-color: #13151a;   --hero-bg-gradient: linear-gradient(90deg, #191b20, #131313, #131313); }

That’s the general idea. We’re going to use custom properties in the same sort of way, though for changing language directions.

3) CSS pseudo-classes and selectors

CSS has a few features that help with writing directions. The following two pseudo-classes and attribute are good examples that we can put to use in this example.

The :lang() pseudo-class

We can use :lang() pseudo-class to target specific languages and apply CSS property values to them individually, or together. For example, in this example, we can change the font size when the :lang pseudo-class switches to either Arabic or Japanese:

html:lang(ar), html:lang(jp){   --offers-item-after-font-size: 1.2rem;  }

Once we do that, we also need to change the writing-mode property from its horizontal left-to-right default direction to vertical right-to-left direction account:

html:lang(jp) .about__text {   writing-mode: vertical-rl; }

The :attr() pseudo-class

The :attr() pseudo-class helps makes the “content” of the pseudo-elements like ::before or ::after “dynamic” in a sense, where we can drop the dir HTML attribute into the CSS content property using the attr() function. That way, the value of dir determines what we’re selecting and styling.

<div dir="ltr"></div> <div dir="rtl"></div>
div::after {   content: attr(dir); }

The power is the ability to use any custom data attribute. Here, we’re using a custom data-name attribute whose value is used in the CSS:

<div data-name="English content" dir="ltr"></div> <div data-name="محتوى عربي" dir="rtl"></div>
div::after {   content: attr(data-name); }

This makes it relatively easy to change the content after switching that language without changing the style. But, back to our design. The three-up grid of cards has a yellow “special” or “best” off mark beside an image.

This is the HTML for each card:

<div class="offers__item relative" data-attr="data-offer" data-i18n_attr="special_offer">   <figure class="offers__item_img">     <img src="./assets/images/offer1.png" data-attr="alt" data-i18n_attr="image_alt" alt="" class="w-100">   </figure>   <div class="offer-content_item-text">     <p class="para" data-i18n="offer_item_text"></p>     <span class="price bolder" data-i18n="offer_item_price"></span>   </div> </div>

JavaScript’s role is to:

  1. Set an attribute called data-offer on each card.
  2. Assign a “special offer” or “best offer” value to it.

Finally, we can use the data-offer attribute in our style:

.offers__item::after {   content: attr(data-offer);   /* etc. */ }

Select by the dir attribute

Many languages are left-to-right, but some are not. We can specify what should be different in the [dir='rtl']. This attribute must be on the element itself or we can use nesting to reach the wanted element. Since we’ve already added the dir attribute to our HTML tag, then we can use it in nesting. We will use it later on our sample page.

4. Prepare the web fonts

In a multilingual website, we may also want to change the font family between languages because perhaps a particular font is more legible for a particular language.

Fallback fonts

We can benefit from the fallback by writing the right-to-left font after the default one.

font-family: 'Roboto', 'Tajawal', sans-serif;

This helps in cases where the default font doesn’t support right-to-left. That snippet above is using the Roboto font, which doesn’t support Arabic letters. If the default font supports right-to-left (like the Cairo font), and the design needs it to be changed, then this is not a perfect solution.

font-family: 'Cairo', 'Tajawal', sans-serif; /* won't work as expected */

Let’s look at another way.

Using CSS variables and the :lang() pseudo-class

We can mix the previous two technique where we change the font-family property value using custom properties that are re-assigned by the :lang pseudo class.

html {   --font-family: 'Roboto', sans-serif; }  html:lang(ar){   --font-family: 'Tajawal', sans-serif; }  html:lang(jp){   --font-family: 'Noto Sans JP', sans-serif; }

5. CSS Logical Properties

In CSS days past, we used to use left and right to define offsets along the x-axis, and the top and bottom properties to to define offsets along the y-axis. That makes direction switching a headache. Fortunately, CSS supports logical properties that define direction‐relative equivalents of the older physical properties. They support things like positioning, alignment, margin, padding, border, etc.

If the writing mode is horizontal (like English), then the logical inline direction is along the x-axis and the block direction refers to the y-axis. Those directions are flipped in a vertical writing mode, where inline travels the y-axis and and block flows along the x-axis.

Writing Mode x-axis y-axis
horizontal inline block
vertical  block inline

In other words, the block dimension is the direction perpendicular to the writing mode and the inline dimension is the direction parallel to the writing mode. Both inline and block levels have start and end values to define a specific direction. For example, we can use margin-inline-start instead of margin-left. This mean the margin direction automatically inverts when the page direction is rtl. It’s like our CSS is direction-aware and adapts when changing contexts.

There is another article on CSS-Tricks, Building Multi-Directional Layouts from Ahmad El-Alfy, that goes into the usefulness of building websites in multiple languages using logical properties.

This is exactly how we can handle margins, padding and borders. We’ll use them in the footer section to change which border gets the rounded edge.

The top-tight edge of the border is rounded in a default ltr writing mode.

As long as we’re using the logical equivalent of border-top-right-radius, CSS will handle that change for us.

.footer {   border-start-end-radius: 120px; }

Now, after switching to the rtl direction, it’ll work fine.

The “call to action” section is another great place to apply this:

.cta__text {   border-start-start-radius: 50%;   border-end-start-radius: 50px; } .cta__img {   border: 1px dashed var(--secondary-color);   border-inline-start-color: var(--light-color); }

Now, in Arabic, we get the correct layout:

You might be wondering exactly how the block and inline dimensions reverse when the writing mode changes. Back to the Japanese version, the text is from vertical, going from top-to-bottom. I added this line to the code:

/* The "About" section when langauge is Japanese */ html:lang(jp) .about__text {   margin-block-end: auto;   width: max-content; }

Although I added margin to the “block” level, it is applied it to the left margin. That’s because the text rotated 90 degrees when the language switched and flows in a vertical direction.

6. Other layout considerations

Even after all this prep, sometimes where elements move to when the direction and language change is way off. There are multiple factors at play, so let’s take a look.

Position

Using an absolute or fixed position to move elements may affect how elements shift when changing directions. Some designs need it. But I’d still recommend asking yourself: do I really need this?

Fro example, the newsletter subscription form in the footer section of our example can be implemented using position. The form itself takes the relative position, while the button takes the absolute position.

<form id="newsletter-form" class="relative">   <input type="email" data-attr="placeholder" data-i18n_attr="footer_input_placeholder" class="w-100">   <button class="btn btn--tertiary footer__newsletter_btn bolder absolute" data-i18n="footer_newsLetter_btn"></button> </form>
html[dir="ltr"] .footer__newsletter_btn {   right: 0; } html[dir="rtl"] .footer__newsletter_btn {   left: 0; }
This works fine in a rtl writing mode.

In the “hero” section, I made the background using a ::before pseudo-class with an absolute position:

<header class="hero relative">   <!-- etc. --> </header>
.hero {   background-image: linear-gradient(90deg, #30333f, #161616, #161616); } .hero::before  {   content: '';   display: block;   height: 100%;   width: 33.33%;   background-color: var(--primary-color);   clip-path: polygon(20% 0%, 100% 0, 100% 100%, 0% 100%);   position: absolute;   top: 0;   right: 0; }

Here’s the HTML for our hero element:

<header class="hero relative">   <!-- etc. -->   <div class="hero__social absolute">     <div class="d-flex flex-col">       <!-- etc. -->     </div>   </div> </header>

Note that an .absolute class is in there that applies position: absolute to the hero section’s social widget. Meanwhile, the hero itself is relatively positioned.

How we move the social widget halfway down the y-axis:

.hero__social {   left: 0;   top: 50%;   transform: translateY(-50%); }

In the Arabic, we can fix the ::before pseudo-class position that is used in the background using the same technique we use in the footer form. That said, there are multiple issues we need to fix here:

  1. The clip-path direction
  2. The background linear-gradient
  3. The coffee-cup image direction
  4. The social media box’s position

Let’s use a simple flip trick instead. First, we wrap the hero content, and social content in two distinct wrapper elements instead of one:

<header class="hero relative">   <div class="hero__content">       <!-- etc. -->   </div>   <div class="hero__social absolute">     <div class="d-flex flex-col">       <!-- etc. -->     </div>   </div> </header>

Then we rotate both of the hero wrappers—the social box inner wrapper and the image—180 degrees:

html[dir="rtl"] .hero, html[dir="rtl"] .hero__content, html[dir="rtl"] .hero__img img, html[dir="rtl"] .hero__social > div {   transform: rotateY(180deg); }

Yeah, that’s all. This simple trick is also helpful if the hero’s background is an image.

transform: translate()

This CSS property and value function helps move the element on one or more axes. The difference between ltr and rtl is that the x-axis is the inverse/negative value of the current value. So, we can store the value in a variable and change it according to the language.

html {   --about-img-background-move: -20%; }  html[dir='rtl']{   --about-img-background-move: 20%; }

We can do the same thing for the background image in the another section:

<figure class="about__img relative">   <img src="image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100"> </figure>
.about__img::after {   content: '';   position: absolute;   z-index: -1;   transform: translateY(-75%) translateX(var(--about-img-background-move));   /* etc. */ }

Margins

Margins are used to extend or reduce spaces between elements. It accepts negative values, too. For example, a positive margin-top value (20%) pushes the content down, while a negative value (e.g. -20%) pulls the content up.

If margins values are negative, then the top and left margins move the item up or to the left. However, the right and bottom margins do not. Instead, they pull content that is located in the right of the item towards the left, and the content underneath the item up. For example, if we apply a negative top margin and negative bottom margin together on the same item, the item is moved up and pull the content below it up into the item.

Here’s an example:

<section>   <div id="d1"></div>   <div id="d2"></div>   <div id="d3"></div> </section>
div {   width: 100px;   height: 100px;   border: 2px solid; } #d1 {   background-color: yellow;   border-color: red; } #d2 {   background-color: lightblue;   border-color: blue; } #d3 {   background-color: green;   border-color: red; }

The result of the above code should be something like this:

Let’s add these negative margins to the #d2 element:

#d2 {   margin-top: -40px;   margin-bottom: -70px; }

Notice how the second box in the diagram moves up, thanks to a negative margin-top value, and the green box also moves up an overlaps the second box, thanks to a negative margin-bottom value.

The next thing you might be asking: But what is the difference between transform: translate and the margins?

When moving an element with a negative margin, the initial space that is taken by the element is no longer there. But in the case of translating the element using a transform, the opposite is true. In other words, a negative margin leads pulls the element up, but the transform merely changes its position, without losing the space reserved for it.

Let’s stick to using margin in one direction:

#d2 {   margin-top: -40px;   /* margin-bottom: -70px; */ }

Now let’s replace the margin with the transform:

#d2 {   /* margin-top: -40px;*/   transform: translateY(-40px); }

You can see that, although the element is pulled up, its initial space is still there according to the natural document flow.

Flexbox

The display: flex provides a quick way to control the how the elements are aligned in their container. We can use align-items and justify-content to align child elements at the parent level.

In our example, we can use flexbox in almost every section to make the column layout. Plus, we can use it in the “offers” section to center the set of those yellow “special” and “best” marks:

.offers__item::after {   content: attr(data-offer);   display: flex;   align-items: center;   justify-content: center;   text-align: center; }

The same can be applied to the hero section to center text vertically.

<div class="d-lg-flex align-items-center">   <div class="hero__text d-xl-flex align-items-center">     <div>       <!-- text -->     </div>   </div>   <figure class="hero__img relative">     <img src="/image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">   </figure> </div>

If the flex-direction value is row, then we can benefit from controlling the width for each element. In the “hero” section, we need to set the image on the angled slope of the background where the color transitions from dark gray to yellow.

.hero__text {   width: 56.5%; } .hero__img {   width: 33.33%; }

Both elements take up a a total of 89.83% of the parent container’s width. Since we didn’t specify justify-content on the parent, it defaults to start, leaving the remaining width at the end.

We can combine the flexbox with any of the previous techniques we’ve seen, like transforms and margins. This can help us to reduce how many position instances are in our code. Let’s use it with a negative margin in the “call to action” section to locate the image.

<section class="cta d-xl-flex align-items-center">   <div class="cta__text w-100">     <!-- etc. -->   </div>   <figure class="cta__img">     <img src="image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">   </figure> </section>

Because we didn’t specify the flex-wrap and flex-basis properties, the image and the text both fit in the parent. However, since we used a negative margin, the image is pulled to the left, along with its width. This saves extra space for the text. We also want to use a logical property, inline-start, instead of left to handle switching to the rtl direction.

Grid

Finally, we can use a grid container to positing the elements. CSS Grid is powerful (and different than flexbox) in that it lays things along both the x-axis and the y-axis as opposed to only one of them.

Suppose that in the “offers” section, the role of the “see all” button is to get extra data that to display on the page. Here’s JavaScript code to repeat the current content:

// offers section ==> "see all" btn functionality (function(){   document.querySelector('.offers .btn').addEventListener('click', function(){     const offersContent = document.querySelector('.offers__content');     offersContent.innerHTML += offersContent.innerHTML;     offersContent.classList.remove('offers__content--has-margin');     this.remove();   }) })();

Then, let’s use display: grid in the CSS for this section. First, here’s our HTML, with our grid container highlighted.

<div class="offers__content offers__content--has-margin d-grid">   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="special_offer">     <!-- etc. -->   </div>   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="best_offer">     <!-- etc. -->   </div>   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="best_offer">     <!-- etc. -->   </div> </div>

We implement CSS Grid on the .offers__content element:

html {   /* custom properties */   --offers-content-column: repeat(3, 1fr);   --offers-content-gap: 5vw; }  .offers__content {   display: grid;   grid-template-columns: var(--offers-content-column);   gap: var(--offers-content-gap); } .offers__content--has-margin {   margin-block-end: 60px; }

This is the result after clicking the button:

Our page is far from being the best example of how CSS Grid works. While I was browsing the designs online, I found a design that uses the following structure:

Notice how CSS Grid makes the responsive layout without media queries. And as you might expect, it works well for changing writing modes where we adjust where elements go on the grid based on the current writing mode.

Wrapping up

Here is the final version of the page. I ensured to implement the responsiveness with a mobile-first approach to show you the power of the CSS variables. Be sure to open the demo in full page mode as well.

I hope these techniques help make creating multilingual designs easier for you. We looked at a bunch of CSS properties we can use to apply styles to specific languages. And we looked at different approaches to do that, like selecting the :lang pseudo-class and data attributes using the attr() function. As part of this, we covered what logical properties are in CSS and how they adapt to a document’s writing mode—which is so much nicer than having to write additional CSS rulesets to swap out physical property units that otherwise are unaffected by the writing mode.

We also checked out a number of different positioning and layout techniques, looking specifically at how different techniques are more responsive and maintainable than others. For example, CSS Grid and Flexbox are equipped with features that can re-align elements inside of a container based on changing conditions.

Clearly, there are lots of moving pieces when working with a multilingual site. There are probably other requirements you need to consider when optimizing a site for specific languages, but the stuff we covered here together should give you all of the layout-bending superpowers you need to create robust layouts that accommodate any number of languages and writing modes.


The post Control Layout in a Multi-Directional Website appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Control Layout in a Multi-Directional Website

Many business websites need a multilingual setup. As with anything development-related, implementing one in an easy, efficient, and maintainable way is desirable. Designing and developing to be ready for multiple languages, whether it happens right at launch or is expected to happen at any point in the future, is smart.

Changing the language and content is the easy part. But when you do that, sometimes the language you are changing to has a different direction. For example, text (and thus layout) in English flows left-to-right while text (and thus layout) in Arabic goes right-to-left.

In this article, I want to build a multilingual landing page and share some CSS techniques that make this process easier. Hopefully the next time you’ll need to do the same thing, you’ll have some implementation techniques to draw from.

We’ll cover six major points. I believe that the first five are straightforward. The sixth includes multiple options that you need to think about first.

1. Start with the HTML markup

The lang and dir attributes will define the page’s language and direction.

<!-- English, left-to-right --> <html lang="en" dir="ltr">  <!-- Arabic, right-to-left --> <html lang="ar" dir="rtl">

Then we can use these attributes in selectors to do the the styling. lang and dir attributes are on the HTML tag or a specific element in which the language varies from the rest of the page. Those attributes help improve the website’s SEO by showing the website in the right language for users who search for it in case that each language has a separate HTML document.

Also, we need to ensure that the charset meta tag is included and its value is UTF-8 since it’s the only valid encoding for HTML documents which also supports all languages.

<meta charset="utf-8">

I’ve prepared a landing page in three different languages for demonstration purposes. It includes the HTML, CSS, and JavaScript we need.

2. CSS Custom Properties are your friend

Changing the direction may lead to inverting some properties. So, if you used the CSS property left in a left-to-right layout, you probably need right in the right-to-left layout, and so on. And changing the language may lead to changing font families, font sizes, etc.

These multiple changes may cause unclean and difficult to maintain code. Instead, we can assign the value to a custom property, then change the value when needed. This is also great for responsiveness and other things that might need a toggle, like dark mode. We can change the font-size, margin, padding, colors, etc., in the blink of an eye, where the values then cascade to wherever needed.

Here are some of the CSS custom properties that we are using in this example:

html {   /* colors */   --dark-color: #161616;   --light-color: #eee;   --primary-text-color: var(--dark-color);   --primary-bg-color: #fff;   --shadow-color: var(--light-color);   --hero-bg-gradient: linear-gradient(90deg, #30333f, #161616, #161616);    /* font sizes */   --logo-font-size: 2rem;   --lang-switcher-font-size: 1.02em;   --offers-item-after-font-size: 1.5rem;    /* margin and padding */   --btn-padding: 7px;   --sec-padding-block: 120px;    /* height and width */   --hero-height: 500px;   --cta-img-width: 45.75%; }

While styling our page, we may add/change some of these custom properties, and that is entirely natural. Although this article is about multi-directional websites, here’s a quick example that shows how we can re-assign custom property values by having one set of values on the <body>, then another set when the <body> contains a .dark class:

body {   background-color: var(--primary-bg-color);   color: var(--primary-text-color); } body.dark {   --primary-bg-color: #0f0f0f;   --primary-text-color: var(--light-color);    /* other changes */   --shadow-color: #13151a;   --hero-bg-gradient: linear-gradient(90deg, #191b20, #131313, #131313); }

That’s the general idea. We’re going to use custom properties in the same sort of way, though for changing language directions.

3) CSS pseudo-classes and selectors

CSS has a few features that help with writing directions. The following two pseudo-classes and attribute are good examples that we can put to use in this example.

The :lang() pseudo-class

We can use :lang() pseudo-class to target specific languages and apply CSS property values to them individually, or together. For example, in this example, we can change the font size when the :lang pseudo-class switches to either Arabic or Japanese:

html:lang(ar), html:lang(jp){   --offers-item-after-font-size: 1.2rem;  }

Once we do that, we also need to change the writing-mode property from its horizontal left-to-right default direction to vertical right-to-left direction account:

html:lang(jp) .about__text {   writing-mode: vertical-rl; }

The :attr() pseudo-class

The :attr() pseudo-class helps makes the “content” of the pseudo-elements like ::before or ::after “dynamic” in a sense, where we can drop the dir HTML attribute into the CSS content property using the attr() function. That way, the value of dir determines what we’re selecting and styling.

<div dir="ltr"></div> <div dir="rtl"></div>
div::after {   content: attr(dir); }

The power is the ability to use any custom data attribute. Here, we’re using a custom data-name attribute whose value is used in the CSS:

<div data-name="English content" dir="ltr"></div> <div data-name="محتوى عربي" dir="rtl"></div>
div::after {   content: attr(data-name); }

This makes it relatively easy to change the content after switching that language without changing the style. But, back to our design. The three-up grid of cards has a yellow “special” or “best” off mark beside an image.

This is the HTML for each card:

<div class="offers__item relative" data-attr="data-offer" data-i18n_attr="special_offer">   <figure class="offers__item_img">     <img src="./assets/images/offer1.png" data-attr="alt" data-i18n_attr="image_alt" alt="" class="w-100">   </figure>   <div class="offer-content_item-text">     <p class="para" data-i18n="offer_item_text"></p>     <span class="price bolder" data-i18n="offer_item_price"></span>   </div> </div>

JavaScript’s role is to:

  1. Set an attribute called data-offer on each card.
  2. Assign a “special offer” or “best offer” value to it.

Finally, we can use the data-offer attribute in our style:

.offers__item::after {   content: attr(data-offer);   /* etc. */ }

Select by the dir attribute

Many languages are left-to-right, but some are not. We can specify what should be different in the [dir='rtl']. This attribute must be on the element itself or we can use nesting to reach the wanted element. Since we’ve already added the dir attribute to our HTML tag, then we can use it in nesting. We will use it later on our sample page.

4. Prepare the web fonts

In a multilingual website, we may also want to change the font family between languages because perhaps a particular font is more legible for a particular language.

Fallback fonts

We can benefit from the fallback by writing the right-to-left font after the default one.

font-family: 'Roboto', 'Tajawal', sans-serif;

This helps in cases where the default font doesn’t support right-to-left. That snippet above is using the Roboto font, which doesn’t support Arabic letters. If the default font supports right-to-left (like the Cairo font), and the design needs it to be changed, then this is not a perfect solution.

font-family: 'Cairo', 'Tajawal', sans-serif; /* won't work as expected */

Let’s look at another way.

Using CSS variables and the :lang() pseudo-class

We can mix the previous two technique where we change the font-family property value using custom properties that are re-assigned by the :lang pseudo class.

html {   --font-family: 'Roboto', sans-serif; }  html:lang(ar){   --font-family: 'Tajawal', sans-serif; }  html:lang(jp){   --font-family: 'Noto Sans JP', sans-serif; }

5. CSS Logical Properties

In CSS days past, we used to use left and right to define offsets along the x-axis, and the top and bottom properties to to define offsets along the y-axis. That makes direction switching a headache. Fortunately, CSS supports logical properties that define direction‐relative equivalents of the older physical properties. They support things like positioning, alignment, margin, padding, border, etc.

If the writing mode is horizontal (like English), then the logical inline direction is along the x-axis and the block direction refers to the y-axis. Those directions are flipped in a vertical writing mode, where inline travels the y-axis and and block flows along the x-axis.

Writing Mode x-axis y-axis
horizontal inline block
vertical  block inline

In other words, the block dimension is the direction perpendicular to the writing mode and the inline dimension is the direction parallel to the writing mode. Both inline and block levels have start and end values to define a specific direction. For example, we can use margin-inline-start instead of margin-left. This mean the margin direction automatically inverts when the page direction is rtl. It’s like our CSS is direction-aware and adapts when changing contexts.

There is another article on CSS-Tricks, Building Multi-Directional Layouts from Ahmad El-Alfy, that goes into the usefulness of building websites in multiple languages using logical properties.

This is exactly how we can handle margins, padding and borders. We’ll use them in the footer section to change which border gets the rounded edge.

The top-tight edge of the border is rounded in a default ltr writing mode.

As long as we’re using the logical equivalent of border-top-right-radius, CSS will handle that change for us.

.footer {   border-start-end-radius: 120px; }

Now, after switching to the rtl direction, it’ll work fine.

The “call to action” section is another great place to apply this:

.cta__text {   border-start-start-radius: 50%;   border-end-start-radius: 50px; } .cta__img {   border: 1px dashed var(--secondary-color);   border-inline-start-color: var(--light-color); }

Now, in Arabic, we get the correct layout:

You might be wondering exactly how the block and inline dimensions reverse when the writing mode changes. Back to the Japanese version, the text is from vertical, going from top-to-bottom. I added this line to the code:

/* The "About" section when langauge is Japanese */ html:lang(jp) .about__text {   margin-block-end: auto;   width: max-content; }

Although I added margin to the “block” level, it is applied it to the left margin. That’s because the text rotated 90 degrees when the language switched and flows in a vertical direction.

6. Other layout considerations

Even after all this prep, sometimes where elements move to when the direction and language change is way off. There are multiple factors at play, so let’s take a look.

Position

Using an absolute or fixed position to move elements may affect how elements shift when changing directions. Some designs need it. But I’d still recommend asking yourself: do I really need this?

Fro example, the newsletter subscription form in the footer section of our example can be implemented using position. The form itself takes the relative position, while the button takes the absolute position.

<form id="newsletter-form" class="relative">   <input type="email" data-attr="placeholder" data-i18n_attr="footer_input_placeholder" class="w-100">   <button class="btn btn--tertiary footer__newsletter_btn bolder absolute" data-i18n="footer_newsLetter_btn"></button> </form>
html[dir="ltr"] .footer__newsletter_btn {   right: 0; } html[dir="rtl"] .footer__newsletter_btn {   left: 0; }
This works fine in a rtl writing mode.

In the “hero” section, I made the background using a ::before pseudo-class with an absolute position:

<header class="hero relative">   <!-- etc. --> </header>
.hero {   background-image: linear-gradient(90deg, #30333f, #161616, #161616); } .hero::before  {   content: '';   display: block;   height: 100%;   width: 33.33%;   background-color: var(--primary-color);   clip-path: polygon(20% 0%, 100% 0, 100% 100%, 0% 100%);   position: absolute;   top: 0;   right: 0; }

Here’s the HTML for our hero element:

<header class="hero relative">   <!-- etc. -->   <div class="hero__social absolute">     <div class="d-flex flex-col">       <!-- etc. -->     </div>   </div> </header>

Note that an .absolute class is in there that applies position: absolute to the hero section’s social widget. Meanwhile, the hero itself is relatively positioned.

How we move the social widget halfway down the y-axis:

.hero__social {   left: 0;   top: 50%;   transform: translateY(-50%); }

In the Arabic, we can fix the ::before pseudo-class position that is used in the background using the same technique we use in the footer form. That said, there are multiple issues we need to fix here:

  1. The clip-path direction
  2. The background linear-gradient
  3. The coffee-cup image direction
  4. The social media box’s position

Let’s use a simple flip trick instead. First, we wrap the hero content, and social content in two distinct wrapper elements instead of one:

<header class="hero relative">   <div class="hero__content">       <!-- etc. -->   </div>   <div class="hero__social absolute">     <div class="d-flex flex-col">       <!-- etc. -->     </div>   </div> </header>

Then we rotate both of the hero wrappers—the social box inner wrapper and the image—180 degrees:

html[dir="rtl"] .hero, html[dir="rtl"] .hero__content, html[dir="rtl"] .hero__img img, html[dir="rtl"] .hero__social > div {   transform: rotateY(180deg); }

Yeah, that’s all. This simple trick is also helpful if the hero’s background is an image.

transform: translate()

This CSS property and value function helps move the element on one or more axes. The difference between ltr and rtl is that the x-axis is the inverse/negative value of the current value. So, we can store the value in a variable and change it according to the language.

html {   --about-img-background-move: -20%; }  html[dir='rtl']{   --about-img-background-move: 20%; }

We can do the same thing for the background image in the another section:

<figure class="about__img relative">   <img src="image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100"> </figure>
.about__img::after {   content: '';   position: absolute;   z-index: -1;   transform: translateY(-75%) translateX(var(--about-img-background-move));   /* etc. */ }

Margins

Margins are used to extend or reduce spaces between elements. It accepts negative values, too. For example, a positive margin-top value (20%) pushes the content down, while a negative value (e.g. -20%) pulls the content up.

If margins values are negative, then the top and left margins move the item up or to the left. However, the right and bottom margins do not. Instead, they pull content that is located in the right of the item towards the left, and the content underneath the item up. For example, if we apply a negative top margin and negative bottom margin together on the same item, the item is moved up and pull the content below it up into the item.

Here’s an example:

<section>   <div id="d1"></div>   <div id="d2"></div>   <div id="d3"></div> </section>
div {   width: 100px;   height: 100px;   border: 2px solid; } #d1 {   background-color: yellow;   border-color: red; } #d2 {   background-color: lightblue;   border-color: blue; } #d3 {   background-color: green;   border-color: red; }

The result of the above code should be something like this:

Let’s add these negative margins to the #d2 element:

#d2 {   margin-top: -40px;   margin-bottom: -70px; }

Notice how the second box in the diagram moves up, thanks to a negative margin-top value, and the green box also moves up an overlaps the second box, thanks to a negative margin-bottom value.

The next thing you might be asking: But what is the difference between transform: translate and the margins?

When moving an element with a negative margin, the initial space that is taken by the element is no longer there. But in the case of translating the element using a transform, the opposite is true. In other words, a negative margin leads pulls the element up, but the transform merely changes its position, without losing the space reserved for it.

Let’s stick to using margin in one direction:

#d2 {   margin-top: -40px;   /* margin-bottom: -70px; */ }

Now let’s replace the margin with the transform:

#d2 {   /* margin-top: -40px;*/   transform: translateY(-40px); }

You can see that, although the element is pulled up, its initial space is still there according to the natural document flow.

Flexbox

The display: flex provides a quick way to control the how the elements are aligned in their container. We can use align-items and justify-content to align child elements at the parent level.

In our example, we can use flexbox in almost every section to make the column layout. Plus, we can use it in the “offers” section to center the set of those yellow “special” and “best” marks:

.offers__item::after {   content: attr(data-offer);   display: flex;   align-items: center;   justify-content: center;   text-align: center; }

The same can be applied to the hero section to center text vertically.

<div class="d-lg-flex align-items-center">   <div class="hero__text d-xl-flex align-items-center">     <div>       <!-- text -->     </div>   </div>   <figure class="hero__img relative">     <img src="/image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">   </figure> </div>

If the flex-direction value is row, then we can benefit from controlling the width for each element. In the “hero” section, we need to set the image on the angled slope of the background where the color transitions from dark gray to yellow.

.hero__text {   width: 56.5%; } .hero__img {   width: 33.33%; }

Both elements take up a a total of 89.83% of the parent container’s width. Since we didn’t specify justify-content on the parent, it defaults to start, leaving the remaining width at the end.

We can combine the flexbox with any of the previous techniques we’ve seen, like transforms and margins. This can help us to reduce how many position instances are in our code. Let’s use it with a negative margin in the “call to action” section to locate the image.

<section class="cta d-xl-flex align-items-center">   <div class="cta__text w-100">     <!-- etc. -->   </div>   <figure class="cta__img">     <img src="image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">   </figure> </section>

Because we didn’t specify the flex-wrap and flex-basis properties, the image and the text both fit in the parent. However, since we used a negative margin, the image is pulled to the left, along with its width. This saves extra space for the text. We also want to use a logical property, inline-start, instead of left to handle switching to the rtl direction.

Grid

Finally, we can use a grid container to positing the elements. CSS Grid is powerful (and different than flexbox) in that it lays things along both the x-axis and the y-axis as opposed to only one of them.

Suppose that in the “offers” section, the role of the “see all” button is to get extra data that to display on the page. Here’s JavaScript code to repeat the current content:

// offers section ==> "see all" btn functionality (function(){   document.querySelector('.offers .btn').addEventListener('click', function(){     const offersContent = document.querySelector('.offers__content');     offersContent.innerHTML += offersContent.innerHTML;     offersContent.classList.remove('offers__content--has-margin');     this.remove();   }) })();

Then, let’s use display: grid in the CSS for this section. First, here’s our HTML, with our grid container highlighted.

<div class="offers__content offers__content--has-margin d-grid">   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="special_offer">     <!-- etc. -->   </div>   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="best_offer">     <!-- etc. -->   </div>   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="best_offer">     <!-- etc. -->   </div> </div>

We implement CSS Grid on the .offers__content element:

html {   /* custom properties */   --offers-content-column: repeat(3, 1fr);   --offers-content-gap: 5vw; }  .offers__content {   display: grid;   grid-template-columns: var(--offers-content-column);   gap: var(--offers-content-gap); } .offers__content--has-margin {   margin-block-end: 60px; }

This is the result after clicking the button:

Our page is far from being the best example of how CSS Grid works. While I was browsing the designs online, I found a design that uses the following structure:

Notice how CSS Grid makes the responsive layout without media queries. And as you might expect, it works well for changing writing modes where we adjust where elements go on the grid based on the current writing mode.

Wrapping up

Here is the final version of the page. I ensured to implement the responsiveness with a mobile-first approach to show you the power of the CSS variables. Be sure to open the demo in full page mode as well.

I hope these techniques help make creating multilingual designs easier for you. We looked at a bunch of CSS properties we can use to apply styles to specific languages. And we looked at different approaches to do that, like selecting the :lang pseudo-class and data attributes using the attr() function. As part of this, we covered what logical properties are in CSS and how they adapt to a document’s writing mode—which is so much nicer than having to write additional CSS rulesets to swap out physical property units that otherwise are unaffected by the writing mode.

We also checked out a number of different positioning and layout techniques, looking specifically at how different techniques are more responsive and maintainable than others. For example, CSS Grid and Flexbox are equipped with features that can re-align elements inside of a container based on changing conditions.

Clearly, there are lots of moving pieces when working with a multilingual site. There are probably other requirements you need to consider when optimizing a site for specific languages, but the stuff we covered here together should give you all of the layout-bending superpowers you need to create robust layouts that accommodate any number of languages and writing modes.


The post Control Layout in a Multi-Directional Website appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

How do you make a layout with pictures down one side of a page matched up with paragraphs on the other side?

I got this exact question in an email the other day, and I thought it would make a nice blog post because of how wonderfully satisfying this is to do in CSS these days. Plus we can sprinkle in polish to it as we go.

HTML-wise, I’m thinking image, text, image, text, etc.

<img src="..." alt="..." height="" width="" /> <p>Text text text...</p>  <img src="..." alt="..." height="" width="" /> <p>Text text text...</p>  <img src="..." alt="..." height="" width="" /> <p>Text text text...</p>

If that was our entire body in an HTML document, the answer to the question in the blog post title is literally two lines of CSS:

body {   display: grid;   grid-template-columns: min-content 1fr; }

It’s going to look something like this…

Not pretty but we got the job done very quickly.

So cool. Thanks CSS. But let’s clean it up. Let’s make sure there is a gap, set the default type, and reign in the layout.

body {   display: grid;   padding: 2rem;   grid-template-columns: 300px 1fr;   gap: 1rem;   align-items: center;   max-width: 800px;   margin: 0 auto;   font: 500 100%/1.5 system-ui; } img {   max-width: 100%;   height: auto; }

I mean… ship it, right? Close, but maybe we can just add a quick mobile style.

@media (max-width: 650px) {   body {     display: block;     font-size: 80%;   }   p {     position: relative;     margin: -3rem 0 2rem 1rem;     padding: 1rem;     background: rgba(white, 0.8);   } }

OK, NOW ship it!


The post How do you make a layout with pictures down one side of a page matched up with paragraphs on the other side? appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

The Almost-Complete Guide to Cumulative Layout Shift

Here’s Jess B. Peck writing all about Google’s Core Web Vitals:

Let’s step back one. CLS is when you’re about to click on a link, and the whole page shifts and you click on a different link instead. It’s when you’re halfway through a blogpost and an ad loads and you lose your place. It is when… the layout shifts. At least, that’s what it’s trying to measure– both those shifts, how often they happen, and the irritation that causes the user.

I didn’t quite understand just how complex Cumulative Layout Shift is before reading Jess’s piece. As Jess explains:

CLS is a measure for a robot to approximate the user perception of instability. This means we’re getting a unit of change over time. It’s a three dimensional equation, and there are tons of things that can affect it. […] The idea is more to alert devs to a problem area, rather than be a perfect measurement of how annoying a page is.

I had this problem on, of all places, Google dot com. I kept tapping an element just as it appeared on screen and this sent me to the wrong page.

Jess notes that these metrics are sometimes more of an art than a science, and so we shouldn’t be obsessed with making sure that just these Core Web Vital metrics are okay. Chris mentioned a while ago that he worries folks might begin to game these metrics for improving their SEO:

This feels like the start of a weird new era of web performance where the metrics of web performance have shifted to user-centric measurements, but people are implementing tricky strategies to game those numbers with methods that, if anything, slightly harm user experience.

Harry Roberts mentioned something similar:

I feel like this is our responsibility as web developers, to explain that what we want to do here is reduce user misery on our websites. That’s not to say it’s easy, though, and there’s certainly not much we can do to avoid the shady folks who’ll game these metrics only to improve SEO.

As Jeremy wrote just the other day:

The map is not the territory. The numbers are a proxy for user experience, but it’s notoriously difficult to measure intangible ideas like pain and frustration.

Direct Link to ArticlePermalink


The post The Almost-Complete Guide to Cumulative Layout Shift appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

The Holy Grail Layout with CSS Grid

A reader wrote in asking specifically how to build this layout in CSS Flexbox:

My answer: That’s not really a layout for CSS Flexbox. You could pull it off if you had to, but you’d need some kind of conceit, like grouping the nav and article together in a parent element (if not more grouping). CSS Grid was born to describe this kind of layout and it will be far easier to work with, not to mention that the browser support for both is largely the same these days.

What do you mean by “Holy Grail”?

See, kids, layout on the web used to be so janky that the incredible simple diagram above was relatively difficult to pull off, particularly if you needed the “columns” there to match heights. I know, ridiculous, but that was the deal. We used super weird hacks to get it done (like huge negative margins paired with positive padding), which evolved over time to cleaner tricks (like background images that mimicked columns). Techniques that did manage to pull it off referred to it as the holy grail. (Just for extra clarity, usually, holy grail meant a three-column layout with content in the middle, but the main point was equal height columns).

CSS is much more robust now, so we can use it without resorting to hacks to do reasonable things, like accomplish this basic layout.

Here it is in CSS Grid

This grid is set up both with grid-template-columns and grid-template-rows. This way we can be really specific about where we want these major site sections to fall.

I slipped in some extra stuff

  • I had another question come my way the other day about doing 1px lines between grid areas. The trick there is as simple as the parent having a background color and using gap: 1px;, so I’ve done that in the demo above.
  • It’s likely that small screens move down to a single-column layout. I’ve done that at a media query above. Sometimes I use display: block; on the parent, turning off the grid, but here I’ve left grid on and reset the columns and rows. This way, we still get the gap, and we can shuffle things around if needed.
  • Another recent question I was asked about is the subtle “body border” effect you can see in the demo above. I did it about as simple as possible, with a smidge of padding between the body and the grid wrapper. I originally did it between the body and the HTML element, but for full-page grids, I think it’s smarter to use a wrapper div than use the body for the grid. That way, third-party things that inject stuff into the body won’t cause layout weirdness.

The post The Holy Grail Layout with CSS Grid appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Native CSS Masonry Layout In CSS Grid

Rachel Andrew introducing the fact that masonry layout is going to be a thing in native CSS via CSS grid layout. The thing with masonry is that we can already do it for the most part, but there is just one thing that makes it hard: doing the vertical-staggering and having a left-to-right source order. So that’s what this new ability will solve in addition to it just being less hacky in general.

You can already test a partial implementation in Firefox Nightly by enabling layout.css.grid-template-masonry-value.enabled.

.container {   display: grid;   grid-template-columns: repeat(4, 1fr);   grid-template-rows: masonry; }

I like the grid-template-rows: masonry; syntax because I think it clearly communicates: “You aren’t setting these rows. In fact, there aren’t even really rows at all anymore, we’ll take care of that.” Which I guess means there are now rows to inherit in subgrid, which also makes sense.


The post Native CSS Masonry Layout In CSS Grid appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]