# Components

> sitelo-ui: buttons, cards, forms, tables, modals and the rest, as functions that return HTML.

sitelo-ui is a component library for sitelo. Every component is a function that returns a string of HTML, so it nests straight into a [javascript-to-html](https://www.npmjs.com/package/javascript-to-html) tree with nothing in between — no compiler, no runtime, no hydration. What you build is what lands in `dist/`.

It ships with sitelo, under the `sitelo/ui` entry point.

## Quick start

`Terminal`

```bash
npm install sitelo javascript-to-html
```

Put `styles()` in the head and call components in the body. That is the whole setup:

`src/index.ht.js`

```javascript
import { body, head, html, meta, title } from 'javascript-to-html'
import { styles, container, stack, heading, text, button } from 'sitelo/ui'

export default () => html({ lang: 'en' },
  head(
    meta({ charset: 'utf-8' }),
    meta({ name: 'viewport', content: 'width=device-width, initial-scale=1' }),
    title('My site'),
    styles(),
  ),
  body(
    container({ size: 'md' },
      stack({ gap: 'md' },
        heading({ level: 1 }, 'Hello'),
        text({ variant: 'lead' }, 'A page built from components.'),
        button({ href: '/docs' }, 'Read the docs'),
      ),
    ),
  ),
)
```

Component names deliberately match what they render, which means a few of them — `button`, `input`, `table`, `link`, `code`, `select`, `progress` — collide with javascript-to-html’s element functions. Import the library as a namespace when you need both:

`src/index.ht.js`

```javascript
import * as ui from 'sitelo/ui'

ui.card(
  ui.cardHeader({ title: 'Routing', subtitle: 'File based' }),
  ui.cardBody(ui.text('src/about.ht.js becomes /about.')),
  ui.cardFooter({ divided: true }, ui.button({ size: 'sm' }, 'Read more')),
)
```

## The calling convention

Every component takes an optional props object followed by children, exactly like a javascript-to-html element. Props the component understands are consumed by name; **everything else falls through to the rendered element as an attribute**, so `id`, `data-*`, `aria-*` and event attributes work without the library having to list them:

`Pass-through props`

```javascript
button({ id: 'save', 'data-analytics': 'save-click', onclick: 'save()' }, 'Save')
// <button type="button" id="save" data-analytics="save-click" onclick="save()" class="su-btn …">
```

A prop the component does not recognise the value of — `variant: 'nonsense'` — falls back to the default rather than throwing. A cosmetic typo should not fail a build.

## Styling

`styles()` returns a `<link>` to one file, which the browser caches across pages; sitelo’s plugin serves it in dev and writes it into the build, under a content-hashed name you can serve `immutable`. `{ inline: true }` returns a `<style>` holding the whole sheet instead — around 11 kB over the wire, no extra request, and nothing that can go missing from `dist/`:

`src/index.ht.js`

```javascript
import { styles } from 'sitelo/ui'

head(
  // the whole sheet in the page — no request at all
  styles({ inline: true }),
)
```

The sheet covers every component in the library, and a site uses a handful. `pruneCss: true` in `sitelo.config.js` writes only the rules the build can match: the plugin reads the `su-` classes off the pages it has just written — and off the scripts beside them, for the classes a toast or a step adds later — and drops every rule whose selector names one that nothing carries. Decided from the output rather than the imports, so `button({ variant: 'soft' })` keeps `.su-btn--soft` and a plain `button()` does not. A linked sheet is pruned against the whole site and gets a new hash, with every `<link>` rewritten to match; an inlined one is pruned to its own page. A dozen components come to 2–3 kB gzipped. Dev serves the whole sheet; markup the build never sees, such as a server island’s, needs its classes named in `keep`:

`sitelo.config.js`

```javascript
export default {
  pruneCss: true,
  // or name classes the build never sees; `*` for a prefix
  // pruneCss: { keep: ['su-card', 'su-btn*'] },
}
```

### Theming

Everything is driven by CSS custom properties on `:root` — five palettes, a spacing scale, radii, type and shadows. `theme()` writes overrides for them, and takes camelCase names (`radiusMd` → `--su-radius-md`), palette objects, or literal custom properties:

`src/index.ht.js`

```javascript
import { styles, theme } from 'sitelo/ui'

head(
  styles(),
  // After styles(), so these win.
  theme({
    primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff' },
    radiusMd: '2px',
    fontSans: '"Inter", system-ui, sans-serif',
  }, {
    dark: { primary: { base: '#8f8ff0' } },
  }),
)
```

Dark mode resolves from `prefers-color-scheme` on its own. Setting `data-theme` or `data-su-theme` to `light` or `dark` on any ancestor overrides it — which is what `themeToggle()` does:

`src/index.ht.js`

```javascript
import { styles, themeScript, themeToggle } from 'sitelo/ui'

head(
  themeScript(), // applies the stored choice before the first paint
  styles(),
)

// …anywhere in the body
themeToggle()
```

Or change the whole look at once: `styles({ preset: 'neumorphism' })` links a preset’s sheet straight after the core one, every component follows it, and `theme()` still works on top. It runs live on the [Neumorphism](https://sitelo.dev/ui/theming/neumorphism) page.

## JavaScript, and how little of it there is

Most components need none. The modal and the drawer are `<dialog>` elements a button opens with a command, so the browser handles the backdrop, focus, Escape and a click outside. The accordion is `<details name>`. Menus are `<details>`. Tooltips are CSS. Tabs whose panels swap in place are a radio group: each tab is a `<label>`, and the panel that follows the checked radio is the one CSS shows.

Three things do want a script, and each one goes and gets its own:

- the close button on a dismissible alert
- closing a menu on an outside click or Escape
- the theme toggle

There is nothing to add to your entry file — the import is the event attribute:

`Rendered markup`

```html
<!-- rendered by alert({ dismissible: true }) -->
<button class="su-alert-dismiss"
        onclick="import('/su/alert.js').then(m=>m.dismiss(this))">
  <svg class="su-icon" …></svg>
</button>
```

sitelo serves those modules from /su/ (or /repo/su/ with --base /repo/) while you develop, and copies the ones your pages actually reference into the build. Each is well under a kilobyte, none is fetched before the first interaction, and every component renders correctly until it is: menus open and close on their own, the dismiss button does nothing.

The exception is `toast()`, because nothing on the page triggers it for you:

`src/main.js`

```javascript
// src/main.js
import { toast } from 'sitelo/ui/client'
```

## Examples

Forms wire their own labels, ids, help text and error messages:

`A form`

```javascript
import { card, cardBody, cardFooter, button, stack, textField, selectField } from 'sitelo/ui'

card(
  cardBody(
    stack({ gap: 'md' },
      textField({ label: 'Email', name: 'email', type: 'email', help: 'Never shared.' }),
      textField({ label: 'Site', name: 'site', startAdornment: 'https://', error: 'Not a URL.' }),
      selectField({ label: 'Plan', name: 'plan', options: ['Free', 'Pro'], value: 'Pro' }),
    ),
  ),
  cardFooter({ divided: true }, button({ type: 'submit' }, 'Save')),
)
```

A modal is a `<dialog>` and its trigger is any button whose commandfor points at its id:

`A modal`

```javascript
import { button, modal } from 'sitelo/ui'

button({ commandfor: 'confirm', command: 'show-modal' }, 'Delete…')

modal({
  id: 'confirm',
  title: 'Delete this page?',
  footer: button({ color: 'danger' }, 'Delete'),
}, 'This cannot be undone.')
```

Tabs come in two shapes — links, or panels:

`Tabs`

```javascript
// Link tabs: one page per tab, no script at all.
tabs({ items: [
  { label: 'Docs', href: '/docs', active: true },
  { label: 'API', href: '/api' },
] })

// Panel tabs: swap in place, on a radio group and CSS.
tabs({ value: 'use', items: [
  { id: 'install', label: 'Install', panel: code('npm install sitelo') },
  { id: 'use', label: 'Use', panel: code("import * as ui from 'sitelo/ui'") },
] })
```

Tables take `columns` and `rows`, with a `render` function wherever a cell needs more than a value:

`A table`

```javascript
table({
  striped: true,
  columns: [
    { key: 'page', header: 'Page' },
    { key: 'size', header: 'Size', align: 'end' },
    { header: 'Status', render: (row) => chip({ color: row.ok ? 'success' : 'danger' }, row.ok ? 'ok' : 'failed') },
  ],
  rows: pages,
})
```

The `examples/ui` directory in the repository renders every component on one page — it is the fastest way to see the whole set.

## Component reference

Every export, by group. Props are typed: `sitelo/ui` ships `.d.ts` files, so an editor completes `variant`, `color` and `size` for you in JavaScript as well as TypeScript.

| Group | Components |
| --- | --- |
| Layout | `container`, `stack`, `grid`, `divider`, `card`, `cardHeader`, `cardTitle`, `cardSubtitle`, `cardMedia`, `cardBody`, `cardFooter`, `aspectRatio` |
| Typography | `text`, `heading`, `link`, `code`, `inlineCode`, `kbd`, `visuallyHidden`, `prose` |
| Inputs | `button`, `iconButton`, `buttonGroup`, `field`, `input`, `textarea`, `select`, `textField`, `textareaField`, `selectField`, `checkbox`, `radio`, `toggle`, `choiceGroup`, `slider`, `sliderField`, `toggleButton`, `toggleGroup` |
| Data display | `avatar`, `avatarGroup`, `badge`, `chip`, `tooltip`, `table`, `list`, `listItem`, `figure` |
| Feedback | `alert`, `progress`, `skeleton`, `toasts`, `empty` |
| Navigation | `breadcrumbs`, `pagination`, `tabs`, `appBar`, `appBarNav`, `appBarSpacer`, `appBarActions`, `navLink`, `themeToggle` |
| Overlays | `modal`, `drawer`, `closeButton`, `menu`, `menuItem`, `menuSeparator`, `accordion`, `accordionItem`, `collapsible` |
| Sections | `hero`, `footer`, `siteFooter`, `footerColumn`, `footerBottom`, `stat`, `statGroup`, `steps`, `timeline`, `timelineItem`, `mockup` |
| Styling | `styles`, `stylesheet`, `stylesUrl`, `theme`, `themeScript` |

Two names differ from what you might expect: the switch is `toggle`, because `switch` is a reserved word and cannot be an import binding; and the styled anchor is exported as both `link` and `textLink`, so it can sit alongside javascript-to-html’s `link`. `table`, `input`, `select` and `progress` have the same escape hatch: `dataTable`, `textInput`, `selectField`, `progressBar`.

## Extras

A second entry point, `sitelo/ui-extras`, holds the components that are not for everyone — textures and effects, a film grain to start. Each brings a stylesheet of its own, `grainStyles()` beside `styles()`, so a page links only what it uses, and `sitelo/ui-extras/client` carries their page-side calls. They are catalogued at [sitelo UI extras](https://sitelo.dev/ui-extras).
