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
,padding
for all elements to be0
. - Our
box-sizing
will beborder-box
which basically lets us get rid of overflow error. - We're setting our base
font-size
to62.5%
i.e.10px
which makes ourrem
calculation 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: none
to 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
-
font
is a shorthand property for<font-weight> <font-size> <font-family>
- So, our
font
will be400 1.6rem "Josefin Sans", sans-serif
.
-
-
background
-
background
is a shorthand property for<background-color> <background-image> <background-repeat> <background-position> / <background-size>
. -
background-color
andbackground-image
defines color and image. -
background-repeat
defines whether the background image needs to be repeated or not. In our case, not, sono-repeat
. -
background-position
specifies the position of image.0% 0%
means top left which is default. -
background-size
defines the size of our background.- Syntax here as follows:
<width> <height>
- Syntax here as follows:
-
-
width
- Setting
width
usingmin()
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,54rem
will 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-top
to get that space.
- If you see the design file, there is some space at the top. So we're using
-
margin: auto
to 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
gap
property to get space between items in the card. Make sure you're using latest version of chrome.gap
is supported from Chrome84
. It is not supported in Safari browser.Can you please upload a screenshot here?