0.1.0-alpha.7

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

  • css writes a style sheet scoped to one component, applied with class=${styles}.
  • :scope is the element the styles apply to; other rules match only inside it.
  • A stylesheet imported with ?scoped, as import 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.