DEV Community

Cover image for Ultimate CSS guide: Build 3 card components.
sk
sk

Posted on • Updated on

Ultimate CSS guide: Build 3 card components.

Introduction

As powerful as CSS is as complex, learning CSS can be challenging, but I aim to make it easy with project-based learning, with this and subsequent tutorials. We are going to build three card components in order of complexity, and will explain everything as we go along.

You can find the complete code on git; if you want the starter files only, you can download them on frontend mentor:

Product preview

QR Component

Interactive Rating Component

Final Result

QR Card

QR Card

Product Review

Product review

Rating Card

Rating Card

QR code component

Project setup:

Extract the files from frontend mentor and copy the image folder and index.html to your created folder.

Create and link style.css file in your HTML.

A split setup in vs code is ideal for this tutorial, and I place the style guide and design on the right and the code on the left.

Vs code setup

you can split your vscode by right-clicking on a file and click
the open to the side option.

if you see... in the code, it means continuation from the previous code.

Lastly, I follow mobile-driven development. For every project, we start with the mobile design file first.

padding vs margin

Padding is the space between the element's border and the content inside, while margin is the space around the element's border.

margin vs padding

Box-sizing

CSS property to set how the total width and height of an element is calculated.

*{
  box-sizing: border-box;
}
Enter fullscreen mode Exit fullscreen mode

border-box accounts for padding and margin as part of the element size, a 100px sized element with a border-box includes the margin and padding in it, w/o border-box padding, and the margin are added on top of the size, the element will look bigger than expected.

Reset body padding and margin

Some HTML elements have margin and padding by default, and you reset by making both or either 0.

body {
  padding: 0;
  margin: 0;
}
Enter fullscreen mode Exit fullscreen mode

In this case, we do not need space around the body element.

variables

a custom CSS property that allows the reuse of values.

instead of typing the same color over and over again, we can use a variable,

Syntax

 --myvar : property;
Enter fullscreen mode Exit fullscreen mode
:{

  --lg-fontsize: 34px;
  --primary-color: blue;
}
Enter fullscreen mode Exit fullscreen mode

they start with --, you can camelcase: --lgFontsize, separate: --lg-fontsize as long as your variable starts with --.

Using variables

.myClass { 
 font-size: var(--lg-fontsize); 
 color: var(--primary-color)

}
Enter fullscreen mode Exit fullscreen mode

Variables for QR card

:root{} - root selector, select the root of the document, <html>

:root{
     --light-gray:hsl(212, 45%, 89%);
    --grayish-blue:hsl(220, 15%, 55%); 
    --dark-blue: hsl(218, 44%, 22%);

}
Enter fullscreen mode Exit fullscreen mode

Container

The QR card is centered in a dark container that takes the entire screen(viewport).

VH and VW CSS units are relative to the visible screen size or viewport, for example:

.Vexample{
   width: 100vw;
   height: 100vh;
}
Enter fullscreen mode Exit fullscreen mode

100 vh - the elements take 100% of the viewport height-wise, while 200 takes twice as much, 50 half, and so forth, all relative to the screen.

vw - view width.

vh - view height

container styles

  <div class="container">
       <!-- QR card here -->
 </div>
Enter fullscreen mode Exit fullscreen mode
.container{
    height: 100vh;
    background-color: var(--cream);
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 1em;


}
Enter fullscreen mode Exit fullscreen mode

we only set the height, the div already takes the entire width

  height: 100vh;
Enter fullscreen mode Exit fullscreen mode

flex-box

  display: flex;
Enter fullscreen mode Exit fullscreen mode

is a web layout model, it is one dimensional. You can either place elements as a row or column, flex-box is responsive, a deep dive on web layouts require its own article if you need a refresher or intro MDN has excellent articles.

center the elements

flex-box allows us to position elements with justify-content and align-items

Justify - positions the elements inside the container horizontally,

based on a given instruction e.g:

flex-start - being the start of the container horizontally

center - center of the container

flex-end - end of the container

and many more ways: justify-evenly, space-between.

Align - positions the elements inside the container vertically.

flex will center everything vertically and horizontally with the following.

 justify-content: center;
 align-items: center;
Enter fullscreen mode Exit fullscreen mode

em and rem

em is a CSS unit relative to the font size of the parent element, while rem is relative to the font size of the root element.

the default font size for browsers is 16px. Therefore, 1rem or em is 16px unless you override the font size from the root or parent container.

formula: em or rem * font size = unit

therefore: 1em * 16px = 16px

The space between the container and content all around will be 16px

 padding: 1em;
Enter fullscreen mode Exit fullscreen mode

you can set a specific side or sides etc

   padding-left: ;
   padding-bottom: ;
   padding-top: ;
   padding-right: ;
Enter fullscreen mode Exit fullscreen mode

QR card

max-width and min-width

max suffixed with width or height prevents an element from becoming larger than the value specified, min does the opposite and prevents the element from becoming smaller than the value specified.

  <div class="container">
         <div class="QR_card">
                <img src="./images/image-qr-code.png">

          <section>
            <h1>Improve your frontend skills by building projects</h1>

            <p>Scan the QR code to vist Frontend Mentor and take your coding skills to the next level</p>
          </section>
        </div>
 </div>
Enter fullscreen mode Exit fullscreen mode

QR card styling

.QR_card{
    background-color: white;
    max-width: 375px;
    margin: 2em;
    height: max-content;
    border-radius: var(--radius);

}
Enter fullscreen mode Exit fullscreen mode

add radius as variable(for rounded corners)

:root{
...
 --radius: 1.5em;
}
Enter fullscreen mode Exit fullscreen mode

The QR card will never grow beyond 375px mobile size value from the style guide.

 max-width: 375px;
Enter fullscreen mode Exit fullscreen mode

height of the card will be controlled by the size of the content inside.

 height: max-content;
Enter fullscreen mode Exit fullscreen mode

border-radius is for rounded corners.

 border-radius: var(--radius);
Enter fullscreen mode Exit fullscreen mode

Image Styling

Styling images with CSS can be pretty tricky, the following is basic, and we will build upon it.

.QR_card img {
    height: 40%;
    width: 100%;
    padding: 1em;
    border-radius: var(--radius);
}
Enter fullscreen mode Exit fullscreen mode

The styles are self-explanatory; however, you typically don't want to round edges on an image, but the container, we will explore this later.

Section and Paragraph

.QR_card section{
    text-align: center;
    padding: 1.5em;
}

.QR_card p {
    font-size: 15px;
}
Enter fullscreen mode Exit fullscreen mode

Font Family

The style guide specifies Outfit for font, weight being 300 and 700 you can follow this link to download the font on google fonts; if you already know how to do that, you can link the font with HTML below, in the head tag.

<head>
  ...

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;700&display=swap" rel="stylesheet">

 <link rel="stylesheet" href="style.css">
</head>
Enter fullscreen mode Exit fullscreen mode

Using the font

body {
   ...
  font-family: "Outfit"; 
}
Enter fullscreen mode Exit fullscreen mode

all elements inside the body tag will use Outfit font as the default font,

lastly, we need to update font weights, larger font weights are for headers etc, while smaller ones are for paragraphs and the like.

h1 {
    font-weight: 700;
}

p{
    font-weight: 400;
    color: var(--grayish-blue)
}
Enter fullscreen mode Exit fullscreen mode

the color property set's the color of the text, gray or grayish colors work well with text, that's why I picked grayish-blue.

The QR card component is complete; all subsequent projects and articles will build from the basics learned here, I will only explain new concepts.

Product Review Card

The setup is the same as QR card, extract the relevant files to a new folder, create and link style.css.

Familiar Code.

HTML

Fonts

<head>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

 <!-- Fraunches && Montserrat  -->
 <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,700&family=Montserrat:wght@500;700&display=swap" rel="stylesheet">
<!-- Custom Styles  -->
 <link rel="stylesheet" href="style.css">
</head>

<body>


  <div class="container">

    <div class="product_card">

    </div>
  </div>
</body>
Enter fullscreen mode Exit fullscreen mode

CSS

* {
    box-sizing: border-box;



}

:root{
    --dark-cyan: hsl(158, 36%, 37%);
    --dark-cyan-darker:hsl(158, 36%, 17%);
    --cream: hsl(30, 38%, 92%);


    --very-dark-blue: hsl(212, 21%, 14%);
    --dark-grayish-blue: hsl(228, 12%, 48%);
    --white:  hsl(0, 0%, 100%);
}

body{
    margin: 0;
    padding: 0;


    font-family: "Montserrat";
    font-weight: 500;


}

.container{
    height: 100vh;
    background-color: var(--cream);
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 1em;

}
Enter fullscreen mode Exit fullscreen mode

New Concepts

semantic HTML

HTML elements that clearly describe their role to both the browser and developers, MDN article for more.

example: the following is semantic(meaningful), by reading the HTML you know what's going on and can follow the structure,

    <div>
       <section>
          <h1></h1>
          <p></p>
       </section>

       <section>
          <h2></h2>
          <p></p>
       </section>
       <form>

       </form>

    </div>
Enter fullscreen mode Exit fullscreen mode

unsemantic HTML

  <div>
       <div>
          <span></span>
          <span></span>
       </div>

       <div>
          <span></span>
          <span></span>
       </div>
       <div>

       </div>

    </div>
Enter fullscreen mode Exit fullscreen mode

Writing semantic HTML is vital for a whole lot other reasons than code readability and is important when your projects get larger. writing semantic HTML takes practice and familiarity with many HTML elements. This is a newbie tutorial I will use as little as possible and limit it to a few elements to avoid information overload.

rest of the HTML

  <div class="product_card">
     <section class="product_img">
        <img src="./images/image-product-mobile.jpg" class="hidden-for-desktop"/>

        <img src="./images/image-product-desktop.jpg" class="show-for-desktop hidden-for-mobile"/>

      </section>

      <section class="product_details">

       <small>perfume</small>
       <h2>Gabrielle Essence Eau De Parfum</h2> 

       <p>
          A floral, solar and voluptuous interpretation composed by Olivier Polge, 
          Perfumer-Creator for the House of CHANEL.
       </p>


       <h1>$149.99 <span>$169.99</span></h1>


      <div>
        <a href="#" class="btn">
          <img src="./images/icon-cart.svg"/>
          Add to Cart
        </a>
      </div>


      </section>
  </div>
Enter fullscreen mode Exit fullscreen mode

Styling

.product_card{
    max-width: 375px;
    height: fit-content;
    background-color: var(--white);
    border-radius: 1.2em;
    overflow: hidden;  
}

.product_img {
    height: 10em;

}
Enter fullscreen mode Exit fullscreen mode

anything inside .product_card that overflows or is bigger than 375px will be hidden

overflow: hidden;
Enter fullscreen mode Exit fullscreen mode

If you comment out overflow hidden, you will notice the image overflows, the card is smaller than the image. remember what I said about images and them being unwieldy, it is overflowing outside its parent .product_img. let's fix that by directly targeting the image,

.product_img img{

    width: 100%;
    height: 100%;

    object-fit:cover;
    object-position: center;

}
Enter fullscreen mode Exit fullscreen mode

setting the width and height works perfectly fine, but object-fit and position make sure the image looks good responsively(as screen size changes). it will cover the container, and the zoom or crop will be at the center of the image.

The mobile design is taking shape.

Product details

.product_details{
    padding: 1em;
}

.product_details h2 {
    font-family: "Fraunces";
    font-weight: 700;
    margin: .3em 0;
    color: var(--very-dark-blue)
}
Enter fullscreen mode Exit fullscreen mode

the following is a short hand for manipulating multiple sides at once, works for both margin and padding

   margin: .3em 0;
Enter fullscreen mode Exit fullscreen mode

translation: margin: top and bottom = .3em left and right = 0

.product_details small {
    text-transform: uppercase;
    font-weight: 700;
    color: var(--dark-grayish-blue);
    letter-spacing: .3em;
}

.product_details p {

    font-size:14px;
    color: var(--dark-grayish-blue);
}
Enter fullscreen mode Exit fullscreen mode

There's nothing that is new or special with the above CSS, except maybe letter-spacing, which is the space between letters.

.product_details h1 {
    display: flex;
    color: var(--dark-cyan);
    font-family: "Fraunces";
}
Enter fullscreen mode Exit fullscreen mode

display: flex; for a one-dimensional layout, h1, and span must be in a single row.

.product_details h1 span {
    display: flex;
    align-items: center;
    padding-left: .8em;
    text-decoration-line: line-through;
    font-size: .4em;
    color: var(--dark-grayish-blue);
    font-family: "Montserrat";

}
Enter fullscreen mode Exit fullscreen mode

text-decoration-line: line-through; - strike thru the text.

buttons

Depending on your love or hate relationship with CSS, styling buttons can be either the most annoying or fulfilling task, the following styles are the basis for most button styles. Keep them somewhere accessible for reuse, it does not even matter that much if you don't understand how they entirely work, as long as you know which part to customize for a custom button.


.btn {


    display: inline-block;
    /* outline: 0; */
    /* appearance: none; */
    padding: 12px 12px;
    margin-bottom: .5em;
    border: 0px solid transparent;
    border-radius: 4px;
    text-decoration: none;
    cursor: pointer;
    background-color: var(--dark-cyan);
    /* box-shadow: rgb(19 170 82 / 40%) 0px 2px 3px; */
    color: rgb(255, 255, 255);
    /* font-size: 14px; */
    font-weight: 400;
    width: 100%;

    transition: all 150ms ease-in-out 0s;


}

.btn {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 5px;

}

.btn:hover{
    background-color: var(--dark-cyan-darker);
}
Enter fullscreen mode Exit fullscreen mode

Important for customization.

 background-color: var(--dark-cyan);
   /* text color */
 color: rgb(255, 255, 255);
    /* affect the size of the button */
 padding: 12px 12px;
 margin-bottom: .5em;

  /* roundedness */
 border-radius: 4px;
  /* remove a tag underline*/
 text-decoration: none;

 /* button feedback on user interaction*/
.btn:hover{
    background-color: var(--dark-cyan-darker);
}
Enter fullscreen mode Exit fullscreen mode

The card should be looking pretty good on mobile,

Desktop Design

Responsive development can be complex at times, accounting for multiple screen sizes, devices, and modes can be complicated, It helps to have at least one simple robust approach to responsive development to get up and running quickly. That's the method I am going to teach you.

Simplifying everything to a single break-point reduces the complexity significantly, the idea is to have a point where everything below is considered mobile, and anything above is desktop; this way you don't have to worry about a plethora of devices; 1024px is the magical breakpoint.

.hidden-for-mobile {
    display: none;
}

.show-for-mobile {
    display: block;
}

@media screen and (min-width:1024px){
/*styles inside this block will only apply to screens from 1024px upwards*/
.hidden-for-desktop{
        display: none;

    }

    .show-for-desktop {
        display: block;
    }
}
Enter fullscreen mode Exit fullscreen mode

Currently, the card is a one-dimensional column, everything is stacked, we want it to be a one-dimensional row on desktops, and we know from previous experience we can achieve that with flexbox.

Override the layout of .product_card with a display :flex;

@media screen and (min-width:1024px){
...
display: flex;
/*allowing the card to take more width*/
max-width: 600px;
}
Enter fullscreen mode Exit fullscreen mode

Image and details must be the same width and take the entire height and width of its parent.

@media screen and (min-width:1024px){
  ...
   .product_card > * {
        width: 50%;
    }

    .product_img {
        height: 100%;
    }
}
Enter fullscreen mode Exit fullscreen mode

.product_card > * selects all the children of the product card and applies the width of 50% to each child. finally, fix the details to be a column.



@media screen and (min-width:1024px){
...

   .product_details {
        display: flex;
        flex-direction: column;
        justify-content: center;
    }



    .product_details h2 {
      font-size: 2.3em;
    }
}
Enter fullscreen mode Exit fullscreen mode

if you approach responsive development with this basic mentality and approach until you are comfortable, it's hard to go wrong, I've built multiple projects quickly with this approach, you can trust it is well-tested.

Rating Card

The focus here is not the styles or design, but I am introducing a bit of JavaScript and style manipulation based on a specific state. The styles are actually almost the same as the QR card. I will not explain them that would be tedious and repetitive, the focus is JavaScript.

HTML


  <h2>How did we do?</h2>

  <p>
    Please let us know how we did with your support request. All feedback is appreciated 
    to help us improve our offering!
  </p>


  <section class="rating">
    <span class="icon rate">1</span> 
    <span class="icon rate">2</span> 
    <span class="icon rate">3</span> 
    <span class="icon rate">4</span> 
    <span class="icon rate">5</span> 

  </section>


  <div>
    <a href="#" class="btn">

     Submit
    </a>
  </div>

</div>

<!-- thank you card -->
<div class="card hidden" id="thanks">
  <div class="hero_img">

    <img src="./images/illustration-thank-you.svg"/>
    <span> You selected <label id="rate">5</label>  out of 5</span>
  </div>

 <div class="details">

  <h2>Thank you!</h2>

  <p>
    We appreciate you taking the time to give a rating. If you ever need more support, 
    don’t hesitate to get in touch!
  </p>

 </div>

</div>
Enter fullscreen mode Exit fullscreen mode

Basic common styles

*{
    box-sizing: border-box;
}

:root{
    --orange:  hsl(25, 97%, 53%);
    --white: hsl(0, 0%, 100%);
    --light-grey: hsl(217, 12%, 63%);
    --medium-grey: hsl(216, 12%, 54%);
    --dark-blue: hsl(213, 19%, 18%);
    --dark-blue-lighter: hsl(213, 19%, 22%);
    --very-darkblue: hsl(216, 12%, 8%);
}

body{
    margin: 0; 
    padding: 0;
}

.container{
    display: flex; 
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: var(--very-darkblue);
    padding: 1em;
}
Enter fullscreen mode Exit fullscreen mode

Card


.card {
  display: flex;
  flex-direction: column;
  background-color: var(--dark-blue);
  border-radius: 1.2em;
  max-width: 375px;
  height: fit-content;
  padding: 2em;
  color: var(--white);
}

p {
    color: var(--medium-grey);
    margin-bottom: 0;
}
.icon {
    background-color: var(--dark-blue-lighter);
      /*  circle start */
    border-radius: 50%;
    width: 48px;
    height: 48px;
    /* circle end */px
    width: max-content;
    padding: .7em;
}

.icon img {
    width: 28px;
    height: 28px;
    /* padding: 2em; */

}


.rating{
    display: flex;
    /* justify-content: space-evenly; */
    padding: 1.5em 0;

}

.rate {
    padding: 1em;
    margin-right:1.5em;
    cursor: pointer;
}

.rate:hover{
    background-color: var(--light-grey);
}

.selected {
    background-color: var(--orange);
}
Enter fullscreen mode Exit fullscreen mode

Thank you card

.hero_img{
    display: grid;
    place-content: center;
    gap: 1em;
    padding-bottom: 1em;

}
.details{
    text-align: center;
}
Enter fullscreen mode Exit fullscreen mode

Utilities

.btn {


    display: inline-block;
    /* outline: 0; */
    /* appearance: none; */
    padding: 15px 12px;
    margin-bottom: .5em;
    border: 0px solid transparent;
    border-radius: 20px;
    text-decoration: none;
    cursor: pointer;
    background-color: var(--orange);
    color: rgb(255, 255, 255);
    font-weight: 400;
    width: 100%;

    transition: all 150ms ease-in-out 0s;


}

.btn {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 5px;
   text-transform: uppercase;

}

.btn:hover{
    background-color: var(--white);
    color: var(--orange);
}

.hidden{
    display: none;
}
Enter fullscreen mode Exit fullscreen mode

JavaScript

If you are comfortable with JS, you can copy the code and explore it on your own; else read on as I explain and add a script tag at the end of the body tag.

  <script>
    let rating = 0;
    document.querySelector(".rating").addEventListener("click", (e)=> {
      const allRatings = [1, 2, 3, 4, 5]

      if(allRatings.includes(+e.target.outerText)){

        const childs = document.querySelector(".rating").children
        for(let i = 0; i < childs.length; i++){
         childs.item(i).classList.remove("selected")
        }
        rating = e.target.outerText
        e.target.classList.add("selected")
      }
    })

    document.querySelector(".btn").addEventListener("click", ()=> {

       if(rating === 0) return; 
      document.querySelector("#rating_card").classList.add("hidden")
      document.querySelector("#rate").innerHTML = rating
      document.querySelector("#thanks").classList.remove("hidden")
    })
  </script>
Enter fullscreen mode Exit fullscreen mode

Event bubbling

Events fired from children of an element can bubble and be caught at the parent level; the idea is instead of adding event listeners to a bunch of children and handling them individually, we can add a single event listener to the parent, which will account for all the children when clicked,

instead of adding an event for each rating, we add one to the parent element; when a rating is clicked, the event will bubble.


document.querySelector(".rating").addEventListener("click", (e)=> {
}
Enter fullscreen mode Exit fullscreen mode

this means anything inside .rating, even if it's not the ratings themselves; if clicked, will send an event, so we need a way to tell if the bubbled event is from one of the ratings. the event object e has a target property that represents the element clicked, we can query it and see if it's one of the ratings

document.querySelector(".rating").addEventListener("click", (e)=> {
    const allRatings = [1, 2, 3, 4, 5]
    if(allRatings.includes(+e.target.outerText)){

    }

}
Enter fullscreen mode Exit fullscreen mode

e.target.outerText is the visible text in your browser, text inside the tags 1 in this case. +e.target.outerText - the plus is another way in JS to parse a string to a number, because we are comparing with the allRatings array, which contains numbers. the following code says if the element clicked has an outer text of either 1, 2, 3,4,5 we care about that element, run the if block, else do nothing.

  if(allRatings.includes(+e.target.outerText)){

        const childs = document.querySelector(".rating").children
        for(let i = 0; i < childs.length; i++){
         childs.item(i).classList.remove("selected")
        }
        rating = e.target.outerText
        e.target.classList.add("selected")
      }
Enter fullscreen mode Exit fullscreen mode

when a rating is selected, we need to make sure that any rating that was selected before we remove it as selected; this will happen when the user changes their mind after selecting a rating.

 const childs = document.querySelector(".rating").children
 for(let i = 0; i < childs.length; i++){
      childs.item(i).classList.remove("selected")
    }
Enter fullscreen mode Exit fullscreen mode

we are getting all the children of the .rating element and removing the selected class from them if any has it.

childs.item(i).classList.remove("selected")
Enter fullscreen mode Exit fullscreen mode

the selected class

.selected {
    background-color: var(--orange);
}
Enter fullscreen mode Exit fullscreen mode

after removing any selected from a previous selection, we mark the new element as selected

// for display in the thank you card
rating = e.target.outerText e.target.classList.add("selected")
Enter fullscreen mode Exit fullscreen mode

all that is left is to handle the submit button, on submit we want to transition from the rating card to the thank you, we follow the same process of adding and removing classes with classList and the elements will be updated accordingly.

    document.querySelector(".btn").addEventListener("click", ()=> {
      // if no rating is selected we don't want to run the code below we return.
       if(rating === 0) return; 
    // hiding the rating card
      document.querySelector("#rating_card").classList.add("hidden")
      // setting the  rate for the thank you card 
      document.querySelector("#rate").innerHTML = rating
     // showing the thank you
      document.querySelector("#thanks").classList.remove("hidden")
    })
Enter fullscreen mode Exit fullscreen mode

as a challenge, you can implement the back button for the user to go back and change their rating, while seeing their old rating; that could be a great challenge.

Conclusion

This article is the beginning of multiple HTML, CSS, JavaScript, React, and Svelte articles, as I am taking this blog to that new direction of project-based learning.
In the meantime, if you want to take your HTML, CSS, and JavaScript skills from beginner to writing large applications in a focused path, I have a dedicated digital download with 1v1 help, w/o 1v1 help

If you like the blog and like to support you can buy me a coffee, your support would be highly appreciated.

Buy Me A Coffee

Thank you for reading.

Top comments (0)