Search

Building a TODO App from Scratch — Step 4 — Styling & Interactive Choices

I’m already a little over all the green. What was I thinking? I’m much more of a nice gray color palette kinda guy with some fun poppy colors. That’s what we need to get to here. When we left off, we just had wireframes going. It’s time to get some style on this thing!

Article Series

Custom Properties

We can get rid of that wireframe look by removing all those 1px borders and applying some backgrounds. Let’s give ourselves some Custom Properties to work with, so we can apply colors in such a way they are easy to change later.

html {
  --gray800: oklch(10% 0% 0);
  --gray600: oklch(40% 0% 0);
  --gray100: oklch(92% 0% 0);
  --brand: oklch(85% 0.3 145);

  background: var(--gray100);
}Code language: CSS (css)

You can see we’re using the lightest gray right away in colorizing the background.

I’m using OKLCH color function here because I find it the generally-nicest of all the color models. One very immediate thing it does for us here is that it unlocks P3 colors. That --brand color you see there is actually a bit more bright and vibrant than is even possible in the sRGB color space (like a hex code or any of the more common color functions you may be familiar with, like rgb()).

Applying those colors to the relevant sections gets us a start:

Fonts

We don’t need to go too far down the typography rabbit hole here, as all our design calls for is the header, one input, and a list of to-do items. Keeping it simple and fast (not loading fonts is… fast), is ideal, so:

html {
  font-family: system-ui, sans-serif;
}Code language: CSS (css)

You’d think system-ui would be boring, but as a little fun and progressive enhancement, we can use some font-variation-settings on it in the case that the font that loads supports them. It’s not the only one, but Apple’s San Francisco font ships as a variable font, so we can do fun things like stretch it out.

header {
  background: var(--gray800);
  color: white;
  h1 {
    font-variation-settings: "wght" 900, "wdth" 700;
  }
}Code language: CSS (css)

I love this look, myself:

SVGs

We need SVG’s for the “check” of the logo and that we’ll use when a user checks off a to-do. I’m kinda thinking we just re-use the same one for both as that just makes good sense. Plus we’ll make a “+” SVG too. It might be tempting to find unicode characters for these, but I’m telling you, just use SVG. The predictable bounding box of SVG will be your friend (no fighting against weird space).

There are loads of resources for finding SVGs to use. I’m usually a Noun Project guy, but if we needed, say, 10 or more icons I’d probably try to find a more cohesive set somewhere. These are also so simple that could (should) draw them ourselves (even directly in code), but we’ll save that for another day.

Ultimately, we’ll just get our hands on a “check” icon and use it in the logo…

<header>
  <svg ... class="svg-check site-logo">
    <polyline ... />
  </svg>
  <h1>TODOs</h1>
</header>Code language: HTML, XML (xml)

… in our template literal for each to-do …

HTML += `<li id="${todo.id}">
  ${todo.title}
  <button aria-label="Complete" class="button-complete">
    <svg ... class="svg-check">
      <polyline ... />
    </svg>
  </button>
</li>`;Code language: JavaScript (javascript)

… and then our “+” in the form:

<form id="todo-form" class="todo-form">
  <label>
    <span class="screen-reader-text">New TODO</span>
    <input type="text" name="todo">
  </label>
  <button id="button-add-todo" class="button-add-todo">
    <svg ... class="svg-plus">
      <g>
        <line ... />
        <line ... />
      </g>
    </svg>
  </button>
</form>Code language: JavaScript (javascript)

There is a lot to know about SVG so I’m really just dropping the basic usage in here. I’ve chosen to drop the SVG code right “inline” with the HTML because I generally think that’s best because it’s fast and offers the most styling control. If you wanna dig more into SVG, I did write a book about it one time you could check out.

With the SVG’s I chose in there:

Adding To-Do Interactions

When we add a new to-do, I want to give the adding button a temporary class so we can do something fun with it. So we’ll use a saved reference to that button and do that:

buttonAddTodo.classList.add("added");Code language: JavaScript (javascript)

But this is an opportunity! We could add a different class upon a failure. What failure? Not entering any text, for one. We could make the call not to allow entering blank to-dos, which at this point I think is a decent idea.

So the form event handler then becomes like:

form.addEventListener("submit", (event) => {
  event.preventDefault();
  // Don't allow empty todo
  if (!form[0].value) {
    buttonAddTodo.classList.add("shake");
    return;
  }
  addTodo(event);
  form.reset();
});Code language: JavaScript (javascript)

Now check out these little @keyframe animations I can do with those classes:

.button-add-todo {
  ...

  &.shake {
    rotate: 0deg;
    transform-origin: bottom right;
    animation: shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
  }
  &.added {
    transform-origin: center center;
    animation: added 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
  }
}
@keyframes shake {
  50% {
    rotate: -12deg;
  }
}
@keyframes added {
  50% {
    rotate: 1turn;
    translate: 0 50px;
  }
}Code language: CSS (css)

A little fun, maybe?

Let’s be careful to remove those classes after the animation happens, so that another animation can be triggered freshly when needed:

buttonAddTodo.addEventListener("animationend", () => {
  buttonAddTodo.classList.remove("shake", "added");
});Code language: JavaScript (javascript)

Completing To-Do Interactions

We’ve got a big ol’ box sitting there waiting for to-do items to be checked. And now that we’re using SVG, my temptation is to do a little fun “line drawing” with that. That is, see the checkbox be “drawn” as we complete the to-do, before removing it.

Here’s the trick.

First we go find the SVG itself in our template literal, and add this special pathLength attribute to the actual shape:

<svg width="20" height="20" viewBox="0 0 241.44 259.83" class="svg-check">
  <polyline pathLength="1" points="16.17 148.63 72.17 225.63 225.17 11.63" />
</svg>
Code language: HTML, XML (xml)

This just makes animating it much easier.

Then we set and animate some specific SVG properties to do the drawing:

.todo-list {
  ...
  li {
    ...
    .svg-check {
      opacity: 0;
    }
    &.complete {
      .svg-check {
        opacity: 1;
        stroke-dasharray: 1;
        stroke-dashoffset: 1;
        animation: do-check 1s infinite alternate;
      }
    }
  }
}

@keyframes do-check {
  from {
    stroke-dashoffset: 1;
  }
  to {
    stroke-dashoffset: 0;
  }
}Code language: CSS (css)

Check out the drawing effect at work:

The trick to getting the animation to run before being removed is just to … wait.

function removeTodo(event) {
  const listItem = event.target.parentElement;
  listItem.classList.toggle("complete");
  setTimeout(() => {
    TODOs = TODOs.filter((todo) => todo.id !== event.target.parentElement.id);
    localStorage["data"] = JSON.stringify(TODOs);
    buildUI();
  }, 1000);
}Code language: JavaScript (javascript)

You can see I’m just waiting for a second there, which is about the same, a little longer, than the animation itself. We could tie it to the animationend event here also, but for some reason it felt better to me to leave this bit of business logic in the code here rather than tying it to a CSS thing.

Really smoooooth completions

What if we remove a to-do from the middle of the list. It’ll be jerky won’t it? It certainly could be. This is one of those classic situations in CSS where being able to “animate to/from auto” would be nice. In this case, we want to animate from “however tall a list item is” to zero. That way the bottom items in the list would slide up, making a much more comfortable animation. But honestly, even animating the height here feels like slight of hand, what I actually want is just not to think very hard about it and for the list items to just slide up into place, if at all possible.

Turns out this is a lovely use-case for View Transitions. Without getting very deep at all into it, essentially all we need to do is:

  1. Give all our to-dos a unique view-transition-name
  2. When we update the DOM, e.g. buildUI(), wrap that in a startViewTransition() function.

So our template literal is updated to be like…

  TODOs.forEach((todo) => {
    HTML += `<li id="${todo.id}" style="view-transition-name: list-item-${todo.id};">Code language: JavaScript (javascript)

We already have a unique ID so we’ll steal that for the dual purpose here.

Then wrap our DOM updating function like:

if (!document.startViewTransition) {
  buildUI();
} else {
  document.startViewTransition(() => {
    buildUI();
  });
}Code language: JavaScript (javascript)

That’s the same and progressive-enhancement friendly way to do it. It’s kind of required here, as that startViewTransition will throw in browsers that don’t support it and it could break our whole site.

Look at us know:

This is friggin’ cool if you ask me.

So far

Just a little bit more functionality to go!

Article Series

Wanna learn SVG & Animation deeply?

Frontend Masters logo

We have an incredible course on all things CSS and SVG animation from Sarah Drasner. Sarah comprehensively covers the possibilty of animation, the tools, and does it all in a very practical way.

Leave a Reply

Your email address will not be published. Required fields are marked *