DEV Community

loading...
Cover image for TODO APP using HTML, CSS and JS - Local Storage [Design - HTML and CSS]

TODO APP using HTML, CSS and JS - Local Storage [Design - HTML and CSS]

Hari Ram
Hello Everyone, I'm Hari Ram
Updated on ・9 min read

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,

TODO design

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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

In the above code block,

  • We're setting margin, padding for all elements to be 0.
  • Our box-sizing will be border-box which basically lets us get rid of overflow error.
  • We're setting our base font-size to 62.5% i.e. 10px which makes our rem 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]
Enter fullscreen mode Exit fullscreen mode
  • 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 presses Ctrl + 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;
}
Enter fullscreen mode Exit fullscreen mode

Here, In this code block,

  • font
    • font is a shorthand property for <font-weight> <font-size> <font-family>
    • So, our font will be 400 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 and background-image defines color and image.
    • background-repeat defines whether the background image needs to be repeated or not. In our case, not, so no-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>
  • width
    • Setting width using min() 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.
  • padding
    • If you see the design file, there is some space at the top. So we're using padding-top to get that space.
  • margin: auto to center the body.

After we add background to our page, It looks like,

TODO first step - adding background

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

footer

<footer>
  <p>Drag and drop to reorder list</p>
</footer>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Alt Text Alt Text

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,

Todo 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

calc() allows us to do arithmetic calculations in css. Here,

calc(var(--base-font) * 2)
    = calc(1.6rem * 2)
    = 3.2rem
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

And the text input container should stretch to the end. flex: 1 will do that.

.add .txt-container {
  flex: 1;
}
Enter fullscreen mode Exit fullscreen mode

and the actual input field,

.add .txt-container .txt-input {
  width: 100%;
  padding: 0.7rem 0;
  color: var(--clr-gb-1);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Checkbox

MARKUP

.cb-container  [Container for checkbox]
  .cb-input    [Actual checkbox] 
  .check       [A span to indicate the value of checkbox]
Enter fullscreen mode Exit fullscreen mode

.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;
}
Enter fullscreen mode Exit fullscreen mode

.cb-input

.card .cb-container .cb-input {
  transform: scale(1.8);
  opacity: 0;
}
Enter fullscreen mode Exit fullscreen mode

Here, we're using transform: scale() ie. scale() just zooms the field.

without scale() with scale()
Alt Text Alt Text

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

TODO container

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]
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

And the .clear button,

.clear {
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.5s ease;
}
Enter fullscreen mode Exit fullscreen mode

.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;
}
Enter fullscreen mode Exit fullscreen mode

Stat containers .stat

Alt Text

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      
Enter fullscreen mode Exit fullscreen mode
.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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Footer

There is only one paragraph in the footer.

footer {
  margin: 4rem 0;
  text-align: center;
  color: var(--clr-gb-5);
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (4)

Collapse
hariramjp777 profile image
Hari Ram Author

I've included javascript section in my new post.

Please check this out, TODO App - Javascript

Collapse
sudipto05 profile image
EverydayRealisations

Somehow on iPad, there is no space between (+) symbol and text for the todo.

Collapse
hariramjp777 profile image
Hari Ram Author

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 Chrome 84. It is not supported in Safari browser.

Collapse
hariramjp777 profile image
Hari Ram Author

Can you please upload a screenshot here?