Styles
The page works; now it should look like a packing list. The starter's src/styles.css sets the page-wide basics and the color variables, --text, --muted, --line, and --accent. Styles that belong to one component live with that component, written with the css tag.
A styled row
Update src/components/item.ts:
// One row of the list, with styles that apply to this row and nothing else.
import { type ReadableSignal, type View, computed, css, html } from "zerolith";
import { type Item, remove, toggle } from "../store.ts";
const row = css`
:scope {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.5rem 0; border-bottom: 2px solid var(--line);
}
label { display: flex; align-items: center; gap: 0.75rem; flex: 1; }
:scope[data-packed="true"] label {
color: var(--muted); text-decoration: line-through;
}
`;
export function ItemRow({ item }: { item: ReadableSignal<Item> }): View
{
const name = computed(() => item.get().name);
const packed = computed(() => item.get().packed);
return html`
<li class=${row} data-packed=${packed}>
<label>
<input type="checkbox" .checked=${packed}
@change=${() => toggle(item.get().id)}>
${name}
</label>
<button type="button" aria-label="Remove ${name}"
@click=${() => remove(item.get().id)}>Remove</button>
</li>`;
}css returns a style sheet scoped to the elements that use it: class=${row} applies it to the row. Inside it, :scope is the row itself, and the other rules match only inside the row, so label here never styles a label anywhere else on the page. The styles are added to the document once, however many rows there are, with the browser's @scope rule and no shadow DOM, so the page's own styles and your design tokens still reach in.
data-packed=${packed} turns the item's state into an attribute the style sheet can select, which keeps the rule in CSS rather than in a class name built in JavaScript. The styles guide shows where a scope stops when one styled component sits inside another.
A styled page
The page's styles can live in a stylesheet of their own, beside the component. Create src/components/packing.css:
:scope
{
display: grid;
gap: 1.5rem;
}
h1,
p
{
margin: 0;
}
ul
{
margin: 0;
padding: 0;
list-style: none;
border-top: 2px solid var(--line);
}Then import it in src/components/packing.ts:
// The packing list page: progress, the filters, the rows, and a note when
// the filter leaves nothing to show. Its styles are in packing.css.
import { type View, computed, each, html, when } from "zerolith";
import { items, packedCount, shown } from "../store.ts";
import { AddItem } from "./add-item.ts";
import { Filters } from "./filters.ts";
import { ItemRow } from "./item.ts";
import page from "./packing.css?scoped";
export function Packing(): View
{
const total = computed(() => items.get().length);
const empty = computed(() => shown.get().length === 0);
return html`
<section class=${page}>
<h1>Packing list</h1>
<p>${packedCount} of ${total} packed</p>
${Filters()}
<ul>${each(shown, (item) => item.id, (item) => ItemRow({ item }))}
</ul>
${when(empty, () => html`<p>Nothing to show here.</p>`)}
${AddItem()}
</section>`;
}?scoped on the import turns the file into the same scoped style sheet a css literal makes, and class=${page} applies it the same way: :scope is the section, and the rules stop at the rows, which have styles of their own. A literal suits a few rules beside the markup, as in the row; a file suits a longer stylesheet, which your editor checks and completes as CSS. zerolith dev applies an edit to the file in place, without reloading the page.
The page is now a section with its own scoped styles, and the list is the finished one. src/components/add-item.ts and src/components/filters.ts stay as they were.
What you learned
csswrites a style sheet scoped to one component, applied withclass=${styles}.:scopeis the element the styles apply to; other rules match only inside it.- A stylesheet imported with
?scoped, asimport page from "./packing.css?scoped", is scoped the same way, for styles that read better in a file of their own. - Attributes bound to state, such as
data-packed, let CSS follow the state.