- Introduction
- Final Result
- QR code component
- Variables for QR card
- Container
- QR card
- Product Review Card
- Conclusion
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:
Final Result
QR Card
Product Review
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.
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.
Box-sizing
CSS property to set how the total width and height of an element is calculated.
*{
box-sizing: border-box;
}
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;
}
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;
:{
--lg-fontsize: 34px;
--primary-color: blue;
}
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)
}
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%);
}
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;
}
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>
.container{
height: 100vh;
background-color: var(--cream);
display: flex;
justify-content: center;
align-items: center;
padding: 1em;
}
we only set the height, the div already takes the entire width
height: 100vh;
flex-box
display: flex;
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;
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;
you can set a specific side or sides etc
padding-left: ;
padding-bottom: ;
padding-top: ;
padding-right: ;
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>
QR card styling
.QR_card{
background-color: white;
max-width: 375px;
margin: 2em;
height: max-content;
border-radius: var(--radius);
}
add radius as variable(for rounded corners)
:root{
...
--radius: 1.5em;
}
The QR card will never grow beyond 375px
mobile size value from the style guide.
max-width: 375px;
height of the card will be controlled by the size of the content inside.
height: max-content;
border-radius is for rounded corners.
border-radius: var(--radius);
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);
}
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;
}
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>
Using the font
body {
...
font-family: "Outfit";
}
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)
}
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>
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;
}
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>
unsemantic HTML
<div>
<div>
<span></span>
<span></span>
</div>
<div>
<span></span>
<span></span>
</div>
<div>
</div>
</div>
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>
Styling
.product_card{
max-width: 375px;
height: fit-content;
background-color: var(--white);
border-radius: 1.2em;
overflow: hidden;
}
.product_img {
height: 10em;
}
anything inside .product_card
that overflows or is bigger than 375px will be hidden
overflow: hidden;
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;
}
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)
}
the following is a short hand for manipulating multiple sides at once, works for both margin and padding
margin: .3em 0;
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);
}
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";
}
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";
}
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);
}
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);
}
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;
}
}
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;
}
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%;
}
}
.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;
}
}
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>
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;
}
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);
}
Thank you card
.hero_img{
display: grid;
place-content: center;
gap: 1em;
padding-bottom: 1em;
}
.details{
text-align: center;
}
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;
}
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>
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)=> {
}
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)){
}
}
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")
}
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")
}
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")
the selected class
.selected {
background-color: var(--orange);
}
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")
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")
})
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.
Thank you for reading.
Top comments (0)