# Todo App: Markup and Styles The complete markup and stylesheet for the [todo app](todo-app). The recipe builds the page piece by piece; this is the whole of it, assembled, so the page looks finished the first time it renders. Copy both files. The stylesheet is mostly layout. The card, tabs, buttons, inputs and checkbox come styled from `@elements/style`, and every color, space and duration below is a token, so it follows dark mode and a brand theme with nothing more. ## The template The top of `template.ehtml` imports what the functions in the recipe use. The functions go between the imports and the markup. ```ts app/pages/home/template.ehtml import "./style.css"; import { LiveView, flush } from "@elements/app"; import { byPriority, Draft, Edit, Filter, matches, parseFilter, Todo, View } from "./models"; ``` ```ehtml app/pages/home/template.ehtml , view: View, private draft: Draft = { title: "" }, private edit: Edit = { id: "", title: "", }, ) oninit={() => window.addEventListener("popstate", () => onPopState(view))}> Todos

Todos

onAdd(todos, draft)}>

Nothing on the list

All caught up

Nothing finished yet

0}>
``` The label is a button, not a span, so it can take focus and be clicked to rename. The stylesheet makes it read as text. ## The stylesheet Save it before the template (`start/patterns`): the reload that first shows the markup then already has its rules. ```css app/pages/home/style.css @import "#app/shared/styles/page.css"; .todo-page { padding-block: var(--space-10); } .todo-heading { margin-block: 0 var(--space-6); } /* The card's own parts would pad every row the same; the list wants its rows edge to edge, so the card is laid out here instead. */ .todo-composer { display: flex; gap: var(--space-2); padding: var(--space-4); } .todo-composer input { flex: 1; min-width: 0; } .todo-tabs { padding-inline: var(--space-4); } .todo-tab-count { margin-inline-start: var(--space-1-5); color: var(--ink-faint); font-variant-numeric: tabular-nums; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-item { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-4); border-bottom: 1px solid var(--rule-soft); } .todo-item:last-child { border-bottom: 0; } /* The label is a button so it can be focused and clicked to rename, but it reads as text: no box, left-aligned, wrapping. */ .todo-label { flex: 1; min-width: 0; height: auto; padding: var(--space-1) 0; border: 0; background: none; color: var(--ink); font: inherit; text-align: start; justify-content: flex-start; overflow-wrap: anywhere; } .todo-text { flex: 1; min-width: 0; } .todo-item.is-done .todo-label { color: var(--ink-faint); text-decoration: line-through; } .todo-star, .todo-remove { border-color: transparent; color: var(--ink-faint); } .todo-star.is-starred { color: var(--warning); } .todo-remove:hover { color: var(--danger); } /* The delete waits on this animation's animationend; LEAVE_MS in the template matches --duration-normal as the backstop. */ .todo-item.is-leaving { animation: todo-leave var(--duration-normal) var(--ease-in) forwards; } @keyframes todo-leave { to { opacity: 0; transform: translateX(var(--space-4)); } } @media (prefers-reduced-motion: reduce) { .todo-item.is-leaving { animation-duration: 1ms; } } .todo-empty { padding: var(--space-10) var(--space-4); text-align: center; } .todo-empty-title { margin: 0; color: var(--ink-soft); } .todo-foot { display: flex; justify-content: flex-end; padding: var(--space-3) var(--space-4); border-top: 1px solid var(--rule-soft); } ``` The leave animation is what ends a delete. `onLeaveEnd` runs on its `animationend`, and `LEAVE_MS` in the template is the backstop timer, set to match `--duration-normal`. Change one and change the other.