# Design screens with GizWeb

GizWeb ships one design system. Every app uses it: a site of pages, an admin, an agent workspace, a data
studio. You write what a screen shows. The parts give it a look that works on a phone and on a desktop, in
light and dark, with a keyboard and a screen reader, and under every accessibility setting of the browser.

- The guide is for developers who build screens with GizWeb. It assumes you can serve a page
  ([GizWeb guide](web.md)) and write a `.ghtml` file ([GHTML](../reference/ghtml.md)).
- The quick start builds one screen and changes its brand. Each later section answers one question.
- Each example ran on the current toolchain. The screenshots are of that run.

## The model in one picture

```text
 tokens        web/theme/base.json, dark.json      --color-accent, --text-sm, --radius-lg, --spacing
   │           a brand or a tenant: an overlay of a few tokens
   ▼
 parts         web/components/ui.css, *.ghtml      .ui-card, .ui-table, .ui-button, <Dialog>, <Menu>
   │           cascade layer `components`
   ▼
 screens       your .ghtml files                   which parts, in which order, with what data
               scoped <style>: only the screen's own layout
```

Three rules follow from it:

1. **A screen uses parts.** A card is `class="ui-card"`, not a rule of your own that looks like a card.
2. **A value is a token.** A color, a size, a radius or a shadow is `var(--…)`. A raw `#5b5bd6` or `13px` is a
   defect.
3. **A missing part goes into GizWeb.** Then every app gets it, with its dark mode, its focus ring and its
   phone layout.

This repository holds its own code to the same rule (the "One design system" line of
[the contributor guide](https://github.com/gizai/gizlang/blob/main/AGENTS.md)).

## Quick start: a screen and a brand

A site of pages is a folder ([GHTML §7](../reference/ghtml.md#7-pages-a-folder-of-files-is-a-site)). This page
shows a list of projects, two figures and a form. It writes no style of its own except the one rule that puts
the two cards side by side.

`pages/index.ghtml`:

```html title="pages/index.ghtml"
<script>
  type Project = { name: string; owner: string; state: string; runs: i64; };
  const projects: Vec<Project> = [Project("Billing", "Mina", "live", 1204), Project("Search", "Jon", "building", 87)];
</script>

<main class="ui-page">
  <header class="ui-pagehead">
    <div><h1>Projects</h1><p>Every project of the team, and its last deploy.</p></div>
    <div class="ui-actions"><a class="ui-button pri" href="/new">New project</a></div>
  </header>

  <div class="ui-stats">
    <div><small>Projects</small><b>{projects.length}</b></div>
    <div><small>Runs today</small><b>1,291</b><span>+12%</span></div>
  </div>

  <div class="cols">
    <section class="ui-card">
      <h3>Projects</h3>
      <table class="ui-table">
        <thead><tr><th>Name</th><th>Owner</th><th>State</th><th class="num">Runs</th></tr></thead>
        <tbody>
          <tr each={p of projects}>
            <td><a href={"/p/" + p.name}>{p.name}</a></td><td>{p.owner}</td>
            <td><span class="ui-badge" data-s={p.state}>{p.state}</span></td><td class="num">{p.runs}</td>
          </tr>
        </tbody>
      </table>
    </section>

    <section class="ui-card">
      <h3>Invite</h3>
      <form class="ui-form in">
        <label>Email<input name="email" type="email" required/></label>
        <button class="ui-button pri">Send the invitation</button>
      </form>
    </section>
  </div>
</main>

<style>
  /* Only this screen's own layout: the two cards side by side when the page is wide enough. */
  .cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); gap: calc(var(--spacing) * 4); }
</style>
```

`brand.json` names only the tokens that the brand changes:

```json title="brand.json"
{
  "color": {
    "$type": "color",
    "accent": { "$value": "#0f766e" },
    "link": { "$value": "#0f766e" }
  },
  "radius": { "$type": "dimension", "lg": { "$value": "14px" } }
}
```

`main.giz` puts the brand over the default theme and serves the site:

```typescript gizlang title="main.giz"
import {System} from "gizlang:system";
import * as Web from "gizweb";
import * as Pages from "gizweb:pages";
import * as Tokens from "gizweb:tokens";
import {text as brand} from "./brand.json";
import {site} from "./pages/.routes";

/** The site on port 8080, in the default theme with the brand's tokens over it. */
function main(sys: System): i64 {
    const theme = Tokens.css(Web.uiTheme().concat([Tokens.Theme("", brand)]), false).unwrap();
    return Web.listen(sys, 8080, Pages.shell(theme), Pages.serving(Pages.standalone), (own: System) => Pages.handler(site(), Pages.shell(theme), Pages.standalone(own)));
}
```

On a desktop, and at 390 px:

![The projects screen at 1280 px: a header with its action, two figures, the table and the form side by side, in the teal brand](../images/design/projects.png)

![The same screen at 390 px: the action under the title, the table and the form one under the other](../images/design/projects-phone.png)

The screen did not choose a font, a color, a border, a padding or a breakpoint. `Pages.shell("")` serves the
same screen in the default theme. Without `Pages.serving`, the site still works; only the grid's imports need it.

## Parts

A part is a class on a native element, or a `.ghtml` component when it needs more than one element. A part has
no state and needs no script; the browser gives the behavior (`<details>`, `<dialog>`, `popover`, radio
inputs).

### Page structure

| Part | Markup | Notes |
| --- | --- | --- |
| `ui-page` | `<main class="ui-page">` | A page of a surface: parts one under another, as wide as reads well. |
| `ui-pagehead` | `<header>`: an `h1`, an optional `p`, then the actions | The actions go to the right; they wrap under the title when narrow. |
| `ui-card` | `<section>`: an `h3`, then the content; `.in` pads it | A titled section. A table or a list in a card goes edge to edge. It has no outer margin: the page or the grid that holds it sets the gap. |
| `ui-stats` | `<div>` of `<div><small>name</small><b>value</b><span>note</span></div>` | Figures in a row that wraps. |
| `ui-tiles` | a list of `ui-tile` | Apps, agents, files. `--tile-min` sets the least width; `aria-selected` marks the chosen one. |
| `ui-fold` | `<details class="ui-fold"><summary>` | A folded section; the mark turns when it opens. |
| `ui-crumbs` | `<nav>` of links, separators with `aria-hidden`, the current place as `b` | Names shrink with an ellipsis. |
| `ui-seg` | links, or a `Menu` of more; `aria-current` on the current one | A choice of views. It scrolls sideways; it never wraps. |
| `ui-empty` | a mark, a `b` title, one sentence, at most one action | Nothing to show yet. |

### Data

| Part | Markup | Notes |
| --- | --- | --- |
| `ui-table` | `<table>`; `th.num`, `td.num` for numbers | Hairline rows, a tint under the pointer. Numbers line up at their end and do not break. |
| `ui-list` | `<ul>`; each `li` is a name (`div` of `b` and `small`), then a badge or actions | What follows the end of a row takes the row's width. |
| `ui-badge` | `<span class="ui-badge" data-s="live">` | A state: a dot and a word. `data-s` is `live`, `building`, `failed`, `new`, `preview`, `rejected` or `replaced`. |
| `ui-chip` | the `Chip` component, `tone` `ok`, `warn` or `bad` | A short tag with a tone. |
| `ui-tag` | `<span class="ui-tag">` | A plain label beside a name. A state with a meaning is a badge. |
| meter | `<progress max="100" value={n} aria-label="Used">` | How much of a whole is done or used. A page sets only its width. |
| `ui-output` | head with `data-s="ok"` or `"failed"`, `h3` and `pre` panes | The output of a run. An empty `pre` shows its `data-empty` text. |
| Region | the `Region` component, a `Load` value | Loading, empty, failed and ready, with the same markup everywhere ([data states](https://github.com/gizai/gizlang/blob/main/web/components/README.md#data-states)). |

### Forms and actions

| Part | Markup | Notes |
| --- | --- | --- |
| `ui-form` | `<form class="ui-form">` of `<label>Name<input/></label>` | A stacked form. Every input, select and textarea gets the control look. |
| `inline` | `<form class="inline">` | A form on one line, for filters and search. It wraps when narrow. |
| `ui-field` | the `Field` component, or `<label class="ui-field">` | A label, a hint and the line for the field's problem. |
| `ui-check` | the `Check` component | A checkbox and its text on one line. |
| `ui-button` | `<button>` or `<a>`; `pri`, `ghost`, `danger`, `ic` (an icon only) | `aria-pressed="true"` makes a toggle. |
| `ui-actions` | a `div` or `nav` of buttons | A row of actions that wraps. |
| `[data-err]` | `<small data-err>` | The kernel writes a refusal there. It takes its own row, in the danger color, and is gone while empty. |

The components with more than one element are `Field`, `DateField`, `Check`, `Chip`, `Combobox`, `Dialog`, `Menu`
and `Chart`. Their props and markup are in [the components reference](https://github.com/gizai/gizlang/blob/main/web/components/README.md#form-and-interaction-controls).
Documentation and site parts (`Callout`, `Tabs`, `SiteHeader`, `DocsLayout`, …) are there too, and so are the
agent parts (`ChatWorkspace`, `ChatMessage`, the composer).

## Tokens

The tokens are DTCG JSON in `web/theme/base.json`, with a `dark.json` overlay. Each group is a Tailwind v4 theme
namespace, so `color.accent` is `--color-accent` and, under Tailwind, `bg-accent`.

| Group | Tokens | Use |
| --- | --- | --- |
| `color` | `bg`, `surface`, `hover`, `fg`, `muted`, `line`, `line-strong` | Page, surfaces, text and edges. |
| | `accent`, `on-accent`, `link` | The one accent: the main action, focus, selection. |
| | `info`, `success`, `warning`, `danger`, `scrim` | States, and the shade behind a dialog. |
| | `code-*`, `term-*` | Code blocks and the terminal, dark in both themes. |
| `font` | `sans`, `mono` | |
| `text` | `2xs` 11, `xs` 12, `sm` 13, `base` 14, `lg` 16, `xl` 20, `2xl` 28 px | The body is `--text-base`. |
| `radius` | `md` controls, `lg` cards, `xl` dialogs, `2xl` large panels, `full` pills | |
| `shadow` | `sm` controls and cards, `pop` menus and dialogs, `float` a part over the page | |
| `spacing` | 4 px | Every gap and padding is `calc(var(--spacing) * N)`. |
| `control` | `h` 32 px | Buttons, fields and selects. |

Write a value as its token, and a tint as a mix of tokens:

```css
.note { color: var(--color-muted); font-size: var(--text-sm); border-radius: var(--radius-md); }
.picked { background: color-mix(in oklab, var(--color-accent) 12%, transparent); }
```

## Themes

`Tokens.css(themes, tailwind)` writes the CSS of a list of themes. The first is the base. Each later theme
names only the tokens it changes, and a token that the base does not have is an error that names the theme and
the token.
Text must read in every theme a page can show: the base after its brands, and each overlay over it. A text color on
what it is read on (`fg`, `muted` and `link` on `bg` and `surface`, `on-accent` on `accent`, the tones on `surface`,
the code colors) below 4.5:1 is an error that names the pair and the ratio. A color that is not hex is not checked.

| Theme name | Applies |
| --- | --- |
| `""` (after the base) | Everywhere: it changes the base. Use it for a brand. |
| `dark` | By the browser's color scheme, and by `data-theme="dark"` on the root. `data-theme="light"` turns it off. |
| any other | By `data-theme="NAME"` on the root: a tenant, a high-contrast variant. |

`Web.uiTheme()` is the default base and its dark overlay. Add your themes after it:

```typescript gizlang title="theme.giz"
import * as Web from "gizweb";
import * as Tokens from "gizweb:tokens";

/** The default theme with a brand over it: BRAND for light pages, BRAND_DARK over the default dark values. */
export function theme(brand: string, brandDark: string): string throws string {
    return try Tokens.css(Web.uiTheme().concat([Tokens.Theme("", brand), Tokens.Theme("dark", brandDark)]), false);
}
```

Give the result to `Pages.shell(theme)`. The parts follow it, so one file of a few tokens changes every screen.
An app shell of its own takes the same sheet: `App.shell(lang, handlers, theme + Web.uiComponents())`.

**With Tailwind.** Write the tokens as Tailwind's `@theme` (`Tokens.css(…, true)`) and give them to
`Tailwind.stylesheet`; its sheet goes to `Pages.shell(css)`. A utility then reads the same tokens:
`class="text-muted rounded-lg"`. See [GHTML §6](../reference/ghtml.md#6-style).

## The cascade

GizWeb's parts are in the cascade layer `components`, after `base`:

```css
@layer base, components;
@layer components { /* every part */ }
```

| Where a rule is | Wins over | Use it for |
| --- | --- | --- |
| `@layer base` | nothing | The app's element defaults: `a`, `svg`, `button`. |
| `@layer components` | `base` | GizWeb's parts. |
| Tailwind utilities | `components` | One-off changes on one element. |
| A rule outside a layer, a scoped `<style>` | all layers, whatever their specificity | The screen's own layout. |

So a screen never raises a selector or writes `!important` to restyle a part. A plain `.cols .ui-card { box-shadow: none }`
in a page beats the part.

## A screen's own style

A `.ghtml` file's `<style>` is scoped. The compiler gives each element of the file a class `g-HASH` and adds it to
each selector. A part that a component writes is not an element of your file, so a scoped rule does not reach it.
Name it with `:global(…)`:

```css
.bar > :global(.ui-menu) { margin-inline-start: auto; }
```

A rule that reaches nothing is a silent defect, so read each selector against the markup it is meant for.

Keep in the screen's style only its own layout: a grid of its regions, an order, a gap. Leave colors, type,
edges and states to the parts and tokens.

**Size by the container.** A part that sits in a pane sizes by the pane, not by the window. The agent
workspace names two containers: `chat` (the conversation) and `surface` (the open surface). A rule for a narrow
pane is a container query:

```css
@container surface (max-width: 940px) { .board { grid-template-columns: 1fr; } }
```

Use `@media` only for what is the window's: the page header and the docs menu.

## Built in: you write none of it

Every part already answers these settings. A screen made of parts gets them without one line of its own.

| Setting | What the parts do |
| --- | --- |
| Dark mode | The `dark` tokens, by the browser's scheme or `data-theme`. |
| `prefers-contrast: more` | Solid edges, no translucent glass, underlined links. |
| `forced-colors: active` | System colors that keep each shape visible. |
| `prefers-reduced-motion: reduce` | No motion that is not needed. |
| Touch (`pointer: coarse`) | 40 px buttons, and 16 px text in fields so that a phone does not zoom. |
| Right to left | Every margin, padding and position is logical (`inline-start`, `inline-end`). |
| Print | No site header, footer, docs menus or actions. |
| Keyboard | A visible `:focus-visible` ring in `--color-link`; menus and dialogs move with the arrow keys and close on Escape. |

## Add a part to GizWeb

When no part fits, add one to GizWeb, not to your app.

1. Search `web/components/ui.css` and `web/components/*.ghtml` for a part that is close. Extend it before you add
   one.
2. Write the rules in `ui.css`, after one comment line that says what the part is and its markup. Use only
   tokens. Use logical properties.
3. If the part needs more than one element, write a component in `web/components/NAME.ghtml` that takes its words
   from the caller.
4. Check it in dark mode, at 390 px, with the keyboard, and under `forced-colors`.
5. Test its markup in `tests/web/ui.test.giz` or `tests/web/controls.test.giz`, and add its row to this guide or to
   [the components reference](https://github.com/gizai/gizlang/blob/main/web/components/README.md).
6. Remove the copies that apps kept of it, in the same change.

## Review a screen

- Each section, table, list, form and button is a part.
- No raw color, size, radius or shadow. A `style=""` holds only custom properties. The compiler checks all but sizes.
- The screen's `<style>` holds only its layout, and each of its selectors reaches an element.
- Each image, button and control has a name a screen reader says (the compiler checks it).
- A pane's narrow layout is a container query.
- The screen reads well at 390 px and at 1280 px, in light and dark.

## Where to go next

- [GizWeb guide](web.md): routes, actions, forms and live updates.
- [GHTML](../reference/ghtml.md): the markup language, scoped style and Tailwind.
- [Components reference](https://github.com/gizai/gizlang/blob/main/web/components/README.md): props and markup of each component, data states and Web
  Components.
- [Web architecture](../internals/web.md): how the shell, the kernel and the parts fit together.
