DEV Community

Stephanie Eckles
Stephanie Eckles

Posted on

Style a Card Layout with CSS

In this post, we will cover:

  • consolidating global styles
  • the :first-child pseudo class*
  • display: inline-block
  • introduction to CSS grid
  • how to define a dynamically resized image

This is the fourteenth post and video in a series on learning web development. Learn more about the series and see the post schedule >

You may want to refer back to the first lesson that created our initial card HTML, episode 6:

You may watch the following video or follow along with the expanded transcript that follows.

Review the source code >

To begin this lesson, open the starter project you began in episode 1, or review the source files from episode 13.

To begin this lesson, open our project in VSCode.

In the Terminal, type the start command npm run start to run the project.

Open card-layout.html in VSCode, and organize your screen to be split with the browser. Then update the browser url to include /card-layout.html.

window split with VSCode and browser showing card-layout.html

Similar to the last lesson, the first thing we need to do is create a dedicated stylesheet for this page's styles since we do not wish to inherit styles from style.css which is what is giving us the text colors.

Create card-layout.css, and then in our HTML file, update the stylesheet link in the <head> from style.css to card-layout.css and save.

<link rel="stylesheet" href="card-layout.css" />
Enter fullscreen mode Exit fullscreen mode

layout appearance with blank stylesheet

We're nearly back to default browser styles, except we cheated a bit in the HTML lesson and added a handful of styles directly in the HTML. That technique is called "inline styles".

Because inline styles are closest to the element, they are calculated as having a higher specificity in the CSS cascade than element or class selectors. There are ways to override this, but it's an additional rule of the cascade to be aware of.

Refer back to episode 11 if needed to learn more about the CSS cascade.

Let's work on moving the inline styles into our stylesheet.

Looking at the layout we created, it seems reasonable to create two classes: .card-row and .card. We'll move the styles on the <section> element into .card-row and the styles from the <article> elements to .card.

    .card-row {
      display: flex;
      justify-content: space-between;

    .card {
      width: 320px;
      padding: 20px;
      border: 1px solid #c9c9c9;
Enter fullscreen mode Exit fullscreen mode

Then we'll replace the style attribute with the class attribute and the appropriate class:

    <section class="card-row">

    <article class="card">
Enter fullscreen mode Exit fullscreen mode

Be sure to save both the CSS and HTML files and you can see our appearance is unchanged from what we created in the HTML lesson.

For now, let's remove the second and third card so we can focus on the typography and .card styles.

Starting with typography, it would be nice to use what we already defined for the blog page, if we pretend that the blog and product layouts are pages of the same site.

So let's create one more stylesheet - global.css. Then open blog-layout.css and let's move from the body rule through the p rule to our new stylesheet. Then also get from the .container rule through to the body > footer rule.

You can view the final global.css stylesheet in the project repo.

Now, in both blog-layout.html and card-layout.html, we need to add a new stylesheet link, and it needs to be placed above the existing stylesheet link for the cascade to work optimally.

    <link rel="stylesheet" href="global.css" />
    <!-- existing page stylesheet link -->
Enter fullscreen mode Exit fullscreen mode

Following that, we need to update the markup of our <header> and <footer> to match what we did for the blog page, as you can see the spacing is not correct since they are missing the div with the container class.

  <div class="container"> 
  <!-- header content -->

<!-- main -->

  <div class="container"> 
  <!-- footer content -->
Enter fullscreen mode Exit fullscreen mode

Then, we'll add the container class to the main element and our layout is looking much better already.

<main class="container">
Enter fullscreen mode Exit fullscreen mode

layout with container classes added

We can now turn our attention to the .card and first, we'll update the card styles with some other properties we've already learned about:

    .card {
      /* previously added styles */
      border-radius: 7px;
      box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15);
Enter fullscreen mode Exit fullscreen mode

Save, and the card feels more on-trend.

card with additional styles

Next, we'll create styles for the image. If we assume that every time a .card is in use it has one img placed at the top, then we can use the selector .card img to define that the rule will apply to images only within .card.

However, to ensure it only ever applies to the first image, we'll use the direct child selector > and what's called a "pseudo class" to make our full selector .card > img:first-child. The full meaning of this selector is now "apply these properties only to images that are a direct child of the card and are the first-child element within the card."

We'll start with one definition for border-radius which will round the top two corners but not the bottom two.

    .card > img:first-child {
      border-radius: 7px 7px 0 0;
Enter fullscreen mode Exit fullscreen mode

card image with border radius

Then in the HTML let's move the image below the h3 and save.

card image placed below the h3

You'll see that when the image no longer meets the :first-child pseudo class condition, it does not have rounded corners.

Move the image back to the top, and we'll add one more rule for margin-bottom: 20px.

Let's jazz up the card typography, and again use the .card qualifier prior to the typography element tag name:

    .card h3 {
      color: rebeccapurple;

    .card p {
      color: #757575; /* Lightest grey with 4.5:1 contrast over white */
      line-height: 1.5; /* Space between lines of the paragraph */
Enter fullscreen mode Exit fullscreen mode

card typography styles

Finally, we've arrived at the all-important "Add to Cart" link. It's common on e-commerce sites for this to have a more "button" appearance to help it stand out. We may want to have a button appearance on more than just card links, so let's create a rule for .button

    .button {
      /* Reset text link default */
      text-decoration: none;

      background-color: rebeccapurple;
      color: #fff; /* white */
      padding: 0.5em 1em; /* vertical | horizontal */
      border-radius: 4px;
Enter fullscreen mode Exit fullscreen mode

Those are all properties we're familiar with, so let's save. But before we see the results, we need to add the button class to the <a> tag in the HTML, then save.

link with button class styles applied

It looks pretty good, except it seems to be overlapping the bottom spacing of the card. Let's inspect and see what's happening.

inspector highlighting the padding on card

If we inspect the .card, we see that the padding seems to go into the button and our button text is resting there. This is because links are inline elements, and only their horizontal padding is added into the box model algorithm in terms of changing their position in relation to other elements.

Using inspector, we can change the display property to block which we learned is the complement to inline.

button display changed to block

But that's not quite right - we don't want the button to take up the fullwidth, although it did remove the overlapping behavior.

Instead, a third available value for display is inline-block which allows the element width to behave using the inline algorithm, but also allows it to have both it's vertical margin and padding used in the display algorithm as well.

demo of toggling display: inline-block

Toggling this on and off, you can see it also increases the space above the button due to it now being pushed down by the paragraphs bottom margin.

Let's add the display: inline-block to our rule, and save.

We're almost done, but it would be nice for the price to stand out a bit more, so we'll create a rule with the selector .card h3 em, and let's place it following our .card h3 rule for the sake of organization.

    .card h3 em {
      padding: 0.25em;
      /* pale purple used for blog blockquote */
      background-color: #eddbff; 
      border-radius: 4px;
Enter fullscreen mode Exit fullscreen mode

Great, it stands out more, but what happens if we have a longer product title, say "Whizzbang Widget SuperDeluxe"? Let's update the HTML and find out.

card with longer title

Oops! The price get's knocked down to the next line, and since it's an inline element, it's overlapping the text.

A newer display value is grid and this will let us define virtual "columns" and "rows" for our content to live in. If we define display: grid on the .card h3 then a grid cell is created for the text and one for the em tag.

Save, and you'll see the price is still below the title but it's now appearing as a block due to the default grid algorithm.

If we inspect the h3 the inspector has added dashed lines to visualize each grid cell.

inspector visualizing grid lines

What we want are two columns where the price takes the width it needs but always stays to the far right of the title. We can accomplish this by adding grid-template-columns: 1fr auto. Each width designation with this property defines a separate column, so with two definitions, we've created two columns.

The fr keyword means "fraction" and is unique to grid and using it requests the browser to compute the available "fraction" of space that is left to distribute to that column or row. In this case, it will take up the remaining space not needed for the price to fit. Save, and that's almost what we want.

card h3 with columns defined

The final property we will add to the h3 rule is align-items: start which defines how the grid cell content should horizontally align in relation to each other. Save, and you'll see the em is now aligned to the top relative to the product title.

card h3 with align-items: start

This has consequently also fixed its height by not allowing it to stretch the full height of the virtual row.

Now we just need to add back in a couple more cards, so let's copy our card HTML twice to have three cards again, and save.

card layout with three cards

The first apparent thing is that the cards are overflowing the viewable browser area! The reason is the absolute width we used on the .card of 320px. Again, that was for demonstration purposes of our original HTML lesson.

We will remove the width on .card and then change .card-row to use grid for display. Then we'll setup grid-template-columns: 32% 32% 32%. The existing justify-content: space-between rule will distribute any leftover space between each card.

card layout with width removed and grid added

But we still have overflow of the card area... why is that? The reason will not be apparent even from inspecting but is the type of thing to file away in your memory. What is happening is that the browser is able to infer the image display size, and so the image width is keeping the cards wide.

The essential rule to memorize for this scenario is: max-width: 100%; height: auto; which means the image should take up no more than 100% of the available space within its container, and that its height should proportionately resize. We'll add this definition to our rule for card images.

Save, and we now have 3 equal-width, nicely styled cards!

final card layout

We'll learn more tricks for card layouts in our capstone project.

Here's the final result on CodePen:

Note: the demo is best on desktop since we haven't yet covered responsive design - but that's the topic for Episode 16!

Next up in Episode 15: Browser Compatibility

Top comments (0)