From our sponsor: Chromatic - Visual testing for Storybook, Playwright & Cypress. Catch UI bugs before your users do.
Disclaimer: This event is fake, it’s made up. I got the name from a generator, I recommend not going. But if you do, don’t you dare @ me! 😉
In this article, I’ll show you how to create an accessible and responsive layout using semantic HTML and modern CSS. As a bonus, we’ll add some spice with a little bit of JavaScript to make the design feel more alive. We’ll be covering the following:
- Accessible and semantic HTML
- Responsive design
- Flexbox
- CSS Grid
- Clip-path
As a bonus, we’ll look at how to bring the layout to live by adding a subtle parallax scroll effect.
The inspiration for the design comes from this poster and the cubist-style portraits by Brno Del Zou.
You can skip to the sections that most interest you, or follow along on our journey of building the entire layout. This article is for developers of all experience levels. So, if I’m covering something you already know, you can simply skip ahead, no hard feelings.
Getting Started
For following along visually, have a look at the Figma file here, that lays out the desktop, tablet, and mobile designs.
Note that I’ll be using rems for units instead of pixels. In case a user zooms in, it’ll keep the design and fonts scalable. Our root pixel size is 16px, so the formula for wanting to know how many rems a pixel value would be is to divide the pixel size by 16. So, if we wanted to convert 20px into rems, we’d calculate 20 / 16 = 1.25rem.
First, let’s build out the basic layout that involves a <header>
that contains a <nav>
element and adjacent to the header, we have a <main>
element which houses the heading and main component (the magazine cutout).
Here’s the HTML structure:
<body class="site">
<header class="site__header">
<p>When: <span class="block">May 10-12</span></p>
<p>Where: <span class="block">UCL London</span></p>
<nav class="site__nav">
<ul class="site__nav-list">
<li><a class="site__nav-link" href="/">Tickets</a></li>
<li><a class="site__nav-link" href="/">About</a></li>
<li><a class="site__nav-link" href="/">Contact</a></li>
</ul>
</nav>
</header>
<main class="main">
<div class="container">
<h1 class="heading">
heading here
</h1>
<div>Main Component here</div>
</div>
</main>
</body>
Body
Let’s cover the body element’s styles which have a background radial gradient:
.site {
background: radial-gradient(50% 50% at 50% 50%, rgba(123, 131, 126, 0.9) 0%, rgba(54, 75, 73, 0.9) 100%), #364b49;
color: #FFF;
height: 100%;
min-height: 120vh; // we’re adding an extra 20vh here for scrolling purposes we'll use later
}
This line is the gradient: background: radial-gradient(50% 50% at 50% 50%, rgba(123, 131, 126, 0.9) 0%, rgba(54, 75, 73, 0.9) 100%), #898989;
. If you’re unfamiliar with background gradients, the first argument 50% 50%
indicates the width and height of the inner shape and at 50% 50%
refers to the x and y position of the container. Next, the rgba value is the color with a .1 value of transparency and the color starts from the middle of the circle (0%). The next rgba value indicates the last color which starts at the very end (100%). Finally, the last value of #364b49
is the background color that shows beneath the gradient since we’re using a little bit of the transparency in the alpha channel for the two gradient values. And just like that, we have an ambient radial-gradient! Neat!
Next, we place a min-height on the body to span at least 120% of the viewport. This allows the gradient to cover the entire screen, but don’t stare too closely at it… it can read your thoughts.
Navigation
Next, let’s cover the <nav>
and its styles:
<header class="site__header">
<p>When: <span class="block">May 10-12</span></p>
<p>Where: <span class="block">UCL London</span></p>
<nav class="site__nav">
<ul class="site__nav-list">
<li><a class="site__nav-link" href="/tickets">Tickets</a></li>
<li><a class="site__nav-link" href="/about">About</a></li>
<li><a class="site__nav-link" href="/contact">Contact</a</li>
</ul>
</nav>
</header>
We’re using the <header>
element here since it contains a group of information about the site which makes up important conference details and the navigation to all of the links. In the case that a screen reader is reading it, it’s concise and accessible to the user.
Ok, let’s talk about the styles now. What’s required of this component is the following:
- Spread elements across the height of the viewport
- Align items at their top
- Fix it to the window
- Display the text on its side
CSS that requires equal spacing and can align at the top or center is a perfect use case for Flexbox. We don’t have to do too much in order to get that.
For the parent element .site__header
and the .site__nav-list
, we’ll add this flex style:
.site__header,
.site__nav-list {
display: flex;
}
What this does is lay out the direct children of the elements to situate beside each other and align at the top of the elements.
For the direct children of .site__header
, we want them to grow to fill the available space by adding the following:
.site__header > * {
flex: 1 1 auto;
}
The flex
property is a shorthand property for flex children. The three values stand for flex-grow
, flex-shrink
, and flex-basis
. These values indicate to grow to the available space and be able to shrink/get smaller if need be, and auto is the default value which tells the browser to look at the element’s width or height property rather than specifying a particular width value like a percentage.
Finally for the .site__nav-list
, we’ll add justify-content: space-between
so the elements spread out equally among the available space.
.site__nav-list {
justify-content: space-between;
}
Alright, now let’s finish the header by turning it on its side and fixing it to the window!
.site__header {
height: 100%;
padding: 1.25rem 0;
position: fixed;
right: 1.25rem;
top: 0;
writing-mode: vertical-rl;
}
In order for the text to turn 90 degrees, we give the writing-mode
property the value of vertical-rl
. The writing-mode property determines if lines of text are horizontal, vertical, and what direction the blocks should be laid out.
Next, we fix the position of the header which means the element stays at a specific point relative to the window as one scrolls, so the user always sees it and never scrolls away from it. It’s best practice to put at least one Y or X value for fixed and absolute positioned elements. We have our Y value of top: 0
, and the X value of right: 1.25rem
to move it to the top and right of the window. Then we want to have some padding on both ends so the text doesn’t hit the sides of the window by adding `1.25rem` which is equal to 20px.
Note: since we’re dealing with a different writing mode, we have a padding-top and bottom instead of padding-left/right as the element now behaves as a vertical element. And to get the header to span the entire height of the body, we add 100%
to the height property.
See the Pen Magazine Cutout Basic Layout – 1 by Bri Camp Gomez (@brianacamp) on CodePen.
Main Component
What we have so far is a responsive foundation of a fixed navigation and background. Great job for making it all this way, dear reader. Now let’s cover the <h1>
and the grid cutout section.
Our HTML looks as follows:
<main class="main">
<div class="container">
<h1 class="heading">
<mark>2020</mark>
<br />
<mark>Golden Makers</mark>
<br />
<mark>Awards & Ceremony</mark>
</h1>
<div>Magazine cutout</div>
</div>
</main>
For the <main>
element we have the following styles:
.main {
padding: 5rem 0;
display: flex;
justify-content: center; // centers content horizontally
align-items: center; // centers content vertically
min-height: 100vh; // make content at least as tall as the viewport
width: 100%;
}
.container {
position: relative;
}
Heading
If we look at the desktop, tablet, and mobile designs we notice that the heading is on top of the cutout component for desktop and tablet indicating it’s out of the document flow, and on mobile, it’s back in the normal document flow. We’ll implement this via the position
property and a media query. Since we’re going to absolutely position the heading, we need to add position: relative
to its parent element so the heading position value is relative to the .container
vs the window.
To implement this layout we’ll leave it a static positioned element (which means it’s in the normal document flow), and then absolutely position it on screens larger than 40rem (640px) and above. We position it 6rem (92px) from the top of the <main>
element and to be exactly on the left edge as we’ll need that for tablet and mobile screens.
.heading {
font-size: 1.5rem;
text-transform: uppercase;
margin-bottom: 2rem;
@media screen and (min-width: 40rem) {
font-size: 2rem;
left: 0;
position: absolute;
top: 6rem;
z-index: 10; // to be on top of grid
}
}
We also slightly change font sizes to be 1.5rem
on mobile and 2rem
on larger screens:
For the heading, we’re using the <mark>
HTML element for the highlight styles instead of a <span>
since it’s a little more semantic. It’s how we get the background color to show beneath the text.
mark {
color: #FFF;
background-color: #000;
line-height: 1.35;
padding: .375rem;
}
See the Pen Magazine Cutout Basic Layout – 2 by Bri Camp Gomez (@brianacamp) on CodePen.
Magazine Cutout
Now it’s time for the magazine cutout. Since there’s a lot of images overlapping each other, we’re going to use CSS Grid. Wahoo, let’s get started!
Alright, let’s take a look at how we can best implement this via a grid.
This image shows us the grid and clip-path outlines of the images so we can easily see what’s happening here with the different layers. The design allows us to divide the grid into 12 equal columns. Perfect! This image will be our rough guide for where to put each item in the grid.
Let’s set up the starting HTML structure:
<div class="grid-container" aria-hidden="true">
<div class="grid" aria-hidden="true">
<div class="grid__item">
<img src="" alt="">
</div>
</div>
</div>
We have a parent div that’ll contain the grid and its styles with an aria-hidden=“true"
attribute which tells screenreaders to not add this element and its children to the Accessibility Tree or in other words, skip over this element because it’s purely for decoration. If you’d like to learn more about when to use aria-hidden=“true”
or role=“presentation”
, I encourage reading this wonderful article explaining the differences and when to use what.
For the grid styles we’ll add:
.grid-container {
margin: 0 auto; // centers itself horizontally
padding: 0 10%;
max-width: 65rem; // restricts the grid from getting too big
}
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-template-rows: repeat(12, 1fr);
position: relative;
}
In order for the grid to act like a grid, we define the display property as, well, grid
. Next, we want to be explicit about how many columns and rows we want for this grid since we’ll be laying out the images at a particular column and row value.
This line: grid-template-columns: repeat(12, 1fr)
means to make 12 equal columns with the available space of 1fr
. fr
is a flexible unit that indicates the fraction of the available space in the grid. To learn more, I’d recommend reading this article and this article to see different fr
unit use cases.
The same goes for grid-template-rows
; we’ll want 12 equally spaced rows. This allows the images to scale beautifully and keep their positions in the grid once the browser is resized. Lastly, we add position: relative
for the ability to overlap images which we’ll be covering soon.
Let’s look at the assets needed for this:
Since we’re dealing with images, we’ll want them to act as a block-level element and take up the entire space of the container. So we’ll add this to all of our images:
img {
display: block;
width: 100%;
}
Next, we add the .grid__item
children elements with their specific classes and placements. I will write about a couple of them so you can see the thinking behind them.
<div class="grid__item grid__item--bg">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/110238/codrops-portrait.jpg">
</div>
.grid__item--bg {
grid-column: 2 / span 9;
z-index: 0;
grid-row: 1 / -1;
}
For each .grid__item
element, we have 3 very important properties that we’ll use to place the element where we want in the grid and where in the z-stack we want it to reside.
The grid-column
property is a shorthand that takes the grid-column-start
and grid-column-end
property values separated by a “/“. Let’s take grid-column: 2 / span 9
. This rule says to start at the second grid-line and span 9 columns. I recommend using Firefox’s dev tools when you’re working with grid so you can easily see the grid lines. The grid-row
property acts very similarly to grid-column
; it’s a shorthand property that combines grid-row-start
and grid-row-end
. This line grid-row: 1 / -1
says start at grid-row 1, and stretch all the way to the end which is -1. It’s the same as saying grid-row: 1 / span 12
. Last we have the z-index
property to be at the very bottom or background of the grid which is what we get with the value of 0.
Another grid__item
is a half portrait:
<div class="grid__item grid__item--portrait-half">
<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/110238/codrops-portrait-half.jpg" alt="">
</div>
We do very close to what we did with the background but shift it to the right of the grid:
.grid__item--portrait-half {
grid-column: 6 / span 6;
z-index: 1;
grid-row: 1 / -1;
}
We start at grid-line 6 and span 6 columns, make it stretch the entire height of the grid with the grid-row
property, and with a higher z-index than the background, so it sits right on top of the background.
Since there are a total of 10 grid elements I won’t list them all out here but here’s a demo so you can see what I did for each and every one of them:
See the Pen Magazine Cutout – Sans Clip Path – 3 by Bri Camp Gomez (@brianacamp) on CodePen.
Clip-path
Now we want to add the clip-paths to make the cutout shapes appear on the images. Cool cool, but what’s a clip-path?
I’m glad you asked! A clip-path is a clipping area that determines what part of an element can be seen. What’s inside of the area is shown, while what’s outside of the area is hidden. Clip-paths can take several values, but we’re going to use the polygon shape for the most part.
The anatomy of a clip-path property value is this:
clip-path: polygon(x1 y1, x2 y2, x3 y3);
You can add more than 3 x/y values, which we’ll be doing for our images. Since it can be complicated to write out clip-path values by hand, I find it necessary to use clip-path tools that make clip-path shapes. I like to use Clippy and Firefox’s dev tools to create the clip-paths because they both make it incredibly easy to get the exact shapes you want and give you the values for it. So nice!
In order to make this shape:
It consists of these values: the first point value (the white dots in the above photo) indicates 5% from the left and 10% from the top, then the second point is 27% from the left and 3% from the top, and so on and so forth for all of the points.
.grid__item--portrait-half {
clip-path: polygon(5% 10%, 27% 3%, 94% 25%, 84% 98%, 39% 98%, 11% 98%, 4% 66%, 4% 34%);
}
I apply different clip-paths to each element to make each image look cutout and unique. I highly recommend experimenting with the different points, it’s loads of fun!
See the Pen Magazine Cutout – With Clip Path – 4 by Bri Camp Gomez (@brianacamp) on CodePen.
And there you have it, a responsive, accessible layout that employs modern CSS and semantic HTML. You’re probably thinking, cool, but how can we spice this up a bit? In the next section, we’ll make the image’s layers come alive!
Bonus: Interactivity and Animation
To get this spice party started there are a two things we could do:
1. Add some fun little parallax
2. Animate the clip-path on hover with the transition property
I recommend doing one instead of both. As much as I love animation, there’s a fine line between a little spice and completely over the top psychopathic.
We’re going to cover the first option, a little bit of parallax, since the overlapping images call for it, in my opinion! If you wanted to see an example of an animated clip-path, check out the demo in the reference section at the bottom of this article.
Adding animation comes with great responsibility. We need to be mindful of users that have vestibular disorders who might get dizzy when seeing parallax. After we implement the parallax we’ll cover how to remove it if the user has their “Prefers Reduced Motion” Preference turned on via their operating system.
This section will cover a basic implementation of the very small parallax library called rellax.js. We only need one line of JavaScript to make it happen, which is great!
Depending on your project, you can import the library via npm/yarn or add the minified file itself in your project. We’re going to go with the latter by way of their CDN. So, before the end of the closing body tag we’ll add:
<script src="https://cdnjs.cloudflare.com/ajax/libs/rellax/1.10.0/rellax.min.js"></script>
In our JavaScript file, all we need to do to instantiate the Rellax object in the following line:
const rellax = new Rellax(‘.js-rellax');
There are many options you can also pass in via JavaScript but for our purposes, we only need this line. We’ll handle the different scrolling speeds in the HTML.
In order for Rellax to know what elements should be used for parallax we need to add the class js-rellax
to them. I like to prepend js to classes that are only used in JavaScript so it’s easy to tell if it’s tied to JavaScript, i.e. if you remove that class from the HTML, something will likely break!
We’ll add the class to the all of the elements in the .grid
so it’s easy to control what we want. Next, Rellax has a handy data attribute called data-rellax-speed
which handles the scrolling speed of the element. If you don’t specify the speed, it’ll fall back to its default speed of -2
. It’s recommended to use the values of -10
through 10
. -10
is the slowest while 10
is the fastest. In order to add a speed, we add this line to each element with a different value, for example: data-rellax-speed="3"
. I encourage you to play around with different speeds, I find it a ton of fun!
Here’s the final output:
See the Pen Magazine Cutout – With Animation – 5 by Bri Camp Gomez (@brianacamp) on CodePen.
Animations and Accessibility
For users who have vestibular (or inner ear) disorders, where they can get dizzy by seeing animations they can tell their operating systems to reduce motion in their system preferences. Wonderfully, there’s a media query that captures that information and is called prefers-reduced-motion
and takes the values of no-preference
and reduce
. Read more about where the browsers look for various operating systems here: prefers-reduced-motion on MDN
Since we’re not animating anything via CSS and only JS, we’ll detect the media query via JavaScript and kill the parallax animations if the media query is set to reduce
.
To turn off the animations for users who prefer reduced motion we’ll add these two lines of code:
// grabs the media query
const motionMediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
// if there is a prefers-reduced-motion media query set to reduce, destroy animations
if (motionMediaQuery.matches) rellax.destroy();
Read more about the topic here: Move Ya! Or maybe, don’t, if the user prefers-reduced-motion!
See the Pen Final Magazine Cutout – With Accessible Animation – 6 by Bri Camp Gomez (@brianacamp) on CodePen.
If you made it this far, you get 5 gold stars! This was a full tutorial that builds from a Figma file, is responsive, uses modern CSS, semantic HTML, and accessible animations. I hope you enjoyed it!
Really loved! feels like it’s have a basic knowledge with a modern touch. Thanks!
Thank you, Ross! Glad you enjoyed it.
Wouldn’t it be more effective (for load times) to do the old sprite technique where you load a single image then just show areas as needed? This way you don’t have to load 8 images for this single image.
Curious.
Hi Andy,
You’re right having an image sprite would be better for performance since the page would only load one image vs 8. But since this article covered a lot of concepts already, I wanted to keep the scope narrow and to the point for readers. With that being said, adding an image sprite would be a fantastic improvement for a production site! Thanks for your suggestion.