<header>
<svg width="20" height="20" viewBox="0 0 240 260" class="svg-check site-logo">
<polyline class="cls-1" points="16 149 72 225 225 16" pathLength="1" />
</svg>
<h1>TODOs</h1>
</header>
<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 viewBox="-20 -20 240 240" class="svg-plus">
<g>
<line x1="100" x2="100" y2="200" />
<line y1="100" x2="200" y2="100" />
</g>
</svg>
</button>
</form>
<ol id="todo-list" class="todo-list"></ol>
* {
box-sizing: border-box;
}
html {
--gray800: oklch(10% 0% 0);
--gray600: oklch(40% 0% 0);
--gray100: oklch(92% 0% 0);
--brand: oklch(85% 0.3 145);
font-family: system-ui, sans-serif;
background: var(--gray100);
}
body {
margin: 0;
}
header {
background: var(--gray800);
color: white;
padding: 1rem;
display: flex;
align-items: center;
gap: 1rem;
h1 {
margin: 0;
font-variation-settings: "wght" 900, "wdth" 700;
}
.svg-check {
width: 2lh;
height: 2lh;
stroke: var(--brand);
}
}
.svg-check,
.svg-plus {
fill: none;
stroke-width: 30;
stroke-linecap: round;
stroke-linejoin: round;
display: block;
pointer-events: none;
}
.todo-form {
background: var(--gray600);
justify-content: center;
padding: 1rem;
display: flex;
gap: 0.5rem;
align-items: stretch;
label {
flex: 1 0;
max-width: 40ch;
}
input {
background: var(--gray100);
width: 100%;
border: 0;
padding: 0.5rem;
box-shadow: inset 1px 1px 2px oklch(0% 0% 0 / 0.6);
border-radius: 5px;
font: inherit;
font-size: 2rem;
&:focus {
background: white;
}
}
.svg-plus {
stroke: black;
width: 66%;
}
}
.button-add-todo {
border: 0;
border-radius: 5px;
font: inherit;
font-size: 2rem;
font-weight: 900;
background: var(--brand);
aspect-ratio: 1;
height: 3.35rem;
display: inline-grid;
place-items: center;
line-height: 0;
&.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;
}
}
.todo-list {
padding: 1rem;
list-style: none;
li {
background: white;
box-shadow: 1px 1px 2px oklch(0% 0% 0 / 0.2);
padding: 0.5rem;
margin-block-end: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
.button-complete {
order: -1;
}
.svg-check {
stroke: black;
position: absolute;
width: 2rem;
height: 2rem;
top: -0.5rem;
left: 0.1rem;
opacity: 0;
}
&.complete {
.svg-check {
opacity: 1;
stroke-dasharray: 1;
stroke-dashoffset: 1;
/* https://css-tricks.com/a-trick-that-makes-drawing-svg-lines-way-easier/ */
animation: do-check 1s infinite alternate;
}
}
}
}
@keyframes do-check {
from {
stroke-dashoffset: 1;
}
to {
stroke-dashoffset: 0;
}
}
.button-complete {
border: 0;
background: var(--gray100);
box-shadow: inset 1px 1px 2px oklch(0% 0% 0 / 0.2);
padding: 0.25rem;
width: 1.5rem;
height: 1.5rem;
position: relative;
}
.screen-reader-text {
text-indent: -9999em;
width: 0;
height: 0;
display: block;
}
const form = document.querySelector("#todo-form");
const list = document.querySelector("#todo-list");
const buttonAddTodo = document.querySelector("#button-add-todo");
// Get Data
let TODOs = [];
if (localStorage["data"] !== null && localStorage["data"] !== undefined) {
TODOs = JSON.parse(localStorage["data"]);
}
function buildUI() {
let HTML = ``;
TODOs.forEach((todo) => {
HTML += `<li id="${todo.id}" style="view-transition-name: list-item-${todo.id};">
${todo.title}
<button aria-label="Complete" class="button-complete">
<svg width="20" height="20" viewBox="0 0 241.44 259.83" class="svg-check">
<polyline points="16.17 148.63 72.17 225.63 225.17 11.63" pathLength="1" />
</svg>
</button>
</li>`;
});
list.innerHTML = HTML;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
// Don't allow empty todo
if (!form[0].value) {
buttonAddTodo.classList.add("shake");
return;
}
addTodo(event);
form.reset();
});
buttonAddTodo.addEventListener("animationend", () => {
buttonAddTodo.classList.remove("shake", "added");
});
function addTodo() {
TODOs.push({
title: form[0].value,
complete: false,
id: self.crypto.randomUUID()
});
localStorage["data"] = JSON.stringify(TODOs);
buttonAddTodo.classList.add("added");
buildUI();
}
document.documentElement.addEventListener("click", (event) => {
if (event.target.classList.contains("button-complete")) {
removeTodo(event);
}
});
function removeTodo(event) {
const listItem = event.target.parentElement;
// Trigger complete animation
listItem.classList.toggle("complete");
setTimeout(() => {
TODOs = TODOs.filter((todo) => todo.id !== event.target.parentElement.id);
localStorage["data"] = JSON.stringify(TODOs);
if (!document.startViewTransition) {
buildUI();
} else {
document.startViewTransition(() => {
buildUI();
});
}
}, 1000);
}
buildUI();
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.