Skip to main content Skip to navigation
All Posts All Tags

How to plan your HTML (1): Product Preview Card

Part 1 of a series of posts teaching you how to break down a static design into the most suitable HTML structure.

Series Intro

I have been mentoring junior developers for years. In that time, one issue comes up again and again. Courses for aspiring developers teach almost nothing about the importance of HTML structure!

So it's not your fault if you struggle with this! Bootcamps and tutorials skip over HTML like it's unimportant. But actually it's the most important skill of all.

HTML is like the foundations of a building. If you get that wrong, issues are inevitable.

That's why I'm starting this series of posts. Each post will go step-by-step through the content of a Frontend Mentor design and write the HTML. I'm not saying my way is the only way to do a challenge. The goal is to help you build up your own 'semantic senses'. We'll learn the thinking process behind choosing elements together. Over time I hope you'll find it easier to write appropriate HTML, even if it differs a bit from mine.

I hope you find this helpful!


The design

This post is going to look at a common design pattern: The Product Preview Card Component Challenge.

Mobile design: A vertcially stacked card with an image of a perfume bottle at the top, then a white background with ribbon text saying 'perfume', a title, paragraph and button
Caption: Small screen design
Large screen design: A horizontally stacked card with an image of a perfume bottle on the side, then a white background with ribbon text saying 'perfume', a title, paragraph and button
Caption: Large screen design

It's rare we should ever need to duplicate HTML, meaning it will be the same for all screen sizes.For the rest of this post, I'll be annotating the mobile/small screen design in images. Just be aware that the notes apply everywhere, because there would only be ONE set of HTML.

Also note that in card examples, I'll be adding BEM-style classes (Read 'BEM 101' on CSS Tricks if you're curious). You don't have to do this and I don't always do this. But it should make it easier for you to see at a glance which bit of content each element relates to.

Landmarks

Landmarks are elements are: header, main, aside and footer. All page content should sit within landmarks.

What about Article and Section?

You may also be aware of article and section, which are often referred to as "landmarks". But they are a bit different, and not page-level landmarks in my opinion.

  1. These almost always sit inside the main landmark
  2. For the accessibility API to recognise them as landmarks, they need to have an accessible name. That means having a heading and using aria-labelledby or using aria-label. Without that, they are treated just like divs.

Section in particular, is often misunderstood! I'll post about these elements another time. For now, I prefer to exclude them when thinking about the 'top level' landmarks on a page.

This is a single component challenge, so we only need a main landmark to wrap the design. You may also need a footer landmark below this for attribution back to the FrontEnd Mentor site. (Attribution is not shown on the design above, but the content is provided when you download the starter files.)

👉🏼 General rule: Every webpage should at least have a main.

The Card

This is a chunk of related content. As such, it would be completely valid to make the card a section or article if you wanted to. But, I think that's unnecessary.

Let's think about how this component would be used on a real site... I envisage a scenario where there would be loads of these cards on one page. In fact, that's very likely for this kind of content.

Cards like this are often displayed in a grid or row of several. The HTML for that is usually a list, so that screenreaders can announce how many product cards there are. For example:

<ul class="product-card-grid">
<li class="product-card-grid__item">
<!-- Product Card goes here -->
</li>
<li class="product-card-grid__item">
<!-- Product Card goes here -->
</li>
<li class="product-card-grid__item">
<!-- Product Card goes here -->
</li>
<li class="product-card-grid__item">
<!-- Product Card goes here -->
</li>
</ul>

Some may want to make the product card an article (or a labelled section). That's fine to do if you want to! But doing that inside each of the list items creates a load of extra 'semantic noise'. I think that's unnecessary. The headings inside each card already give the content plenty of meaningful structure.

Hence, I'll make the component a plain old div!

<div class="c-product-card">
<!-- Content here -->
</div>

If you'd still rather use article or section, the HTML would need to look like this, with an accessible name.

<section class="c-product-card"
aria-labelledby="cardTitle-087633-282652-2267757862-67262">

<!-- Content here -->
</section>

Inside the card we need to set up two containers, one for the image part and one for the text part. Here's where my HTML markup ends up so far:

<main>
<div class="c-product-card">
<div class="c-product-card__img-half"></div>
<div class="c-product-card__text-half"></div>
</div>
</main>

Product Image

You can see in the design and starter files, this component has different images for small and large screens.

Perfume bottle in the center of a small landscape-orientation picture
Caption: Small screen product image
Perfume bottle in the center of a larger portrait-orientation picture
Caption: Larger sceen product image

🤔 You may be tempted to use background images here, but that's a bad idea because:

  1. This is a meaningful image. Product images should always have a proper description in the alt attribute
  2. Background images are usually less performant. They involve a call to the server for the CSS, then another call to fetch the image
  3. You'll have to write extra CSS to show/hide each image

🤔 Or, you may consider using two image elements, with classes on each to show/hide them in a CSS media query. That's a bad idea too.

  1. It's bad for performance. Because the browser will download both images no matter what size screen you are using to view the site.
  2. You'll have to write extra CSS to show/hide each image

🤔 So what is the solution...?

This is the perfect time to use the <picture> element!

Check out the picture element on MDN if you're not already familiar with it. It is one of my favourite elements and is great for performance. (Yes, I have favourite elements!) Only the image you need for your screen size/resolution gets downloaded. And this happens all through pure HTML. So simple.

Notes on the picture element

Just as we should style mobile first, I recommend using the picture element 'mobile first' too.

In other words, put the image for the smallest screen the src in the img element. Then put images for larger screens in the srcset of the source element(s).

Also note, if you ever need more than one source element inside a picture, they will need to be in the correct order. For example:

<picture>
<!-- largest image -->
<source srcset="/media/images/decorative-image-3.jpg"
media="(min-width: 60rem)">

<!-- medium image -->
<source srcset="/media/images/decorative-image-2.jpg"
media="(min-width: 40rem)">

<!-- smallest image (default) -->
<img src="/media/images/decorative-image-1.jpg" alt="">
</picture>

So now our HTML should look like something like this (... denotes where content needs adding):

<main>
<div class="c-product-card">
<div class="c-product-card__img-half">
<picture>
<source srcset="..." media="...">
<img src="..." alt="..."
class="c-product-card__img" />

</picture>
</div>
<div class="c-product-card__text-half"></div>
</div>
</main>

Card Title

The ribbon text that says 'perfume' in capitals, followed by larger title text that says the name of the perfume

Which do you think is the main title of the card?

The word "Perfume" is in a small, pale font with uppercase text and increased letter-spacing. This is usually called "Ribbon text" in the design world.

This doesn't make sense as a heading. Headings must always go in a sequential order (I'll write more about that separately). That means this little category would need to have a higher heading level than the product name below it. Again, imagine a load of these product cards on a page. It wouldn't make sense to have as a content structure:

h2: "Perfume"
h2: "Perfume"
h2: "Perfume"...

The product name is bigger, bolder, darker and makes much more sense as a unique heading!

Some may argue that in this case it would be better to keep them both as headings. Or place them together in one heading, using spans to style each line. Or change the DOM order so screenreaders announce "Perfume" after the heading.

None of that is necessary. The ribbon text is fine as a paragraph and the product name a heading.

What heading level to choose?

If this was a real project, we would have more context for this design. We would know the surrounding page content and have a better idea of where this component sits within the content hierarchy. That would guide what heading level we choose.

But we don't have that in this case. All we have is a basic design. My default is to opt for the highest heading level you think this component could have.

There's always going to be a h1 on the page. So I'd give this card a h2 for now. I can always change it to a h3 later if needed.

Here's where the markup is so far:

<main>
<div class="c-product-card">
<div class="c-product-card__img-half">
<picture>...</picture>
</div>
<div class="c-product-card__text-half">
<p class="c-product-card__ribbon ribbon-text">Perfume</p>
<h2 class="c-product-card__title">Gabrielle Essencce Eau De Parfum</h2>
</div>
</div>
</main>

It kind of bothers me that this ribbon text — which does look sort of 'titley' — is only a paragraph. There is one more thing we can do to improve it's importance when looking at the code... add a hgroup wrapper like this:

<div class="c-product-card__text-half">
<hgroup class="c-product-card__title-group">
<p class="c-product-card__ribbon ribbon-text">Perfume</p>
<h2 class="c-product-card__title">Gabrielle Essencce Eau De Parfum</h2>
</hgroup>
</div>

From my experience, having a hgroup doesn't affect the accessibility of this in any way. But this is the perfect use case for this element. It's what it exists to do. And it does group the titles together nicely when reading the code. It's up to you if you want to use it.

Product description

This is a paragraph. No discussion needed!

Prices

There's another little accessibility challenge in this, because the design includes two prices. One is large and bold. The other is small and has a line through it, like it's crossed out.

Think about what happens if you can't see that though... Screenreader software would read out both prices! Pretty confusing, don't you think?

The good news is there is some HTML designed for this situation. The bad news is we still have to do some extra work to make sure screenreader users know which is the old price.

Here's my initial HTML:

<p class="c-product-card__prices">
<span>$149.99</span>
<!-- The <del> element would also be a good choice instead of <s> -->
<s>$169.99</s>
</p>

As you can see, I've opted for the <s> element to wrap the old price. That means "strike-through", which describes the style in the design. You could use the <del> element instead, which means "deletion". That makes sense too as the old price has kind of been 'deleted'.

To make sure that screenreader users know which is the old price, you can tackle it in two ways:

  1. In the HTML with a class to accessibly hide some label text (commonly known as sr-only or visually-hidden).
  2. In the CSS with a a pseudo element and the same CSS snippet to accessibly hide the label

Number 1 (the HTML-only approach) is simpler for a one-off component, but not so reusable. It looks like this:

<s><span class="visually-hidden">Old price: </span>$169.99</s>

That's fine to use! But I like to know that any time I need to use strike-through text like this it will be accessible. The same with deletion or highlighted text. That's why I have this in my CSS (scss):

mark,
del,
s
{
position: relative;

&::before,
&::after
{
@extend .visually-hidden;
}
}

s::before {
content: ' [strike-through start] ';
}
s::after {
content: ' [strike-through end] ';
}

For more detail on this pseudo-content technique, I recommend Adrian Roselli's excellent article on tweaking text level styles.

With all that done, my HTML will now look like this:

<main>
<div class="c-product-card">
<div class="c-product-card__img-half">
<picture>...</picture>
</div>
<div class="c-product-card__text-half">
<hgroup class="c-product-card__title-group">
<p class="c-product-card__ribbon ribbon-text">...</p>
<h2 class="c-product-card__title">...</h2>
</hgroup>
<p class="c-product-card__desc">...</p>
<p class="c-product-card__prices">
<span class="c-product-card__price">$149.99</span>
<!-- CSS handles <s> accessibility! -->
<s>$169.99</s>
</p>
</div>
</div>
</main>

Note I'm using a span to wrap the price purely for positioning and styling reasons.

Call-to-action (CTA)

A common question in front end developmemt is when to use an anchor (link) and when to use a button.

Clicking 'Add to cart' performs an action. It is unlikely to navigate the user anywhere, so it makes sense to use a button here.

Inside the button there is a little icon as well as the text. That text label in this button already says what clicking it will do. That means we know the icon is decorative and won't need an alt description. For decorative images, you must leave the alt intentionally blank. (It's counterintuitive, I know!)

Here's what we end up with:

<button type="button" class="c-product-card__btn btn btn--primary">
<img src="..." alt="">
<span>Add to cart</span>
</button>
Why wrap words in a span?

The only reason I've wrapped that text in a span is because I know it will help me position it using flexbox!

.c-product-card__btn {
display: flex;
align-items: center;
gap: 1ch;
}

When you're first starting out, you may leave out elements like this until you come to do the styling. That's often when I'll add extra spans or divs, as they are meaningless elements. They only exist to assist with layout/styling, and don't add any semantic meaning.

Repeating CTAs can be a problem!

If there was a page full of these product cards, you'd have a situation where there are multiple "add to cart" buttons on the page. Imagine being confronted with a button menu like this:

Add to cart
Add to cart
Add to cart
Add to cart

That could get pretty confusing to screenreader users. (And to those using screen magnification or zoom etc)

The best solution is to avoid repeating CTA labels in designs altogether! Designers and content editors are becoming more disability-aware and hopefully will stop repeating links like this in future. But we can help a bit via our code right now...

Using aria-describedby or aria-labelledby on CTAs with repeating labels can drastically help screenreader users! This makes "Add to cart" become something like: "Add to cart - Gabrielle Essencce Eau De Parfum".

I'll do a deeper dive on this technique another time. But feel free to do your own research and start using it now!

Summing up

I hope this has helped you understand my process for working out the right semantics for a design.

As we've learnt, there are occasional little 'accessibility extras' we need to add in to our HTML. But these are pretty rare! It's OK if you don't understand all of them yet. As long as you are trying to choose appropriate elements and building your accessibility awareness you are on the right track!

My end HTML looks like this (most class names and content removed):

<main>

<!-- Product Card -->
<div class="c-product-card">

<!-- Image half of the card-->
<div class="c-product-card__image-half">
<!-- images -->
<picture>
<source ...>
<img src="..." alt="...">
</picture>
</div>

<!-- Text half of the card-->
<div class="c-product-card__text-half">
<hgroup>
<!-- Ribbon Text -->
<p>...</p>
<!-- Product name -->
<h2>...</h2>
</hgroup>

<!-- Product Description -->
<p>
...
</p>

<!-- Prices -->
<p>
<!-- Current Price -->
<span></span>
<!-- Old Price -->
<s></s>
</p>

<!-- CTA -->
<button type="button">
<img src="..." alt="">
<span></span>
</button>
</div>

</div>
</main>

It's fine if yours looks a little different! As long as it's properly thought through.

The true test of whether you've chosen appropriate HTML is to view your content without any styling at all. It should be super clear and understandable.

The unstyled HTML with appropriate elements: An image, paragraph, heading, paragraph, two prices, one with a line through, a button
Caption: My final HTML

And the final tests for whether it is accessible involve screenreader and keyboard testing. (Including testing these when this component is placed in its real context on a site!)