---
name: glasskit-elements
description: GlassKit Elements is a vanilla-JS Web Components library (v1.24.0) wrapping GlassKit CSS v1.24.0. It provides 36 custom elements with the `glk-` prefix, Dark/Light mode with automatic theme sync, Shadow DOM encapsulation, and form-associated custom elements. Use this reference whenever generating HTML that uses `<glk-*>` tags to ensure correct attributes, slots, events, and composition.
---

# GlassKit Elements – AI Component Reference

> **Purpose:** AI-optimized reference for generating correct `<glk-*>` markup.
> Companion to the class-based `SKILL.md` in [`@jungherz-de/glasskit`](https://github.com/JUNGHERZ/GlassKit) — the element wrappers delegate visuals and tokens to GlassKit CSS, so both references are best used together.

---

## 1. Setup & Boilerplate

### Installation

```bash
npm install @jungherz-de/glasskit-elements @jungherz-de/glasskit
```

Peer dependency `@jungherz-de/glasskit >=1.24.0` is required — 1.24.0 brings the solid surface preset behind `data-surface`, 1.23.0 lets list text wrap app-wide through tokens, 1.22.2 hands a click on a disabled control in an input affix to the field, 1.22.1 lets date and time fields shrink in narrow columns, 1.22.0 brings the input affixes behind the `prefix` / `suffix` slots, 1.21.2 gives disabled controls their look, 1.21.1 keeps the controls' state on paper, 1.21.0 brings the density tokens behind `data-density`, 1.20.0 the rules the elements build on since then (the dialog overlay, hidden list slots, empty control labels, control inputs on top); 1.9.0 is the release that made the stylesheet splittable, which is what lets document-level branding reach the elements at all.

### Import (ES modules)

```js
// Full bundle — registers all 36 elements
import '@jungherz-de/glasskit-elements';

// Named imports (for direct references to constructor classes)
import { GlkButton, GlkModal } from '@jungherz-de/glasskit-elements';

// Per-component import (since 1.7.0) — registers just this element
import '@jungherz-de/glasskit-elements/components/glk-button.js';

// …or with the constructor class
import { GlkButton } from '@jungherz-de/glasskit-elements/components/glk-button.js';
```

Every element has its own entry at `components/glk-{name}.js`; they import `./base.js` and leave `@jungherz-de/glasskit/glasskit-styles.js` external (bundler or import map resolves it). Importing one pulls that file plus `base.js` (~16 KB, shared by all of them) and GlassKit's `glasskit-styles.js` (~70 KB, resolved once by your project) rather than the 221 KB full bundle; several imports share both. Each module registers its custom element as a side effect, so a bare `import '…'` is enough.

### CDN

```html
<script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
```

Note: the elements bundle the GlassKit component rules via `adoptedStyleSheets`, and put the token defaults on the document themselves. You do **not** need to load `glasskit.css` separately — but do load it if you also use plain `.glass-*` markup outside the elements.

### Minimal template

```html
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
</head>
<body>
  <div class="glass-bg">
    <glk-title>Hello GlassKit</glk-title>
    <glk-card>
      <p>Welcome!</p>
    </glk-card>
  </div>
</body>
</html>
```

The outer `.glass-bg` class (from GlassKit CSS) still provides the aurora background. Inside the shadow roots, each element carries its own glass styling.

### Theme

```html
<html data-theme="dark">  <!-- or "light" -->
```

```js
function toggleTheme() {
  const html = document.documentElement;
  const current = html.getAttribute('data-theme') || 'dark';
  html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
}
```

A single module-level `MutationObserver` watches `data-theme` on `<html>` and syncs every live GlassKit Element instance — you never set `data-theme` on individual elements.

### Density

```html
<html data-theme="dark" data-density="compact">
```

Denser controls for admin screens, desktop layouts and narrow forms: fields and buttons 40px instead of 52 and 56, smaller toggles, checkboxes and radios, denser list rows and modal actions. Without the attribute nothing changes; `removeAttribute('data-density')` switches back at runtime. The sizes are GlassKit's density tokens (GlassKit 1.21.0 or later), so they reach every shadow root by inheritance, and a project's own values do too (`[data-density='compact'] { --gl-btn-height: 36px; }`); with `data-glk-defaults="off"` the preset comes from `glasskit.css` on the page, like every other token. The same observer mirrors `data-density` onto each element's theme wrapper — set it on `<html>` only, never on individual elements.

### Solid surfaces (since 1.24.0)

```html
<html data-theme="dark" data-surface="solid">
```

Opaque surfaces without blur and gloss, for admin screens, forms on a plain background and designs without glass: every surface in the colour the glass shows over the theme's middle background. Without the attribute nothing changes; `removeAttribute('data-surface')` switches back at runtime. It is GlassKit's solid surface preset (GlassKit 1.24.0 or later): tokens only, so it reaches every shadow root by inheritance, and a project's own values do too (`[data-surface='solid'] { --gl-surface-2: … }`); with `data-glk-defaults="off"` it comes from `glasskit.css` on the page. The observer mirrors `data-surface` onto each element's theme wrapper, like `data-theme` and `data-density`, so a rule keyed on it (a subclass's `hostStyles`) matches inside the shadow root — set it on `<html>` only.


### Printing (since 1.21.1)

Print dialogs leave out backgrounds by default; with GlassKit 1.21.1 the parts that carry a state keep their colours on paper, inside the elements too (`print-color-adjust: exact` in GlassKit's `@media print` rule — the component sheet reaches the shadow roots). A dark page prints light text and surfaces on white paper; for pages meant to be printed, switch to the light theme around printing — the elements follow `data-theme` as always:

```js
let printTheme = null;
addEventListener('beforeprint', () => {
  printTheme = document.documentElement.getAttribute('data-theme');
  document.documentElement.setAttribute('data-theme', 'light');
});
addEventListener('afterprint', () => {
  if (printTheme) document.documentElement.setAttribute('data-theme', printTheme);
  else document.documentElement.removeAttribute('data-theme');
});
```

---

## 2. Core Concepts

| Concept | Summary |
|---|---|
| Tag prefix | `glk-*` (analogous to the `glass-*` CSS prefix) |
| Rendering | Shadow DOM (`mode: 'open'`) with `adoptedStyleSheets` |
| Stylesheet sharing | GlassKit's `componentsSheet` is the same `CSSStyleSheet` object in every element — no CSS duplication. Token declarations live on the document, not in the shadow roots |
| Theme sync | Global MutationObserver on `<html data-theme>`, `<html data-density>` and `<html data-surface>` (since 1.24.0) |
| Events | Custom `glk-*` events, all `bubbles: true, composed: true` |
| Form participation | `GlkFormElement` uses `ElementInternals` (`static formAssociated = true`): value, reset and — since 1.20.0 — the inner field's validity |
| Focus | Form elements with one field delegate focus (since 1.20.0): `element.focus()`, a click on the label, a `<label for>` land in the field |
| Disabled | Form elements follow their own `disabled` and a `<fieldset disabled>` around them, as native controls do (since 1.22.3); the `disabled` property reflects the attribute |
| API style | Declarative HTML attributes + reflected JS properties |

Custom properties (`--gl-*`) defined on `:root` or `<html>` pass through shadow boundaries by inheritance, so custom theming works with a single global stylesheet.

### Building your own element (since 1.14.0)

`GlkElement` and `GlkFormElement` are exported — from the bundle (`import { GlkElement } from '@jungherz-de/glasskit-elements'`), from the stable subpath `@jungherz-de/glasskit-elements/base.js` (the very module the components import, so `instanceof GlkElement` holds across both), and as `GlassKitElements.GlkElement` from the CDN `<script>` bundle. A subclass inherits the whole setup: open shadow root with GlassKit's stylesheet adopted (`.glass-*` classes work inside), the theme wrapper following `data-theme` and `data-density`, listeners re-armed when the element moves, `emit()` for bubbling, composed events.

```js
class DemoCounter extends GlkElement {
  static get observedAttributes() { return ['count']; }
  render() {                       // build into this._wrapper
    this._btn = this.createElement('button', ['glass-btn', 'glass-btn--secondary', 'glass-btn--sm', 'glass-btn--auto']);
    this._wrapper.appendChild(this._btn);
  }
  setupEvents() {                  // runs on every connect
    this._onClick = () => { this.count += 1; this.emit('demo-count', { count: this.count }); };
    this._btn.addEventListener('click', this._onClick);
  }
  teardownEvents() { this._btn?.removeEventListener('click', this._onClick); }   // every disconnect
  onAttributeChanged() { this._btn.textContent = `Clicked ${this.count}×`; }      // after first render
  get count() { return Number(this.getAttribute('count')) || 0; }
  set count(v) { this.setAttribute('count', v); }
}
customElements.define('demo-counter', DemoCounter);
```

Which import to take depends on how the elements are loaded, and mixing them is the one mistake to avoid: the `<script>` / ESM **bundle** carries its own copy of `GlkElement`, so a subclass built on `base.js` next to it is a different class — `instanceof` fails across the two, and the GlassKit stylesheet lives twice. Bundle loaded → take `GlassKitElements.GlkElement` or `import { GlkElement } from '@jungherz-de/glasskit-elements'`. Per-component files loaded → `base.js`, which the components import themselves. The per-component files leave `@jungherz-de/glasskit/glasskit-styles.js` external, so a bundler resolves it from `node_modules` and an import-map project maps it to the one copy it already loads for its own elements.

Rules for a subclass: build only inside `this._wrapper` (it carries `data-theme`, and `data-density` while `<html>` has one); keep listeners in `setupEvents()` / `teardownEvents()`, never in `render()`, or a moved element loses them; use `emit()` instead of `dispatchEvent()` so the event bubbles and crosses the shadow boundary — `emit(name, detail, { cancelable: true })` lets a listener call `preventDefault()`, and `emit()` returns `false` then (since 1.19.0); return `true` from `static get displayInline()` for inline elements; set `static get observesLightDom()` to `true` and implement `projectLightDom()` when copying light-DOM children into the shadow tree. `GlkFormElement` adds `setFormValue()`, `setValidity()`, `resetValue()` / `restoreValue()`, a `disabled` property and `static formAssociated = true`. Override `applyDisabled(disabled)` to disable your native controls (since 1.22.3): it runs after the first render and whenever the element's own `disabled` or a `<fieldset disabled>` around it changes — no fieldset reaches into the shadow root by itself.

### Light-DOM children (since 1.12.0)

Most elements project children through a real `<slot>`, which is live by definition.
Three copy them into the shadow tree instead, because GlassKit's CSS has to reach
them: `<glk-select>` (its `<option>`s), `<glk-tab-item>` (its `<svg>`) and
`<glk-modal>` (its `[slot="actions"]` buttons).

Those copies are now kept in step by a `MutationObserver`, so swapping the children
— by hand or through a framework — updates the rendered element. Before 1.12.0 the
copy was made once on the first frame and then silently went stale.

If you ever need to force it, `element.refresh()` re-runs the copy immediately.
There should be no reason to reach into `element.shadowRoot`.

### Moving an element (since 1.12.0)

Moving a `<glk-*>` element in the DOM disconnects and reconnects it. Its event
listeners and its theme-sync registration are re-armed on every connect, so it keeps
working. Before 1.12.0 they were torn down on disconnect and never restored: the
element still looked right but no longer fired events.

An element that was out of the document while `data-theme` or `data-density` changed on `<html>` catches up when it is connected again (since 1.21.0); before, its wrapper kept the old value — and since 1.20.0 that value also sets the `color-scheme` of the native parts inside.

### Branding (since 1.9.0)

Override `--gl-*` tokens anywhere in the document — a brand stylesheet, a `<style>` block,
inline on `<html>`. Every `<glk-*>` element inherits them:

```css
/* brand.css, loaded after glasskit.css */
:root, [data-theme='dark'] { --gl-color-primary: #2e9e8f; }
[data-theme='light']       { --gl-color-primary: #21786d; }
```

No JavaScript, no `::part`, no per-element setup. Both `.glass-btn--primary` in the
document and `<glk-button variant="primary">` end up the same color.

**This did not work before 1.9.0.** Up to and including 1.8.0 every element adopted the
full GlassKit sheet, whose `[data-theme]` blocks matched the element's own theme wrapper
and re-declared all tokens inside the shadow root. A matching rule beats an inherited
value, so document-level overrides were silently ignored inside every component — while
the same override *did* apply to plain `.glass-*` markup, which made projects look
half-branded. Since 1.9.0 the elements adopt only the component rules.

**How the defaults get there.** Because the tokens no longer live in the shadow roots,
this module puts them on the document once, wrapped in `@layer glasskit-defaults`:

- Pages that never load `glasskit.css` still get styled elements, as advertised above.
- The layer loses to any ordinary stylesheet, so your brand file wins regardless of
  load order — and so does a linked `glasskit.css`, at identical values.
- It is appended to `document.adoptedStyleSheets`, never assigned, and only once even if
  several copies of the bundle are loaded.

If your app assigns `document.adoptedStyleSheets = [...]` wholesale at some later point,
re-append the existing entries rather than replacing them, or the defaults are lost.

**Only the tokens, not `color-scheme` (since 1.20.0).** GlassKit's token blocks also set `color-scheme`; on the document that switched the whole page to dark — unstyled text white, links light blue, native controls dark — on pages that never asked for GlassKit's theme. The layer now carries the `--gl-*` tokens only, and every element sets `color-scheme` on its own theme wrapper, so native parts inside it (a select's list, a date picker) match the element.

**Opting out.** The token names are global, so a component of the page that reads `var(--gl-…, fallback)` sees GlassKit's values. A page that declares the tokens itself, or wants them in part of the page only, switches the defaults off — the attribute is watched:

```html
<html data-glk-defaults="off">
```

The elements then inherit the tokens from `glasskit.css` or from a block of the page's own, for instance on a container around them.

### Passing icons (fixed in 1.10.0)

Pass an icon as a **direct child** — the `<svg>` itself, not wrapped:

```html
<glk-button><svg viewBox="0 0 24 24">…</svg>Save</glk-button>
<glk-list-item title="Download"><svg slot="leading" viewBox="0 0 24 24">…</svg></glk-list-item>
```

GlassKit sizes and strokes it. Requires GlassKit CSS >= 1.10.0: the icon rules are
descendant selectors (`.glass-btn svg`), and a slotted icon stays in the light DOM, so it
is no descendant of the shadow tree. 1.10.0 added a `::slotted()` twin to every such rule.
Before that, icons came out at their intrinsic size — `glk-button` icons filled the whole
container, list and status icons collapsed to 0×0.

**Wrapping the icon does not work and cannot be made to work:**

```html
<!-- the assigned node is the <span>; the <svg> inside is unreachable from the shadow root -->
<glk-list-item title="…"><span slot="leading"><svg …></svg></span></glk-list-item>
```

`::slotted()` only ever matches the assigned node, never inside it. If you must wrap,
size the icon yourself in your own stylesheet.

Your own rules keep precedence: for slotted content the outer tree wins over `::slotted()`
from the shadow tree, so `glk-list-item svg { width: 32px }` in your CSS still applies.
Note that `.glass-list__leading` is a fixed 28×28 flex container — enlarging only the icon
shrinks it back on the main axis; size the container too.

### CSS Parts (since 1.8.0)

Custom properties cover colors, but not layout. For that, the structural elements of the
container components are exposed as parts:

| Element | Parts |
|---|---|
| `<glk-card>` | `card` |
| `<glk-list>` | `header`, `list` |
| `<glk-modal>` | `overlay`, `modal`, `header`, `body`, `footer` |

```css
glk-card::part(card) { border-radius: 8px; }
glk-modal::part(footer) { justify-content: flex-start; }
```

Note that `height: 100%` on a part only resolves if the host gives it something definite
to resolve against. A host stretched by a grid keeps `height: auto`, which is why
`<glk-card fill>` makes the host a grid instead.

---

## 3. Element Catalog (36 elements)

### 3.1 `<glk-nav>`

Horizontal navigation bar — flex container. Typically holds `<glk-pill>` buttons.

```html
<glk-nav>
  <glk-pill label="Back" onclick="history.back()">
    <svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
  </glk-pill>
  <glk-pill label="Settings">
    <svg viewBox="0 0 24 24">...</svg>
  </glk-pill>
</glk-nav>
```

No attributes. Default slot for children.

---

### 3.2 `<glk-pill>`

Circular icon button (46×46 px).

```html
<glk-pill label="Back">
  <svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
</glk-pill>
```

| Attribute | Type | Description |
|---|---|---|
| `label` | String | `aria-label` for accessibility |
| `disabled` | Boolean | Disabled state |

Default slot: icon SVG (24×24).

Events: `glk-click` when clicked.

`aria-expanded`, `aria-haspopup` and `aria-pressed` set on the element are handed on to the native button inside (since 1.20.0) — `<glk-popover>` sets `aria-expanded` on its trigger.

---

### 3.3 `<glk-tab-bar>` + `<glk-tab-item>`

Fixed bottom navigation. Requires `.glass-bg--has-tab-bar` on the outer background to reserve padding.

```html
<div class="glass-bg glass-bg--has-tab-bar">
  <!-- page content -->
  <glk-tab-bar>
    <glk-tab-item label="Home" active>
      <svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
    </glk-tab-item>
    <glk-tab-item label="Search">
      <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/></svg>
    </glk-tab-item>
    <glk-tab-item label="Profile" badge="3">
      <svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/></svg>
    </glk-tab-item>
  </glk-tab-bar>
</div>
```

**`<glk-tab-bar>`** — container.

| Attribute | Type | Description |
|---|---|---|
| `static` | Boolean | Switches from `position: fixed` to `position: relative` (useful for docs / previews) |
| `floating` | Boolean | Pill-shaped Liquid Glass variant — adds `.glass-tab-bar--floating`. Use inside `<glk-tab-dock>` |

**`<glk-tab-item>`**

| Attribute | Type | Description |
|---|---|---|
| `label` | String | Tab label |
| `active` | Boolean | Active state — gets a soft radial Spotlight halo when inside a `floating` bar |
| `badge` | String / Number | Numeric badge on the icon |

Default slot: icon SVG. Events: `glk-tab-select` on click.

#### Floating variant + Accessory (`<glk-tab-dock>` + `<glk-tab-accessory>`)

iOS 26 Liquid Glass-style floating tab bar. Wrap a `<glk-tab-bar floating>` in a `<glk-tab-dock>`, optionally with a `<glk-tab-accessory>` (search, compose…) capsule next to it. Use `.glass-bg--has-tab-bar-floating` on the outer background to reserve padding.

```html
<div class="glass-bg glass-bg--has-tab-bar-floating">
  <!-- page content -->
  <glk-tab-dock>
    <glk-tab-bar floating>
      <glk-tab-item label="Home" active>
        <svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
      </glk-tab-item>
      <glk-tab-item label="Docs" badge="3">
        <svg viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/></svg>
      </glk-tab-item>
      <glk-tab-item label="Upload">
        <svg viewBox="0 0 24 24"><path d="M12 16V4"/><polyline points="8 8 12 4 16 8"/></svg>
      </glk-tab-item>
    </glk-tab-bar>
    <glk-tab-accessory variant="accent" label="Compose">
      <svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
    </glk-tab-accessory>
  </glk-tab-dock>
</div>
```

**`<glk-tab-dock>`** — wrapper that holds the floating bar + optional accessory.

| Attribute | Type | Description |
|---|---|---|
| `accessory-left` | Boolean | Place the accessory on the left of the bar (default is right) |

**`<glk-tab-accessory>`** — standalone glass capsule next to the bar (e.g. search, compose). 56×56 px.

| Attribute | Type | Description |
|---|---|---|
| `label` | String | `aria-label` for accessibility |
| `variant` | String | `accent`, `success`, `error` — filled colored capsule with white icon |
| `disabled` | Boolean | Disabled state |

Default slot: icon SVG (22×22). Events: `glk-click` (suppressed when disabled).

---

### 3.4 `<glk-avatar>`

Inline avatar circle.

```html
<glk-avatar size="lg" src="/user.jpg"></glk-avatar>
<glk-avatar>MJ</glk-avatar>
```

| Attribute | Type | Description |
|---|---|---|
| `size` | String | `sm`, `md` (default), `lg` |
| `src` | String | Image URL; if omitted, text content is shown as initials |

---

### 3.5 `<glk-badge>`

Inline status badge — and, with `interactive`, a filter chip.

```html
<glk-badge>Default</glk-badge>
<glk-badge variant="primary">Active</glk-badge>
<glk-badge variant="success">Done</glk-badge>
<glk-badge variant="warning">Pending</glk-badge>
<glk-badge variant="error">Failed</glk-badge>
```

| Attribute | Type | Description |
|---|---|---|
| `variant` | String | `primary`, `success`, `warning` (since 1.18.0), `error`; any other value gives the neutral badge |
| `interactive` | Boolean | Renders the badge as a real `<button>`: pointer cursor, hover tint, focus ring, keyboard operable. Enables `glk-click`. |
| `selected` | Boolean | Marks the chip that is on. Mirrored to `aria-pressed` on the button. |

Default slot: label text. Events: `glk-click` (only when `interactive`).

**Filter chips (since 1.13.0, needs GlassKit 1.12.0).** A row of chips where one is picked: listen for `glk-click` on the row and move `selected`. The element keeps `aria-pressed` in step, so the row is announced as a group of toggles.

```html
<div id="filter">
  <glk-badge interactive selected>Active</glk-badge>
  <glk-badge interactive>Applied</glk-badge>
  <glk-badge interactive>Paused</glk-badge>
</div>
<script>
  filter.addEventListener('glk-click', e => {
    for (const chip of filter.querySelectorAll('glk-badge')) chip.selected = chip === e.target;
  });
</script>
```

`selected` deepens the colour the badge already has: a plain chip turns primary, `<glk-badge variant="success" interactive selected>` stays green. `interactive` can be toggled at runtime — the element swaps between `<span>` and `<button>` underneath, the slotted label stays put. Without `interactive`, clicks do not emit `glk-click`, exactly like `<glk-list-item>`.

---

### 3.6 `<glk-card>`

Glass container for content.

```html
<glk-card>
  <p>Standard card content.</p>
</glk-card>

<glk-card glow>
  <p>Glowing card variant with light streak.</p>
</glk-card>
```

| Attribute | Type | Description |
|---|---|---|
| `glow` | Boolean | Adds frosted glass gradient + light streak |
| `fill` | Boolean | Stretches the card to the full height of its grid or flex cell (since 1.8.0); the card yields to its cell, so nowrap content is truncated instead of widening it (since 1.15.2) |

Default slot: content. CSS part: `card`.

**Equal-height tiles.** In a grid the host stretches but the visible card keeps its
natural height, so tiles end up ragged and footer buttons sit at different heights.
Add `fill`:

```html
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px">
  <glk-card fill>
    <div style="display:flex;flex-direction:column;height:100%">
      <h3>Title</h3>
      <p>Short.</p>
      <div style="margin-top:auto"><glk-button variant="primary">Action</glk-button></div>
    </div>
  </glk-card>
  <glk-card fill>…</glk-card>
</div>
```

`fill` makes the card a flex column, so `margin-top: auto` on the last child pushes a
footer down and buttons line up across tiles. Give your own wrapper `height: 100%` as
shown — otherwise it sizes to its content inside the stretched card.

**The card yields to its cell (since 1.15.2).** `fill` makes the card a grid item of the host, and a grid item's `min-width: auto` is its min-content width — a nowrap subtitle in a `<glk-list-item>` used to widen the card past its cell instead of getting its ellipsis. The card and the host now carry `min-width: 0`, and the card `min-height: 0`, so long nowrap content truncates and a scrollable child (`overflow: auto`) inside a cell of fixed height shrinks and scrolls. Content that can neither wrap nor shrink overflows the card's edge rather than pushing the card out of the cell. A plain `<glk-card>` is unchanged: as a block in a `1fr` track it keeps `min-width: auto` like any block, so give it `min-width: 0` yourself when it holds nowrap content.

---

### 3.7 `<glk-divider>`

Horizontal separator with gradient fade.

```html
<glk-divider></glk-divider>
```

No attributes, no content.

---

### 3.8 `<glk-status>`

Inline status notice (muted surface).

```html
<glk-status message="No results found"></glk-status>
```

| Attribute | Type | Description |
|---|---|---|
| `message` | String | Status text |

---

### 3.9 `<glk-title>`

Page title with text shadow.

```html
<glk-title>Settings</glk-title>
```

No attributes. Default slot: heading text.

---

### 3.10 `<glk-button>`

Full-width glass button (56 px). Three variants, four sizes.

```html
<glk-button variant="primary">Save</glk-button>
<glk-button variant="secondary">Cancel</glk-button>
<glk-button variant="tertiary">Learn more</glk-button>

<glk-button variant="primary" size="sm">Small</glk-button>
<glk-button variant="primary" size="lg">Large</glk-button>
<glk-button variant="primary" size="auto">Auto width</glk-button>

<glk-button variant="primary" disabled>Disabled</glk-button>
```

| Attribute | Type | Default | Description |
|---|---|---|---|
| `variant` | String | — | `primary`, `secondary`, `tertiary` |
| `size` | String | `md` | `sm`, `md`, `lg`, `auto` |
| `disabled` | Boolean | `false` | Disabled state |
| `type` | String | `button` | `button`, `submit`, `reset` — submit and reset act on the element's form (since 1.20.0) |

Events: `glk-click` (suppressed when disabled).

**Submitting (since 1.20.0).** The element is form-associated: `type="submit"` submits the form it sits in and `type="reset"` resets it. Before 1.20.0 both did nothing — the native button lives in the shadow root, outside every form. It acts once the click has finished bubbling, so `preventDefault()` on the click stops it, as on a native button; invalid fields stop the submit with the browser's message; the submit event's `submitter` is the element; `.form` is its form; `element.click()` acts like a click. No implicit submission: Enter in a `<glk-input>` does not submit the form.

**Upgrading from ≤ 1.19:** code that set `type="submit"` and did the work in a `glk-click` handler now gets a real submit as well — handle the form's `submit` event (with `preventDefault()` in an SPA) or drop `type="submit"`, or the browser reloads the page.

`aria-expanded`, `aria-haspopup` and `aria-pressed` set on the element are handed on to the native button inside, which is what screen readers read (since 1.20.0).

Default slot: button label (text or SVG + text).

---

### 3.11 `<glk-checkbox>`

Form-associated checkbox with glass styling.

```html
<glk-checkbox label="I agree to the terms" checked></glk-checkbox>
<glk-checkbox label="Subscribe to newsletter"></glk-checkbox>
```

| Attribute | Type | Description |
|---|---|---|
| `label` | String | Label text |
| `checked` | Boolean | Checked state |
| `disabled` | Boolean | Disabled state |
| `name` | String | Form field name |
| `value` | String | Value submitted when checked (default `"on"`) |
| `required` | Boolean | The form does not submit until it is ticked (since 1.20.0) |

Events: `glk-change` → `{ checked }`; also dispatches native `change`. Property: `.checked`.

`element.click()` and a `<label>` around the element or naming it tick it, as they do a native checkbox (since 1.20.0). A press on the element's empty width (it is a block, the control narrower) still does nothing.

---

### 3.12 `<glk-input>`

Text input with label + hint.

```html
<glk-input label="Email" type="email" placeholder="you@example.com" hint="We won't share."></glk-input>
<glk-input label="Password" type="password" required></glk-input>
<glk-input label="Broken field" error hint="This field is required."></glk-input>
```

| Attribute | Type | Description |
|---|---|---|
| `label` | String | Label above the input |
| `type` | String | Native input type (`text`, `email`, `password`, `number`, …) |
| `placeholder` | String | Placeholder text |
| `hint` | String | Helper text below the input, read as the field's description (`aria-describedby`) |
| `error` | Boolean | Error styling (red border + red hint) and `aria-invalid` on the field — it does not block the form |
| `disabled` | Boolean | Disabled state |
| `required` | Boolean | Required field — the form does not submit while it is empty (since 1.20.0) |
| `readonly` | Boolean | Read-only: focusable, selectable and submitted, not editable, barred from validation like a native field; property `.readOnly` (since 1.22.0) |
| `name` | String | Form field name |
| `value` | String | Current value |
| `min`, `max`, `step` | String | Passed to the inner field — range and step of the picker for dates, times and numbers (since 1.17.0) |
| `minlength`, `maxlength`, `pattern` | String | Passed to the inner field (since 1.17.0) |
| `autocomplete`, `inputmode` | String | Autofill and on-screen keyboard, passed to the inner field (since 1.17.0) |

Events: `glk-input` → `{ value }`, `glk-change` → `{ value }`. Native `input` / `change` also dispatched. Property: `.value`.

**Slots `prefix` and `suffix` (since 1.22.0)** put an icon, a currency, a unit or a button inside the field's box, at its start and end (they swap sides under `dir="rtl"`). Each shows only while something is slotted; its width is measured and the field's padding grows by it, so the text keeps clear of a unit of any length. A click on slotted text, an icon or a disabled control goes through to the field; enabled controls (`<glk-button>`, `<glk-select>` … included — they are form-associated), links and elements with `tabindex` take their own (since 1.22.2 — before, every slotted element took the click). Slot the control itself: one wrapped in a slotted element needs `pointer-events: auto` from the page. A container locked with `pointer-events: none` does not hold a control; lock it with `inert`, or disable the control. Their text is read as part of the field's description. Parts: `box`, `prefix`, `suffix`. Built on GlassKit's `.glass-input-wrap` (GlassKit 1.22.0).

```html
<glk-input label="Price" inputmode="decimal">
  <span slot="suffix">EUR</span>
</glk-input>
<glk-input label="Search" type="search">
  <svg slot="prefix" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
</glk-input>
```

**Validity (since 1.20.0).** The element reports its inner field's validity to the form: `required`, `pattern`, `min`/`max`, `minlength`, `type="email"` … make `form.checkValidity()` false and stop a submit at the first invalid field with the browser's own message; `reportValidity()` focuses the field. `element.validity`, `.validationMessage`, `.willValidate`, `.checkValidity()` and `.reportValidity()` answer on the element. Before 1.20.0 none of this reached the form. `error` stays a marker: a server's verdict arrives that way, and it must not block the next attempt.

**Label and focus (since 1.20.0).** The `label` is tied to the field: it names it for screen readers, and a click on it focuses it — as do `element.focus()` and a `<label for>` naming the element. An outer `<label for>` focuses, but cannot name, the field inside the shadow root; use `label` for the name.

---

### 3.13 `<glk-radio>`

Radio button. Group multiple with the same `name`.

```html
<glk-radio name="plan" label="Free Plan" value="free" checked></glk-radio>
<glk-radio name="plan" label="Pro Plan" value="pro"></glk-radio>
<glk-radio name="plan" label="Enterprise" value="enterprise"></glk-radio>
```

| Attribute | Type | Description |
|---|---|---|
| `label` | String | Label text |
| `name` | String | Group name |
| `value` | String | Value submitted when checked |
| `checked` | Boolean | Checked state |
| `disabled` | Boolean | Disabled state |
| `required` | Boolean | On any radio of the group: the group is required, any checked radio satisfies it (since 1.20.0) |

Events: `glk-change` → `{ checked, value }`.

**Grouping (since 1.11.0).** Each `<glk-radio>` keeps its `<input type="radio">` in
its own shadow root, and native radio grouping works per tree — it does not cross
shadow boundaries. The group is therefore kept by the component, following the HTML
definition: **same `name`, same containing tree, same form owner**. Selecting one
clears the others, whether by click, by arrow key, by the `checked` property or by
the `checked` attribute, and only the selected value is submitted. Two `<glk-radio>`
in two different `<form>`s are two groups, and a radio without `name` is not grouped.
If several carry `checked` in the markup, the last one wins — as with native radios.

Keyboard: the group is a single tab stop. <kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd>
<kbd>→</kbd> move through it, wrap around, skip disabled entries and select as they
go, firing `glk-change` and `change` exactly as a click does.

Not covered: a screen reader still announces the entries as separate radios, not as
"1 of 3" — that needs a `role="radiogroup"` container, which no `<glk-*>` element
provides. Wrap them yourself if you need it:

```html
<div role="radiogroup" aria-label="Plan">
  <glk-radio name="plan" label="Free Plan" value="free" checked></glk-radio>
  <glk-radio name="plan" label="Pro Plan" value="pro"></glk-radio>
</div>
```

---

### 3.14 `<glk-range>`

Slider input.

```html
<glk-range label="Volume" min="0" max="100" value="65" step="5"></glk-range>
```

| Attribute | Type | Description |
|---|---|---|
| `label` | String | Label text |
| `min` | Number | Minimum value |
| `max` | Number | Maximum value |
| `value` | Number | Current value |
| `step` | Number | Step size |
| `name` | String | Form field name |
| `disabled` | Boolean | Disabled state |

Events: `glk-input`, `glk-change`. Properties: `.value`, `.disabled` (since 1.22.3).

---

### 3.15 `<glk-search>`

Search input with leading icon.

```html
<glk-search placeholder="Search components..."></glk-search>
```

| Attribute | Type | Description |
|---|---|---|
| `placeholder` | String | Placeholder text |
| `label` | String | Accessible name of the field — it has no visible label (since 1.20.0) |
| `value` | String | Current value |
| `name` | String | Form field name |
| `disabled` | Boolean | Disabled state |

Events: `glk-input`, `glk-change`. Properties: `.value`, `.disabled` (since 1.22.3).

---

### 3.16 `<glk-select>`

Dropdown select. Pass native `<option>` elements as children.

```html
<glk-select label="Category" name="category">
  <option value="">Please select…</option>
  <option value="1">Insurance</option>
  <option value="2">Rental</option>
  <option value="3">Employment</option>
</glk-select>
```

| Attribute | Type | Description |
|---|---|---|
| `label` | String | Label text, tied to the field |
| `name` | String | Form field name |
| `value` | String | Selected value |
| `disabled` | Boolean | Disabled state |
| `required` | Boolean | With an empty first option (`value=""`), the form does not submit until a choice is made (since 1.20.0) |

Children: native `<option>` elements, read from the light DOM.

Events: `glk-change` → `{ value }`.

**Changing the options later (since 1.12.0).** The options are copied into the
shadow tree, and the copy is kept in step: replace, add or remove `<option>`
children at any time — by hand or through a framework — and the rendered select
follows. Before 1.12.0 the copy was made once and then silently went stale.

The selection survives a rebuild: if the selected value is still in the new list
it stays selected, otherwise the `value` attribute decides, otherwise the browser
falls back to the first option. The **empty string is a real value** throughout —
`value=""` selects `<option value="">` wherever it sits in the list, not just when
it happens to be first.

```html
<!-- "" is a genuine choice here, and it is honoured -->
<glk-select label="Language" value="">
  <option value="de">Deutsch</option>
  <option value="">Detect automatically</option>
</glk-select>
```

A `value` naming no existing option leaves the current selection alone rather than
clearing it — when the matching option arrives later, it is selected then.

**Set as a property (since 1.22.0).** The same holds for `select.value = 'b'`: a value set before the element is connected, before its options are copied in, or before the options exist at all is kept as the wanted value and selected as soon as an option carries it — until the user picks another, which replaces it. `select.value` reads it back meanwhile. Before 1.22.0 such a value threw before connecting and was lost afterwards — the select showed its first option while the app held the other value. A form reset returns to the `value` attribute.

---

### 3.17 `<glk-textarea>`

Multi-line text input.

```html
<glk-textarea label="Message" rows="4" placeholder="Type your message..."></glk-textarea>
```

| Attribute | Type | Description |
|---|---|---|
| `label` | String | Label text |
| `rows` | Number | Visible rows |
| `placeholder` | String | Placeholder text |
| `name` | String | Form field name |
| `value` | String | Current value |
| `disabled` | Boolean | Disabled state |
| `required` | Boolean | Required field |
| `readonly` | Boolean | Read-only, property `.readOnly` (since 1.22.0) |

Events: `glk-input`, `glk-change`.

---

### 3.18 `<glk-toggle>`

Switch-style toggle (form-associated).

```html
<glk-toggle label="Enable notifications" checked></glk-toggle>
<glk-toggle label="Dark mode"></glk-toggle>
<glk-toggle label="Disabled toggle" disabled></glk-toggle>
```

| Attribute | Type | Description |
|---|---|---|
| `label` | String | Label text |
| `checked` | Boolean | Checked state |
| `disabled` | Boolean | Disabled state |
| `name` | String | Form field name |
| `value` | String | Submitted value when checked (default `"on"`) |
| `required` | Boolean | Must be on before the form submits (since 1.20.0) |

Events: `glk-change` → `{ checked }`. Property: `.checked`.

**One control (since 1.20.0).** The switch is the native checkbox inside, with `role="switch"`, named by `label`. The element itself no longer carries `role`, `aria-checked` or `aria-disabled` — before 1.20.0 screen readers met a switch with a checkbox inside. Select and style on `[checked]` / `[disabled]`; in tests, `getByRole('switch', { name })` finds the input and `.click()`, `.check()` work on it. `element.click()` and a `<label>` around the element or naming it toggle it, like a native checkbox. Without `label` the element is exactly its track, 52 × 30 px (it was 64 × 34).

---

### 3.19 `<glk-modal>`

Modal dialog with overlay, title, body, and footer actions.

```html
<glk-button variant="secondary" size="auto"
  onclick="document.getElementById('confirm').show()">
  Open
</glk-button>

<glk-modal id="confirm" title="Delete contract?">
  <p>This action cannot be undone.</p>
  <div slot="actions">
    <button class="glass-modal__action" onclick="document.getElementById('confirm').close()">Cancel</button>
    <button class="glass-modal__action glass-modal__action--danger"
            onclick="deleteContract(); document.getElementById('confirm').close()">Delete</button>
  </div>
</glk-modal>
```

| Attribute | Type | Description |
|---|---|---|
| `open` | Boolean | Visibility state (reflected as `.is-active` on the overlay) |
| `title` | String | Header title; names the dialog |

Slots:
- default — modal body content
- `actions` — container for footer buttons (GlassKit expects `<button class="glass-modal__action">`)

Methods: `.show()`, `.close()`. Property: `.open`. Events: `glk-close`.

Closes automatically on overlay click and <kbd>Escape</kbd>, with its fade, and emits `glk-close` then; `.close()` and removing `open` do not emit.

**A native `<dialog>` (since 1.20.0).** The overlay is opened with `showModal()`: it lies in the top layer, the page behind it is inert for pointer, keyboard and screen readers, focus moves in and returns to where it came from on close, and the content is out of reach while closed. Chrome focuses the first field of the content, Safari the first action — put `autofocus` on the element that should get the focus, in the content or on an action.

While it is open, **everything outside is inert** — a `<glk-toast>` shown then is neither clickable nor announced. Give feedback inside the modal, or close it first.

---

### 3.20 `<glk-popover>`

Anchored dropdown / menu container. Wraps a trigger element (`slot="trigger"`) and floating content. The element handles `.is-open` toggling, outside-click dismiss, and <kbd>Escape</kbd>-key close internally.

```html
<glk-popover placement="end">
  <glk-button slot="trigger" variant="secondary" size="auto">Open menu</glk-button>
  <glk-list bare>
    <glk-list-item title="Share" interactive></glk-list-item>
    <glk-list-item title="Duplicate" interactive></glk-list-item>
    <glk-list-item title="Delete" variant="danger" interactive></glk-list-item>
  </glk-list>
</glk-popover>
```

| Attribute | Type | Default | Description |
|---|---|---|---|
| `open` | Boolean | `false` | Visibility state |
| `placement` | String | `bottom` | `top`, `bottom`, `start`, `end` |

Slots:
- `trigger` — element that toggles the popover on click
- default — floating content shown while open

Methods: `.show()`, `.close()`, `.toggle()`. Property: `.open`.
Events: `glk-open`, `glk-close`.

The trigger gets `aria-expanded`, kept in step with `open` and set again when the trigger is swapped (since 1.20.0); `<glk-button>` and `<glk-pill>` hand it on to their native button.

**Naming warning:** the toggle method is deliberately called `.toggle()`, **not** `.togglePopover()` — the latter collides with the native `HTMLElement.togglePopover()` API from the HTML Popover specification.

---

### 3.21 `<glk-progress>`

Progress bar.

```html
<glk-progress label="Upload" value="75" variant="success"></glk-progress>
<glk-progress label="Storage" value="90" variant="error" size="lg"></glk-progress>
<glk-progress label="CPU" value="45" size="sm"></glk-progress>
```

| Attribute | Type | Description |
|---|---|---|
| `label` | String | Label text shown left of the bar |
| `value` | Number | 0–100 percentage |
| `variant` | String | `success`, `error` |
| `size` | String | `sm`, `md` (default), `lg` |

---

### 3.22 `<glk-toast>`

Notification at the top of the screen that hides itself (shown imperatively) — and, since 1.19.0, one that offers something: "A new version · Reload".

```html
<glk-toast id="toast"></glk-toast>

<script>
  toast.show('Saved successfully!', 'success', 3000);

  // An offer: a button and an ×, stays until one of them is used
  toast.show('A new version is ready', { action: { label: 'Reload', value: 'reload' } });
  toast.addEventListener('glk-action', (e) => { if (e.detail.action === 'reload') location.reload(); });
  toast.addEventListener('glk-close', () => { /* dismissed with × or Escape */ });
</script>
```

| Attribute | Type | Description |
|---|---|---|
| `message` | String | The text |
| `variant` | String | `success`, `error`, `warning` — colours the icon and the action |
| `duration` | Number | Auto-dismiss time in ms (default `3000`); `0` keeps it up. With an action there is no timeout unless `duration` is set |
| `visible` | Boolean | Shown |
| `action-label`, `action-value` | String | The action button, and the value `glk-action` reports (the label when missing) (since 1.19.0) |
| `dismissible` | Boolean | An × without an action; an action always brings one (since 1.19.0) |
| `close-label` | String | Name of the ×, `"Close"` by default (since 1.19.0) |

Methods: `.show(message, variant, duration)`, `.show(message, { variant, duration, action: { label, value }, dismissible })`, `.dismiss()`. In the options form, left-out keys keep their attribute, except `action`: a message without one shows no button, and one with an action but no `duration` stays. Calling `show()` again while the toast is up restarts its timer.

Slot: `icon` — replaces the built-in icon; leave its stroke unset and it takes the variant colour.

Events: `glk-action { action, label }` — the action was used; the toast closes unless a listener calls `preventDefault()` or shows the next message (`show('Restored')`) from the handler. `glk-close` — closed with the × or Escape. `glk-dismiss` — timed out.

While the pointer or the focus is on a toast with buttons, it does not time out. The toast is a polite live region (`role="status"`), so the message is announced; hidden, its buttons are inert, and closing it from the keyboard hands the focus back. Where it sits comes from GlassKit's `--gl-toast-top` — set it on an ancestor to place the toast below your header (with `viewport-fit=cover`, add `env(safe-area-inset-top)`).

---

### 3.23 `<glk-accordion>` + `<glk-accordion-item>`

Collapsible sections.

```html
<glk-accordion>
  <glk-accordion-item title="What is GlassKit?" open>
    A glassmorphism CSS component library.
  </glk-accordion-item>
  <glk-accordion-item title="Do I need a framework?">
    No — vanilla HTML works.
  </glk-accordion-item>
</glk-accordion>
```

**`<glk-accordion>`** — container, no attributes.

**`<glk-accordion-item>`**

| Attribute | Type | Description |
|---|---|---|
| `title` | String | Trigger label |
| `open` | Boolean | Expanded state |

Default slot: body content. Events: `glk-toggle` → `{ open }`. Property: `.open`.

The trigger carries `aria-expanded` and `aria-controls`; a closed section is `inert`, so its links and fields are out of the tab order and the accessibility tree until it opens (since 1.20.0 — before, they were only squeezed to zero height and Tab still reached them).

---

### 3.24 `<glk-list>` + `<glk-list-item>`

iOS-style grouped settings list. Items carry a leading icon, title + optional subtitle, and a trailing element. Dividers between items are drawn automatically by GlassKit CSS — **never** add `<hr>` or divider markup manually. Supports section headers, large icons (40×40), multi-line subtitles, trailing value text, and semantic `danger`/`accent` variants.

#### Settings-style list with section header and large icons

```html
<glk-list header="Recommendations">
  <glk-list-item title="Review downloaded media" subtitle="Save up to 1.38 GB. Review files and remove as needed." leading-lg wrap interactive>
    <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
      <polyline points="7 10 12 15 17 10"/>
      <line x1="12" y1="15" x2="12" y2="3"/>
    </svg>
    <svg slot="trailing" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <polyline points="9 18 15 12 9 6"/>
    </svg>
  </glk-list-item>
  <glk-list-item title="AusweisApp Bund" subtitle="Version 2.5.1" leading-lg detail="24 MB" interactive>
    <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <rect x="2" y="3" width="20" height="18" rx="2"/>
      <path d="M9 3v18"/><path d="M15 3v18"/>
    </svg>
    <svg slot="trailing" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <polyline points="9 18 15 12 9 6"/>
    </svg>
  </glk-list-item>
  <glk-list-item title="View all subscriptions" variant="accent" center interactive></glk-list-item>
</glk-list>
```

#### Compact menu with danger variant

```html
<glk-list>
  <glk-list-item title="Share" center interactive></glk-list-item>
  <glk-list-item title="Duplicate" center interactive></glk-list-item>
  <glk-list-item title="Delete" variant="danger" center interactive></glk-list-item>
</glk-list>
```

#### Attributes

**`<glk-list>`**

| Attribute | Type | Description |
|---|---|---|
| `header` | String | Section header text — renders as uppercase label above the list |
| `flush` | Boolean | Edge-to-edge variant — removes side margin and radius |
| `bare` | Boolean | Strips background, border, shadow — use inside `<glk-popover>` or `<glk-card>` to avoid double glass surfaces |

**`<glk-list-item>`**

| Attribute | Type | Description |
|---|---|---|
| `title` | String | Primary text (medium weight, truncated with ellipsis) |
| `subtitle` | String | Secondary text (muted, small, truncated) |
| `interactive` | Boolean | Enables hover / focus / active states and `glk-click` event |
| `center` | Boolean | Centered single-text variant (for action rows like "Reset to defaults") |
| `leading-lg` | Boolean | Large 40×40 leading icon with rounded corners (for app icons; use 32×32 SVG or `<img>`) |
| `wrap` | Boolean | Multi-line subtitle — allows up to 3 lines with ellipsis (requires `subtitle`) |
| `detail` | String | Muted trailing value text (e.g. file size "24 MB", version number) — rendered before `slot="trailing"` |
| `variant` | String | Semantic color: `"danger"` (red destructive) or `"accent"` (primary color). Inherits to title and leading icon |

#### Slots (on `<glk-list-item>`)

| Slot | Purpose |
|---|---|
| `leading` | Icon slot (24×24 SVG recommended, or 32×32 with `leading-lg`; use `stroke: currentColor`) |
| `trailing` | Chevron, badge, or button — rendered after `detail` value |

#### Wrapping text (since 1.23.0)

Title and subtitle keep to one line, and `wrap` clamps the subtitle at three lines. GlassKit's list tokens change that for a whole app, one list or one row; they are inherited into the element's shadow root: `--gl-list-title-white-space` and `--gl-list-subtitle-white-space` (`nowrap` by default, `normal` wraps) and `--gl-list-subtitle-lines` (the clamp of `wrap`, `3` unless set, `none` = every line). A word wider than the column (an e-mail address) breaks instead of running out of the row. For breaks at syllables set `hyphens: auto; -webkit-hyphens: auto` on `glk-list` or the page; it is inherited too.

```css
:root {
  --gl-list-title-white-space: normal;
  --gl-list-subtitle-white-space: normal;
  --gl-list-subtitle-lines: none;
}
```

#### Events

`<glk-list-item>` emits `glk-click` when clicked, but only if `interactive` is set.

#### Internals (for framework authors)

Both elements use pure Shadow DOM with slot projection — no child node cloning — so they compose cleanly inside lit-html, HybridsJS, React, Vue, and Svelte templates. `<glk-list>` observes its direct children via `MutationObserver` and sets a `data-last` attribute on the final `<glk-list-item>`; each item's shadow carries a one-line override sheet (`:host([data-last]) .glass-list__item::after { content: none; }`) to suppress the auto-divider on the last row. Divider inset automatically adjusts for `leading-lg` via CSS `:has()`. Zero CSS duplication.

---

### 3.25 `<glk-segmented>`

A small, exclusive choice as one control (since 1.15.0) — traffic light, morning / afternoon, mode. One `<button>` per option, the chosen one marked `aria-pressed="true"` (GlassKit styles exactly that attribute); a value change only re-sets the attribute, never rebuilds, so focus stays on the pressed button. Form-associated (`GlkFormElement`): a surrounding `<form>` receives `name=value`, reset restores the initial value.

```html
<glk-segmented full name="status" label="Status" value="y"
  options='[{"value":"g","label":"Green","tone":"success"},{"value":"y","label":"Yellow","tone":"warning"},{"value":"r","label":"Red","tone":"error"},{"value":"x","label":"Locked","disabled":true}]'>
</glk-segmented>
```

| Attribute | Type | Description |
|---|---|---|
| `options` | JSON | `[{ value, label, tone?, disabled? }]`; `tone` is `success` \| `warning` \| `error` and puts a dot in that colour before the label |
| `value` | String | Value of the chosen option (reflected property) |
| `full` | Boolean | Buttons share the width |
| `overflow` | String | What happens when the options do not fit: `scroll` — one row that scrolls sideways, the chosen option kept in view; `wrap` — breaks into lines. Without it the row stays one line (since 1.17.0) |
| `label` | String | `aria-label` of the group (no default — set it) |
| `name` | String | Form field name |
| `disabled` | Boolean | The whole row (since 1.22.3); a `<fieldset disabled>` around it disables it too |

Properties: `value`, `options` (array or JSON text), `full`, `overflow`, `label`, `disabled`. Events: `glk-change` `{ value }` — only on a change made by the user, not on `.value = …`. Part: `group`.

With `overflow="scroll"` the chosen option is centred in the row after the first layout, on every value change and when the row's width changes — the row scrolls, never the page. Seven areas on a phone:

```html
<glk-segmented full overflow="scroll" label="Area" value="g"
  options='[{"value":"a","label":"General"},{"value":"b","label":"Hours"},…,{"value":"g","label":"Privacy"}]'></glk-segmented>
```

The attribute is `overflow`, not `scroll`: `scroll` is a method of every element, and a framework that sets a property whenever the element has one (hybrids, Vue, React 19) would overwrite that method instead of setting the attribute.

---

### 3.26 `<glk-steps>`

Progress through a short flow (since 1.15.0), purely presentational: the current step carries `aria-current="step"`, done steps show an SVG check. The narrowing in tight frames is the CSS block's container query (below 360 px only the current label stays) — the element knows nothing about it, so in a flex row give it a width.

```html
<glk-steps steps="Day,Slot,Dog,Confirm" current="2" label="Progress"></glk-steps>
```

| Attribute | Type | Description |
|---|---|---|
| `steps` | String | Comma-separated labels (property also accepts an array) |
| `current` | Number | 0-based index of the current step (reflected property) |
| `label` | String | `aria-label` of the list, optional |

Part: `steps`.

---

### 3.27 `<glk-sheet>`

Bottom sheet (since 1.15.0) — the mobile sibling of `<glk-modal>`, same API shape. Opening and closing are animated by GlassKit's `.glass-sheet` block: `.is-active` is added one frame after the overlay is unhidden, and on closing the overlay is hidden only after `transitionend` (safety net 400 ms), so no blurred layer idles behind the page; under `prefers-reduced-motion` it switches at once. The Escape listener sits on `document` and is removed in `teardownEvents()`.

```html
<glk-sheet id="sheet" title="Rebook to …">
  <p>Content: a list, text, a form.</p>
  <glk-button slot="actions" variant="secondary" onclick="sheet.close()">Close</glk-button>
</glk-sheet>
<script>sheet.show();</script>
```

| Attribute | Type | Description |
|---|---|---|
| `open` | Boolean | Shown as an overlay (reflected property) |
| `inline` | Boolean | In the flow, no overlay, always visible — e.g. inside a phone frame or a card |
| `title` | String | Heading and `aria-label` of the dialog |

Methods: `show()`, `close()`. Slots: default (content), `actions`. Events: `glk-close` — only when the user closes it (scrim click, Escape), like `<glk-modal>`; `close()` does not emit. Parts: `overlay`, `sheet`, `title`, `body`, `actions`.

---

### 3.28 `<glk-empty>`

Empty state for lists and result pages (since 1.15.0): icon plate, title, short muted text and room for one action. An empty `title` or `text` hides its element instead of leaving an empty paragraph.

```html
<glk-empty title="No bookings yet" text="Pick a day and a dog — the team is looking forward to it.">
  <svg viewBox="0 0 24 24">…</svg>
  <glk-button slot="action" variant="primary" size="sm">Book</glk-button>
</glk-empty>
```

| Attribute | Type | Description |
|---|---|---|
| `title` | String | Heading; hidden when empty |
| `text` | String | Muted text; hidden when empty |

Slots: default (an `<svg>` icon, 24 px stroked; a plain circle as fallback), `action`. Parts: `empty`, `icon`, `title`, `text`, `action`.

### 3.29 `<glk-date-strip>`

A row of day chips that scrolls sideways (since 1.16.0) — a booking horizon, the days around today. Chips are buttons in a `role="group"`, the chosen one `aria-pressed="true"`, each named with its full date from `Intl` for the `locale`. One tab stop: arrow keys move between chips, Home/End jump to the ends, Enter or Space picks. The chosen chip is centred in the strip after the first layout and on every value change; only the strip scrolls, never the page. A value change re-sets state; `start`, `days`, `today`, `marks` and `locale` rebuild the chips.

```html
<glk-date-strip start="2026-09-22" days="29" value="2026-10-11" label="Pick a day"
  marks='{"2026-09-22":"success","2026-09-25":{"tone":"error","disabled":true}}'></glk-date-strip>
```

| Attribute | Type | Description |
|---|---|---|
| `start` | String | First day `YYYY-MM-DD`; today when missing |
| `days` | Number | Number of chips (default 7) |
| `value` | String | Chosen day `YYYY-MM-DD` |
| `today` | String | The underlined day; the browser's day when missing |
| `marks` | JSON | `{ date: tone \| { tone, disabled } }` — tone `primary` / `success` / `warning` / `error`; `disabled` makes the chip unpickable |
| `locale` | String | BCP 47 tag for the names; without it the page language — the `lang` of the nearest ancestor, usually `<html lang>`, also across shadow roots — else the browser language (page language since 1.19.2) |
| `label` | String | `aria-label` of the group |

Event: `glk-change { value }` — only on a change made by the user. Properties: `value`, `marks` (object or JSON text), `locale`, `label`. Part: `strip`.

---

### 3.30 `<glk-calendar>`

One month with a day to pick (since 1.16.0). The days are buttons in a `role="group"`, the chosen one `aria-pressed="true"`, each named with its full date; month title, weekday names and the first day of the week come from `Intl` for the `locale` (`week-start` overrides; Monday where a browser has no week info). One tab stop: arrows move by a day or a week, Home/End to the ends of the week, PageUp/PageDown by a month — past the month's edge the calendar shows that month and emits `glk-month`. Arrows only move focus; Enter or Space picks. Days outside `min`/`max` carry `aria-disabled="true"`: in the arrow path, announced, never picked. A value set to another month flips the calendar there. Form-associated (`GlkFormElement`): a surrounding `<form>` receives `name=value`, reset restores the initial value.

```html
<glk-calendar name="day" value="2026-09-22" min="2026-09-22" label="Pick a day"
  marks='{"2026-09-24":["error"],"2026-09-27":["success","warning"]}'></glk-calendar>
```

| Attribute | Type | Description |
|---|---|---|
| `month` | String | Shown month `YYYY-MM`; without it the month of the value, else of today; reflected when the user navigates |
| `value` | String | Chosen day `YYYY-MM-DD` |
| `today` | String | The day with the warm border; the browser's day when missing |
| `min`, `max` | String | Days outside cannot be picked |
| `marks` | JSON | `{ date: tone \| [tone, …] }` — up to three tones per day |
| `locale` | String | BCP 47 tag for names and week start; without it the page language — the `lang` of the nearest ancestor, usually `<html lang>`, also across shadow roots — else the browser language (page language since 1.19.2) |
| `week-start` | Number | 0 = Sunday … 6 = Saturday; from the locale when missing |
| `label` | String | `aria-label` of the day group |
| `prev-label`, `next-label` | String | Names of the nav buttons ("Previous month" / "Next month") |
| `name` | String | Form field name |
| `disabled` | Boolean | Arrows and days (since 1.22.3); a `<fieldset disabled>` around it disables it too |

Events: `glk-change { value }` on a pick by the user; `glk-month { month }` when the user moves to another month. Properties: `month`, `value`, `marks`, `locale`, `label`, `disabled`. Parts: `calendar`, `head`, `title`, `grid`.

---

### 3.31 `<glk-image-picker>`

One image with a preview, resized on the client before it goes anywhere (since 1.16.0). Choose opens the file dialog from a real button; the file is decoded with `createImageBitmap` (`imageOrientation: 'from-image'`, so a phone photo comes out upright), drawn onto a canvas no larger than `max` and handed out as a data URL in `glk-change`. The `src` property holds it and is not reflected, so a data URL of megabytes never lands in the DOM. A file the browser cannot decode — Chrome and HEIC, a corrupt file — emits `glk-error`. Not form-associated: listen to `glk-change` and upload, or copy the data URL into a hidden field.

```html
<glk-image-picker id="photo" round max="512" label="Profile photo" hint="JPG or PNG"></glk-image-picker>
<script>photo.addEventListener('glk-change', e => upload(e.detail.dataUrl));</script>
```

| Attribute | Type | Description |
|---|---|---|
| `src` | String | Starting image: URL or data URL |
| `label` | String | Visible label; names the group |
| `hint` | String | Small muted line; none by default |
| `round` | Boolean | Circular preview, for avatars |
| `max` | Number | Longest edge in px after resizing (default 1024) |
| `type`, `quality` | String, Number | Output MIME (default `image/jpeg`; `image/webp` where the browser encodes it, PNG otherwise), quality 0–1 (default 0.82) |
| `accept` | String | File dialog filter (default `image/*`) |
| `choose-label`, `change-label`, `remove-label` | String | Button texts ("Choose", "Change", "Remove") |

Events: `glk-change { dataUrl, width, height, size }` after a pick, on remove with an empty `dataUrl` and zeros; `glk-error { message, name }` when the file cannot be decoded. Property: `src` (not reflected). Parts: `picker`, `preview`, `meta`, `label`, `hint`, `actions`.

---

## 4. Composition Patterns

### Login Screen

```html
<div class="glass-bg">
  <glk-nav>
    <glk-pill label="Back" onclick="history.back()">
      <svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
    </glk-pill>
    <glk-pill label="Toggle theme" onclick="toggleTheme()">
      <svg class="icon-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
    </glk-pill>
  </glk-nav>

  <glk-title>Sign In</glk-title>

  <form>
    <glk-input label="Email" type="email" name="email" placeholder="you@example.com" required></glk-input>
    <glk-input label="Password" type="password" name="password" required></glk-input>

    <glk-button variant="primary" type="submit">Log In</glk-button>
    <glk-button variant="tertiary">Forgot password?</glk-button>
  </form>
</div>
```

### Dashboard with Cards + Tab Bar

```html
<div class="glass-bg glass-bg--has-tab-bar">
  <glk-nav>
    <glk-title>Dashboard</glk-title>
  </glk-nav>

  <glk-card glow>
    <p>Capture and upload documents.</p>
  </glk-card>
  <glk-card glow>
    <p>Manage and archive contracts.</p>
  </glk-card>

  <glk-tab-bar>
    <glk-tab-item label="Home" active>
      <svg viewBox="0 0 24 24">...</svg>
    </glk-tab-item>
    <glk-tab-item label="Contracts" badge="3">
      <svg viewBox="0 0 24 24">...</svg>
    </glk-tab-item>
    <glk-tab-item label="Profile">
      <svg viewBox="0 0 24 24">...</svg>
    </glk-tab-item>
  </glk-tab-bar>
</div>
```

### Form Page with Validation

```html
<form id="contract-form">
  <glk-title>New Contract</glk-title>

  <!-- required stops the submit with the browser's message (since 1.20.0) -->
  <glk-input label="Contract Name" name="name" required></glk-input>
  <glk-select label="Category" name="category" required>
    <option value="">Please select…</option>
    <option value="insurance">Insurance</option>
    <option value="rental">Rental</option>
  </glk-select>
  <glk-textarea label="Notes" name="notes" rows="4"></glk-textarea>
  <glk-toggle label="Enable reminder" name="reminder"></glk-toggle>

  <glk-button variant="primary" type="submit">Save</glk-button>
</form>

<script>
  document.getElementById('contract-form').addEventListener('submit', (e) => {
    e.preventDefault();
    const data = new FormData(e.target);
    console.log(Object.fromEntries(data));  // { name, category, notes, reminder? }
  });
</script>
```

### Delete Confirmation Modal

```html
<glk-button variant="secondary" size="auto"
  onclick="document.getElementById('confirm').show()">
  Delete contract
</glk-button>

<glk-modal id="confirm" title="Delete contract?">
  <p>This action cannot be undone.</p>
  <div slot="actions">
    <button class="glass-modal__action" onclick="document.getElementById('confirm').close()">Cancel</button>
    <button class="glass-modal__action glass-modal__action--danger"
            onclick="handleDelete(); document.getElementById('confirm').close()">Delete</button>
  </div>
</glk-modal>
```

### iOS-style Settings Screen (List + Popover)

Full example reproducing the native iOS Settings layout with two grouped lists and an inline action menu.

```html
<div class="glass-bg">
  <glk-nav>
    <glk-pill label="Back" onclick="history.back()">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
    </glk-pill>
    <glk-title>Settings</glk-title>

    <glk-popover placement="end">
      <glk-pill slot="trigger" label="More">
        <svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
      </glk-pill>
      <glk-list bare>
        <glk-list-item title="Edit profile" interactive></glk-list-item>
        <glk-list-item title="Sign out" interactive></glk-list-item>
      </glk-list>
    </glk-popover>
  </glk-nav>

  <glk-list header="Account">
    <glk-list-item title="Marcel Jungherz" subtitle="marcel@jungherz.com" leading-lg interactive>
      <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
      <svg slot="trailing" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
    </glk-list-item>
    <glk-list-item title="Subscriptions" interactive detail="$34.94">
      <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></svg>
      <svg slot="trailing" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
    </glk-list-item>
  </glk-list>

  <glk-list header="Notifications">
    <glk-list-item title="Push notifications" interactive>
      <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
      <span slot="trailing">On</span>
    </glk-list-item>
    <glk-list-item title="Email digest" interactive>
      <svg slot="leading" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22 6 12 13 2 6"/></svg>
      <span slot="trailing">Weekly</span>
    </glk-list-item>
    <glk-list-item title="Reset to defaults" variant="danger" center interactive></glk-list-item>
  </glk-list>
</div>
```

Key points:
- Section headers via `header` attribute — no need for external `<p>` labels
- Large leading icon via `leading-lg` for profile-style rows
- Trailing value via `detail` attribute — muted metadata text before the chevron
- Auto-dividers — no manual `<hr>` between rows; inset adapts for large icons automatically
- `variant="danger"` on destructive "Reset" row — red text signals caution
- Popover anchored in nav — `placement="end"` aligns the menu to the right edge of the trigger
- Bare list inside popover — `<glk-list bare>` strips the inner glass surface so only the popover's glass shows

---

## 5. State & Event Overview

| Element | Key Property | HTML Attribute | Event(s) | Detail |
|---|---|---|---|---|
| `<glk-button>` | — | `disabled` | `glk-click` | — |
| `<glk-pill>` | — | `disabled` | `glk-click` | — |
| `<glk-tab-item>` | — | `active` | `glk-tab-select` | — |
| `<glk-tab-accessory>` | — | `disabled` | `glk-click` | — |
| `<glk-input>` | `.value` | `value` | `glk-input`, `glk-change` (+ native `input`, `change`) | `{ value }` |
| `<glk-textarea>` | `.value` | `value` | `glk-input`, `glk-change` | `{ value }` |
| `<glk-select>` | `.value` | `value` | `glk-change` | `{ value }` |
| `<glk-search>` | `.value` | `value` | `glk-input`, `glk-change` | `{ value }` |
| `<glk-toggle>` | `.checked` | `checked` | `glk-change` (+ native `change`) | `{ checked }` |
| `<glk-checkbox>` | `.checked` | `checked` | `glk-change` | `{ checked }` |
| `<glk-radio>` | `.checked` | `checked` | `glk-change` | `{ checked, value }` |
| `<glk-range>` | `.value` | `value` | `glk-input`, `glk-change` | `{ value }` |
| `<glk-modal>` | `.open` | `open` | `glk-close` | — |
| `<glk-popover>` | `.open` | `open` | `glk-open`, `glk-close` | — |
| `<glk-accordion-item>` | `.open` | `open` | `glk-toggle` | `{ open }` |
| `<glk-list-item>` | — | `interactive` | `glk-click` (only when interactive) | — |
| `<glk-segmented>` | `.value` | `value` | `glk-change` | `{ value }` |
| `<glk-sheet>` | `.open` | `open` | `glk-close` (user close only) | — |
| `<glk-date-strip>` | `.value` | `value` | `glk-change` (user only) | `{ value }` |
| `<glk-calendar>` | `.value`, `.month` | `value`, `month` | `glk-change` (user only), `glk-month` (user only) | `{ value }`, `{ month }` |
| `<glk-image-picker>` | `.src` (not reflected) | `src` | `glk-change`, `glk-error` | `{ dataUrl, width, height, size }`, `{ message, name }` |
| `<glk-badge>` | `.selected` | `interactive`, `selected` | `glk-click` (only when interactive) | — |
| `<glk-toast>` | `.show()` | `visible` | `glk-action`, `glk-close`, `glk-dismiss` | `{ action, label }`, —, — |

All `glk-*` events bubble and are `composed: true`, so they pierce shadow boundaries naturally.

---

## 6. Rules & Common Mistakes

### Always follow

1. **`data-theme` and `data-density` on `<html>`** — never on `<body>` or individual elements; the global MutationObserver only watches `<html>`.
2. **Form elements belong inside a `<form>`** — otherwise `ElementInternals.setFormValue()` has no effect and `new FormData()` will be empty.
3. **`<glk-list-item>` must be a direct child of `<glk-list>`** — nested deeper, it won't be observed for the auto-divider last-item detection.
4. **Popover trigger via `slot="trigger"`** — don't reference external elements by ID. The popover's built-in click handler listens on the trigger slot.
5. **Toggle popovers through the API** — `popover.open = true` or `popover.toggle()`, never `classList.add('is-open')` on internal nodes.
6. **Use `<glk-list bare>` inside `<glk-popover>`** — otherwise you get a double glass surface.
7. **Import the library once** — `import '@jungherz-de/glasskit-elements'` registers all elements via `customElements.define`. Multiple imports are idempotent but unnecessary.
8. **SVG icons: `stroke: currentColor, stroke-width: 2`** — matches GlassKit's icon convention and inherits the token-driven text color.
9. **`title` on `<glk-sheet>`, `<glk-modal>`, `<glk-list-item>`, `<glk-accordion-item>` and `<glk-empty>` is the heading, not a tooltip** (since 1.16.1) — the element reads it and takes the attribute off the host, so `el.getAttribute('title')` is `null` after upgrade; read and write `el.title` instead. Every other element keeps the native `title` tooltip.

### Common mistakes

| Mistake | Correction |
|---|---|
| `onclick="popover.togglePopover()"` | Collides with the native `HTMLElement.togglePopover()` method (HTML Popover API). Use `.toggle()` on the `<glk-popover>` element, or set the `open` attribute. |
| `<hr>` or `<glk-divider>` manually between `<glk-list-item>`s | Remove it — dividers render automatically via `::after`. |
| `<glk-list>` inside `<glk-popover>` without `bare` | Add `bare` to the inner list so only the popover surface has glass. |
| `document.querySelector('.glass-modal').classList.add('is-active')` | Set `<glk-modal>.open = true` instead — the element manages `.is-active` internally. |
| `data-theme` directly on a `<glk-*>` element | Set it on `<html>`; theme propagates automatically through the MutationObserver. |
| Shrinking elements one by one (`::part`, host heights) for a denser UI | `data-density="compact"` on `<html>`, or a density token such as `--gl-btn-height` on `:root` — GlassKit 1.21.0 or later. |
| Form element outside `<form>` | Wrap in a `<form>`; `ElementInternals` needs a form owner. |
| Overriding shadow styles with external CSS | Use CSS custom properties (`--gl-color-primary`, etc.) — they pierce shadow roots. Hard-coded selectors inside shadow roots cannot be targeted externally. |
| Forgetting `slot="trigger"` on the popover trigger element | Without it, the popover's internal click handler can't identify the trigger and never toggles. |
| Using `open` as a method name (`mypopover.open()`) | `open` is a reflected property. Use `.show()` / `.close()` / `.toggle()` for imperative control. |
| `<glk-tab-bar>` without `.glass-bg--has-tab-bar` on the outer background | Tab bar covers bottom content. Add the modifier to the outer `.glass-bg` container. |
| `<glk-tab-bar floating>` without `<glk-tab-dock>` wrapper | Floating bar relies on the dock for fixed centered positioning. Always wrap it in `<glk-tab-dock>`. |
| `<glk-tab-dock>` without `.glass-bg--has-tab-bar-floating` on the outer background | Use `--has-tab-bar-floating` (not `--has-tab-bar`) for the floating variant — the padding budget differs. |
| `<glk-toast>` shown while a `<glk-modal>` is open | Everything outside the open modal is inert — the toast is neither clickable nor announced. Show the message inside the modal, or close it first. |
| `glk-toggle[aria-checked="true"]` / `[role="switch"]` on the host | Since 1.20.0 the host carries neither; use `glk-toggle[checked]`. The role sits on the native checkbox inside. |
| Validating `<glk-input required>` by hand before submit | Not needed since 1.20.0 — the form's own validation stops the submit. |

---

## 7. Quick Reference

| Tag | Category | Key attributes | Key slots | Key events |
|---|---|---|---|---|
| `<glk-nav>` | Navigation | — | default | — |
| `<glk-pill>` | Navigation | `label`, `disabled` | default (icon) | `glk-click` |
| `<glk-tab-bar>` | Navigation | `static`, `floating` | default | — |
| `<glk-tab-item>` | Navigation | `label`, `active`, `badge` | default (icon) | `glk-tab-select` |
| `<glk-tab-dock>` | Navigation | `accessory-left` | default | — |
| `<glk-tab-accessory>` | Navigation | `label`, `variant`, `disabled` | default (icon) | `glk-click` |
| `<glk-avatar>` | Content | `size`, `src` | default (initials) | — |
| `<glk-badge>` | Content | `variant`, `interactive`, `selected` | default | `glk-click` (only when interactive) |
| `<glk-empty>` | Content | `title`, `text` | default (icon), `action` | — |
| `<glk-segmented>` | Forms | `options`, `value`, `full`, `overflow`, `label`, `name` | — | `glk-change` |
| `<glk-steps>` | Navigation | `steps`, `current`, `label` | — | — |
| `<glk-sheet>` | Feedback | `open`, `inline`, `title` | default, `actions` | `glk-close` |
| `<glk-date-strip>` | Navigation | `start`, `days`, `value`, `today`, `marks`, `locale`, `label` | — | `glk-change` |
| `<glk-calendar>` | Forms | `month`, `value`, `today`, `min`, `max`, `marks`, `locale`, `week-start`, `label`, `name` | — | `glk-change`, `glk-month` |
| `<glk-image-picker>` | Forms | `src`, `label`, `hint`, `round`, `max`, `type`, `quality`, `accept` | — | `glk-change`, `glk-error` |
| `<glk-card>` | Content | `glow` | default | — |
| `<glk-divider>` | Content | — | — | — |
| `<glk-status>` | Content | `message` | — | — |
| `<glk-title>` | Content | — | default | — |
| `<glk-button>` | Buttons | `variant`, `size`, `disabled`, `type` | default | `glk-click` |
| `<glk-checkbox>` | Forms | `label`, `checked`, `disabled`, `name`, `value`, `required` | — | `glk-change` |
| `<glk-input>` | Forms | `label`, `type`, `placeholder`, `hint`, `error`, `disabled`, `readonly`, `required`, `name`, `value`, `min`, `max`, `step`, `minlength`, `maxlength`, `pattern`, `autocomplete`, `inputmode` | `prefix`, `suffix` | `glk-input`, `glk-change` |
| `<glk-radio>` | Forms | `label`, `name`, `value`, `checked`, `disabled`, `required` | — | `glk-change` |
| `<glk-range>` | Forms | `label`, `min`, `max`, `value`, `step`, `name`, `disabled` | — | `glk-input`, `glk-change` |
| `<glk-search>` | Forms | `placeholder`, `label`, `value`, `name`, `disabled` | — | `glk-input`, `glk-change` |
| `<glk-select>` | Forms | `label`, `name`, `value`, `disabled`, `required` | default (`<option>`) | `glk-change` |
| `<glk-textarea>` | Forms | `label`, `rows`, `placeholder`, `name`, `value`, `disabled`, `readonly`, `required` | — | `glk-input`, `glk-change` |
| `<glk-toggle>` | Forms | `label`, `checked`, `disabled`, `name`, `value`, `required` | — | `glk-change` |
| `<glk-modal>` | Feedback | `open`, `title` | default, `actions` | `glk-close` |
| `<glk-popover>` | Feedback | `open`, `placement` | `trigger`, default | `glk-open`, `glk-close` |
| `<glk-progress>` | Feedback | `value`, `label`, `variant`, `size` | — | — |
| `<glk-toast>` | Feedback | `message`, `variant`, `duration`, `visible`, `action-label`, `action-value`, `dismissible`, `close-label` | `icon` | `glk-action`, `glk-close`, `glk-dismiss` |
| `<glk-accordion>` | Containers | — | default | — |
| `<glk-accordion-item>` | Containers | `title`, `open` | default | `glk-toggle` |
| `<glk-list>` | Containers | `header`, `flush`, `bare` | default | — |
| `<glk-list-item>` | Containers | `title`, `subtitle`, `interactive`, `center`, `leading-lg`, `wrap`, `detail`, `variant` | `leading`, `trailing` | `glk-click` (when interactive) |

---

## 8. Framework Integration

GlassKit Elements are standards-compliant Custom Elements. They work in every major framework:

**Properties set early are kept (since 1.22.0).** Frameworks bind values as properties right after creating an element — before it is connected, and sometimes before the bundle has even defined it. A property set before the element rendered waits for it (`value`, `checked` and the rest no longer throw), and one set before the class was defined, which lands on the instance and hides the setter, is taken over through the setter on first connect.

### Vanilla HTML / JS

```html
<script type="module" src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.esm.js"></script>
<glk-button variant="primary" onclick="alert('clicked')">Click</glk-button>
```

### React

```jsx
import '@jungherz-de/glasskit-elements';

function App() {
  return (
    <glk-input
      label="Name"
      value={name}
      onGlkInput={(e) => setName(e.detail.value)}
    />
  );
}
```

For React 18, custom-event listeners use the lowercase `onglkchange` form, or attach manually via `ref` + `addEventListener`. React 19 supports `onGlkChange`-style camelCase directly.

### Vue 3

```vue
<template>
  <glk-input
    label="Name"
    :value="name"
    @glk-input="e => name = e.detail.value"
  />
</template>

<script setup>
import '@jungherz-de/glasskit-elements';
import { ref } from 'vue';
const name = ref('');
</script>
```

### Svelte

```svelte
<script>
  import '@jungherz-de/glasskit-elements';
  let name = '';
</script>

<glk-input
  label="Name"
  value={name}
  on:glk-input={e => name = e.detail.value}
/>
```

### lit-html / Hybrids

```js
import { html } from 'lit-html';
import '@jungherz-de/glasskit-elements';

const template = (items) => html`
  <glk-list>
    ${items.map(item => html`
      <glk-list-item title=${item.title} subtitle=${item.subtitle} ?interactive=${true}>
        ${item.icon ? html`<svg slot="leading">...</svg>` : ''}
      </glk-list-item>
    `)}
  </glk-list>
`;
```

Framework-safe note: `<glk-list>` and `<glk-list-item>` use pure slot projection — no child cloning — so lit-html parts, Hybrids templates, and React virtual-DOM bindings stay intact across re-renders.

### SSR

GlassKit Elements are currently **client-side only** — they rely on `document.documentElement`, `adoptedStyleSheets`, and `ElementInternals` which are unavailable during server-side rendering. In SSR frameworks (Next.js, Nuxt, SvelteKit, Astro), use client-only directives or dynamic imports to defer hydration.

---

## 9. Custom Theming

Theming works identically to GlassKit CSS — define custom-property overrides in your global stylesheet; they pass through shadow boundaries automatically:

```css
:root {
  --gl-color-primary:      #007AFF;
  --gl-color-primary-dark: #0055CC;
  --gl-color-primary-mid:  #0066E0;

  --gl-border-warm:        rgba(0, 122, 255, 0.35);
  --gl-border-focus:       rgba(0, 122, 255, 0.60);

  --gl-shadow-btn-primary: 0 6px 24px rgba(0, 100, 220, 0.35),
                            0 2px 8px rgba(0, 0, 0, 0.15);
  --gl-shadow-focus:       0 0 0 3px rgba(0, 122, 255, 0.3);
}
```

See the class-based [GlassKit CSS `SKILL.md`](https://github.com/JUNGHERZ/GlassKit/blob/main/SKILL.md) Section 2 for the complete list of design tokens (colors, surfaces, borders, blur, radii, spacing, shadows, typography).

---

## 10. Architecture Notes

| Concept | Location |
|---|---|
| Base class | `src/base.js` → `GlkElement`, `GlkFormElement` |
| Theme sync | Single module-level `MutationObserver` in `base.js` |
| Adopted stylesheets | `componentsSheet` (from `@jungherz-de/glasskit/glasskit-styles.js`) + module-level `hostSheet` / `inlineHostSheet` (they also set `color-scheme` per theme) + an element's own `hostStyles` |
| Per-component structure | One `.js` file per element in `src/components/{category}/glk-{name}.js` |
| Barrel | `src/index.js` — exports and registers all 36 elements, and exports `GlkElement` / `GlkFormElement` (since 1.14.0) |
| Build | Rollup → IIFE (`glasskit-elements.js`), minified IIFE, ESM (`glasskit-elements.esm.js`), and per-element ESM in `dist/components/` with `base.js` as a stable entry |

Each element is a subclass of `GlkElement` (or `GlkFormElement` for form controls) and follows a consistent lifecycle:

1. `constructor()` — attach open shadow (`delegatesFocus` for form elements with one field), adopt GlassKit stylesheet + host display sheet
2. `connectedCallback()` — create theme wrapper, call `render()`, call `setupEvents()`, register for global theme sync
3. `render()` — build shadow DOM tree (override per component)
4. `setupEvents()` — attach DOM listeners (override per component)
5. `attributeChangedCallback()` — call `onAttributeChanged()` (override per component)
6. `disconnectedCallback()` — unregister, call `teardownEvents()`

The `GlkFormElement` subclass adds `ElementInternals` for native form participation via `this.setFormValue(value)`, plus `formResetCallback` → `resetValue()` and `formStateRestoreCallback` → `restoreValue()`. Since 1.20.0 it mirrors the validity of the native field returned by `get _validityField()` onto the host (`syncValidity()`, run after every `setFormValue()` and attribute change); `<glk-radio>` overrides it with the group's verdict.
