Hello developers, I've created a TODO app only using frontend technologies (HTML, CSS and JS). It is a challenge from the website called Frontend Mentor.
If you want to look at my solution, Here is my live site URL and Github Repository.
Here, In this blog, I'm going to share with you how I did this.
Design
Here is the design file,
Boilerplate
First thing we should do is set up our project with HTML Boilerplate.
Here's mine,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="author" content="Your Name" />
<title>Frontend Mentor | TODO APP</title>
<meta
name="description"
content="This is a front-end coding challenge - TODO APP"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="./assets/images/favicon-32x32.png"
/>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@400;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="./css/styles.css" />
</head>
<body>
</body>
</html>
Set up colors and fonts
Next, we set up our colors, fonts that we're going to use using css custom properties.
:root {
--ff-sans: "Josefin Sans", sans-serif;
--base-font: 1.6rem;
--fw-normal: 400;
--fw-bold: 700;
--img-bg: url("../assets/images/bg-desktop-dark.jpg");
--clr-primary: hsl(0, 0%, 98%);
--clr-white: hsl(0, 0%, 100%);
--clr-page-bg: hsl(235, 21%, 11%);
--clr-card-bg: hsl(235, 24%, 19%);
--clr-blue: hsl(220, 98%, 61%);
--clr-green: hsl(192, 100%, 67%);
--clr-pink: hsl(280, 87%, 65%);
--clr-gb-1: hsl(236, 33%, 92%);
--clr-gb-2: hsl(234, 39%, 75%);
--clr-gb-3: hsl(234, 11%, 52%);
--clr-gb-4: hsl(237, 12%, 36%);
--clr-gb-5: hsl(233, 14%, 35%);
--clr-gb-6: hsl(235, 19%, 24%);
--clr-box-shadow: hsl(0, 0%, 0%, 0.1);
}
Custom properties in CSS are like variables. Variable name (Identifier) should be prefixed with --
We can use these variables defined here later in our code using var() function.
So, var(--fw-normal) returns 400.
Get rid of default css - Using css resets
Every browser has a default style sheet called User Agent Stylesheet from which we get some styles for our headings, paragraphs and other elements.
But, It's better to start from scratch. So, Our css resets will be,
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-size: 62.5%;
position: relative;
}
html,
body {
min-height: 100%;
}
ul {
list-style: none;
}
img {
user-select: none;
}
In the above code block,
- We're setting
margin,paddingfor all elements to be0. - Our
box-sizingwill beborder-boxwhich basically lets us get rid of overflow error. - We're setting our base
font-sizeto62.5%i.e.10pxwhich makes ourremcalculation easier.
1rem = 1 * base-font-size (base-font-size is 16px by default)
= 1 * 16px
= 16px
We're setting base to 10px. So,
1rem = 10px
1.5rem = 15px
2.5rem = 25px
4.6rem = 46px
[Calculation is super easy here]
- Our page's height will be minimum 100%.
- We're disabling bullets for unordered list.
- We're using
user-select: noneto prevent user from selecting images i.e. when user pressesCtrl + A
Background
When we look the above design, first thing we can see clearly is backgrounds.
Yes! we need to add background-image and background-color.
body {
font: var(--fw-normal) var(--base-font) var(--ff-sans);
background: var(--clr-page-bg) var(--img-bg) no-repeat 0% 0% / 100vw 30rem;
padding-top: 8rem;
width: min(85%, 54rem);
margin: auto;
}
Here, In this code block,
-
font-
fontis a shorthand property for<font-weight> <font-size> <font-family> - So, our
fontwill be400 1.6rem "Josefin Sans", sans-serif.
-
-
background-
backgroundis a shorthand property for<background-color> <background-image> <background-repeat> <background-position> / <background-size>. -
background-colorandbackground-imagedefines color and image. -
background-repeatdefines whether the background image needs to be repeated or not. In our case, not, sono-repeat. -
background-positionspecifies the position of image.0% 0%means top left which is default. -
background-sizedefines the size of our background.- Syntax here as follows:
<width> <height>
- Syntax here as follows:
-
-
width- Setting
widthusingmin()function. -
min()function returns minimum value of its arguments. -
min(85%, 54rem)- In mobile devices,
85%will be body's width, but for desktop devices,54remwill be body's width.
- In mobile devices,
- Setting
-
padding- If you see the design file, there is some space at the top. So we're using
padding-topto get that space.
- If you see the design file, there is some space at the top. So we're using
-
margin: autoto center thebody.
After we add background to our page, It looks like,
HTML
Next step is writing HTML Content.
We're going to use three semantic elements header, main and footer.
header
<header class="card">
<h1>TODO</h1>
<button id="theme-switcher">
<img src="./assets/images/icon-sun.svg" alt="Change color theme" />
</button>
</header>
main
<main>
<div class="card add">
<div class="cb-container">
<button id="add-btn">+</button>
</div>
<div class="txt-container">
<input
type="text"
class="txt-input"
placeholder="Create a new todo..."
spellcheck="false"
autocomplete="off"
/>
</div>
</div>
<ul class="todos"></ul>
<div class="card stat">
<p class="corner"><span id="items-left">0</span> items left</p>
<div class="filter">
<button id="all" class="on">All</button>
<button id="active">Active</button>
<button id="completed">Completed</button>
</div>
<div class="corner">
<button id="clear-completed">Clear Completed</button>
</div>
</div>
</main>
footer
<footer>
<p>Drag and drop to reorder list</p>
</footer>
Don't worry about HTML, We're going to discuss each and every line. 👍
Some more resets
In the above code blocks, we've used input and button elements. We can have some resets for them,
input,
button {
font: inherit; /* by default input elements won't inherit font
from its parent */
border: 0;
background: transparent;
}
input:focus,
button:focus {
outline: 0;
}
button {
display: flex;
user-select: none;
}
In the above code blocks, I've used display: flex; for button since we're including img inside button in markup.
without display: flex
|
with display: flex
|
|---|---|
Hope you can see the different between two images.
Approach
If you look at the design file that I've included in the top of this post, you may get lot of ideas to replicate the same in browser.
One thing I got, We're going to assume all as cards. Each card may contain one or more items.
If you take header,
It contains two, one is heading h1 and on the other side is a button
This is going to be our approach.
Let's design a card
.card {
background-color: var(--clr-card-bg);
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.9rem 2rem;
gap: 2rem;
}
But, some cards will look different, for eg. header card doesn't contain any background-color and our last div.stat looks very different.
So,
header.card {
background: transparent;
padding: 0;
align-items: flex-start;
}
Let's continue..
There's a h1 in header.
header.card h1 {
color: var(--clr-white);
letter-spacing: 1.3rem;
font-weight: 700;
font-size: calc(var(--base-font) * 2);
}
calc() allows us to do arithmetic calculations in css. Here,
calc(var(--base-font) * 2)
= calc(1.6rem * 2)
= 3.2rem
Add Todo container
It is also a card. But It has some margins at the top and bottom and border-radius. So, let's add that.
.add {
margin: 4rem 0 2.5rem 0;
border-radius: 0.5rem;
}
And for plus button #add-btn,
/* add-btn */
.add .cb-container #add-btn {
color: var(--clr-gb-2);
font-size: var(--base-font);
transition: color 0.3s ease;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
/* add some transition for background */
.add .cb-container {
transition: background 0.3s ease;
}
/* define some states */
.add .cb-container:hover {
background: var(--clr-blue);
}
.add .cb-container:active {
transform: scale(0.95);
}
.add .cb-container:hover #add-btn {
color: var(--clr-white);
}
And the text input container should stretch to the end. flex: 1 will do that.
.add .txt-container {
flex: 1;
}
and the actual input field,
.add .txt-container .txt-input {
width: 100%;
padding: 0.7rem 0;
color: var(--clr-gb-1);
}
We can also style the placeholder text using ::placeholder,
Here we go,
.add .txt-container .txt-input::placeholder {
color: var(--clr-gb-5);
font-weight: var(--fw-normal);
}
Checkbox
MARKUP
.cb-container [Container for checkbox]
.cb-input [Actual checkbox]
.check [A span to indicate the value of checkbox]
.cb-container
.card .cb-container {
width: 2.5rem;
height: 2.5rem;
border: 0.1rem solid var(--clr-gb-5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.cb-input
.card .cb-container .cb-input {
transform: scale(1.8);
opacity: 0;
}
Here, we're using transform: scale() ie. scale() just zooms the field.
without scale()
|
with scale()
|
|---|---|
Since we're hiding our input using opacity: 0, User can't see the input, but can see the container. i.e. Input has to fill the entire container. That's the point of using scale().
And our span element i.e. .check
.card .cb-container .check {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
border-radius: inherit;
}
We're using pointer-events: none; here. Since, It's positioned absolute, It hides its parent which is .cb-container thereby not letting the user to check the checkbox.
To fix that, we can use pointer-events: none; which means that the current element i.e. .check won't react to any kind of mouse events. If user clicks there, checkbox will be clicked.
We can find whether the checkbox is checked or not using :checked
.card .cb-container .cb-input:checked + .check {
background: url("../assets/images/icon-check.svg"),
linear-gradient(45deg, var(--clr-green), var(--clr-pink));
background-repeat: no-repeat;
background-position: center;
}
Here, the selector defines,
.check coming after .cb-input which is checked.
We're just adding a background image and color to indicate that this checkbox is true (checked).
Todos container
Todos container .todos is a collection of .card.
MARKUP
.todos [todo container]
.card [a card]
.cb-container + ------------ +
.cb-input | [CHECKBOX] |
.check + ------------ +
.item [Actual text i.e. todo]
.clear [clear button only visible when user hovers over
the card]
We need to add border-radius only for first card. We can add that using :first-child.
.todos .card:first-child {
border-radius: 0.5rem 0.5rem 0 0;
}
If you look at the above image, you can see there's a line after each card. We can add that easily using,
.todos > * + * {
border-top: 0.2rem solid var(--clr-gb-6);
}
In this block, Each card will be selected and border-top will be added to the card next to the selected card.
And for the actual text, .item
.item {
flex: 1; /* item needs to be stretched */
color: var(--clr-gb-2);
}
/* Hover state */
.item:hover {
color: var(--clr-gb-1);
}
And the .clear button,
.clear {
cursor: pointer;
opacity: 0;
transition: opacity 0.5s ease;
}
.clear button is visually hidden. It'll only be visible when user hovers over the card.
Hover state
/* .clear when .card inside .todos is being hovered */
.todos .card:hover .clear {
opacity: 1;
}
Stat containers .stat
MARKUP
.stat [stat container]
#items-left [text - items-left]
.filter [filter-container to filter todos, we use in js]
#all
#active
#completed
.corner [corner contains button for Clear Completed]
button
.stat {
border-radius: 0 0 0.5rem 0.5rem;
border-top: 0.2rem solid var(--clr-gb-6);
font-size: calc(var(--base-font) - 0.3rem);
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
/* Add color property */
.stat * {
color: var(--clr-gb-4);
}
We're using grid layout here, since It is easy to make .stat container responsive in smaller devices.
And for the filter buttons .filter
.stat .filter {
display: flex;
justify-content: space-between;
font-weight: var(--fw-bold);
}
.stat .filter *:hover {
color: var(--clr-primary);
}
And finally if you see the corner Clear Completed, It's aligned to the right side.
.stat .corner:last-child {
justify-self: end;
}
/* Hover state for button */
.stat .corner button:hover {
color: var(--clr-primary);
}
Footer
There is only one paragraph in the footer.
footer {
margin: 4rem 0;
text-align: center;
color: var(--clr-gb-5);
}
Responsive css
We need to change grid style of .stat in smaller devices, introducing two grid rows.
@media (max-width: 599px) {
.stat {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 5rem 2rem;
}
.stat .filter {
grid-row: 2/3;
grid-column: 1/3;
justify-content: space-around;
}
}
Thank you!, That's it for this post! Next is adding interactivity to our page using JavaScript. A post on adding interactivity to our app is here.
Feel free to check my Github Repository
If you have any questions, please leave them in the comments.

Top comments (4)
I've included javascript section in my new post.
Please check this out, TODO App - Javascript
Somehow on iPad, there is no space between (+) symbol and text for the todo.
Hi, I've used
gapproperty to get space between items in the card. Make sure you're using latest version of chrome.gapis supported from Chrome84. It is not supported in Safari browser.Can you please upload a screenshot here?