# 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))}>
```
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.