Thyme UI

Native Web Component UI library · zero dependencies

th-button

Variants

Sizes

Icon + text

Icon only

States

Link mode

Custom color

Loading on click

Click to load
<th-button id="btn-load-demo">Click to load</th-button>
<script>
  btn.addEventListener('click', function () {
    this.setAttribute('loading', '');
    setTimeout(() => this.removeAttribute('loading'), 2000);
  });
</script>

th-field

Input types

Date picker

selected: 2026-05-13

<th-field label="Date picker" type="date"></th-field>
<script>
  field.addEventListener('change', () => {
    console.log(field.value);
  });
</script>

Textarea

Validation

Custom error

Set error Clear
field.setCustomValidity('Custom error message');
field.setCustomValidity('');  // clear

Disabled & Readonly

Value binding

Value: (empty)

field.addEventListener('input', () => {
  display.textContent = field.value;
});

Custom content (slot)

th-switch

Basic

Disabled

Toggle event

Off
switchEl.addEventListener('change', function (e) {
  status.textContent = e.detail.checked ? 'On' : 'Off';
});

th-check

Checkbox

Radio group

Disabled

Change event

Toggle me unchecked
checkEl.addEventListener('change', function (e) {
  status.textContent = e.detail.checked ? 'checked' : 'unchecked';
});

th-select

Basic

With label & value

Disabled

Change event

selected: (none)

selectEl.addEventListener('change', function (e) {
  status.textContent = 'selected: ' + e.detail.value;
});

th-dialog

Basic alert

Show dialog
<th-dialog id="dlg" title="提示">
  <p>操作已成功完成。</p>
  <button slot="footer">好</button>
</th-dialog>

Confirm (two buttons)

Show confirm
<th-dialog id="dlg" title="确认删除">
  <p>确定要删除吗?</p>
  <button slot="footer" id="cancel">取消</button>
  <button slot="footer" id="ok">删除</button>
</th-dialog>

No title

No title dialog

Non-closable (ESC disabled)

Non-closable

Custom width

Wide dialog (500px)
<th-dialog title="Wide" width="500">...</th-dialog>

操作已成功完成。

此操作不可撤销,确定要删除吗?

这是一个没有标题栏的对话框。

按 ESC 无法关闭,必须点击按钮。

This dialog has width="500".

Shared color

All components read --th-primary from their parent. Set it once to theme everything.

Purple Purple Purple
Open purple dialog

对话框按钮颜色同样跟随 --th-primary。

<div style="--th-primary:#7c3aed">
  <th-button variant="tonal">Purple</th-button>
  <th-button variant="outlined">Purple</th-button>
  <th-switch checked></th-switch>
  <th-check checked>Purple</th-check>
  <th-field label="Purple input" value="Shared theme"></th-field>
  <th-select label="Choose" value="2">...</th-select>
</div>

th-toast

Standalone toast component with slide-in animation, auto-dismiss, click-to-dismiss.

Attributes

<th-toast type="info|warn|error|success" duration="3">message</th-toast>

Thyme.alert / Thyme.confirm

Programmatic dialog via Thyme.alert(message, title?) and Thyme.confirm(message, title?) — returns a Promise.

Alert message With title Confirm result: (none)
// One button — auto-resolves on click
Thyme.alert('Operation completed.', 'Success');

// Two buttons — returns Promise<boolean>
Thyme.confirm('Are you sure?', 'Confirm').then(result => {
  // true = OK clicked, false = Cancel clicked
});

Thyme.info / .warn / .error / .success

Programmatic toasts. Second parameter is custom duration in seconds (default 3).

info warn error success 5 seconds 1 second
Thyme.info('Welcome!');
Thyme.warn('Be careful');
Thyme.error('Failed');
Thyme.success('Done!');
Thyme.warn('Custom duration', 5);  // 5 seconds

Thyme.locale

Toggle between 'en' and 'zh'. Affects date picker, alert/confirm buttons, and internally via locale.translate(key).

Switch to 中文 Current: en
// Get
console.log(Thyme.locale);  // 'en' | 'zh'

// Set
Thyme.locale = 'zh';

Thyme.form

Serialize form data to/from JSON objects. Handles all [name] elements including radio, checkbox, switch, select.

JS CSS Rust Male Female
JS CSS Rust Male Female
getJsonObject getJsonArray setJsonObject
// Collect all [name] values from a single scope
const data = Thyme.form.getJsonObject(formEl);
// Returns null if ANY field fails checkValidity()

// Collect from multiple scopes (e.g. table rows)
const rows = Thyme.form.getJsonArray('.form-row');
// Returns null if ANY scope fails

// Set values back into a scope
Thyme.form.setJsonObject(formEl, { name: "Alice", interest: ["rust"] });

// Validate first, then submit
const data = Thyme.form.getJsonObject('#my-form');
if (data === null) return;  // ⬅ validation failed, already focused on first invalid field
await fetch('/api/submit', { method: 'POST', body: JSON.stringify(data) });

Thyme.http

Lightweight HTTP client wrapping fetch() with JSON handling, response auto-detection, and error reporting.

Methods

Thyme.http.get(url, opts?)
Thyme.http.post(url, data?, opts?)
Thyme.http.put(url, data?, opts?)
Thyme.http.patch(url, data?, opts?)
Thyme.http.delete(url, opts?)

Basic usage

// GET
const users = await Thyme.http.get('/api/users');

// POST with JSON body
const user = await Thyme.http.post('/api/users', { name: 'Marco' });

// PUT / PATCH / DELETE
await Thyme.http.put('/api/users/1', { name: 'Updated' });
await Thyme.http.patch('/api/users/1', { age: 31 });
await Thyme.http.delete('/api/users/1');

Custom fetch options

Pass native fetch options like signal, credentials, or custom headers via the last argument.

// Abort request
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 3000);
const data = await Thyme.http.get('/api/slow', { signal: ctrl.signal });

// Custom headers (merged with defaults)
Thyme.http.post('/api/data', { foo: 1 }, {
  headers: { Authorization: 'Bearer xxx' }
});

// FormData (not JSON-stringified)
const fd = new FormData();
fd.append('file', blob);
await Thyme.http.post('/api/upload', fd);

Response types

Body is parsed automatically based on Content-Type:

// text/*       → response.text()
// application/json → response.json()
// everything else  → response.blob()

// Non-OK status throws with the parsed server message
try {
  await Thyme.http.get('/api/data');
} catch (e) {
  console.error(e.message);
}

Utilities

Called via Thyme.utils.*.

delay(ms)

Promise-based setTimeout — await Thyme.utils.delay(1000) waits 1 second.

Demo Wait 1s
await Thyme.utils.delay(1000);
console.log('1 second later');

nanoId(size = 24)

URL-safe unique ID generator (A-Za-z0-9, no - _).

Result
Thyme.utils.nanoId();      // e.g. "Xa3Rt9Kf2LmNpQwZbY7VcJ1"
Thyme.utils.nanoId(12);    // custom length

formatDate(date, pattern, utc?)

Format a Date object with custom pattern. Supports yyyy/yy/MM/M/dd/d/hh/h/mm/m/ss/s/SSS/S.

yyyy-MM-dd
yyyy/MM/dd hh:mm:ss
Thyme.utils.formatDate(new Date(), 'yyyy-MM-dd');       // 2026-05-13
Thyme.utils.formatDate(new Date(), 'yyyy/MM/dd hh:mm'); // 2026/05/13 14:30
Thyme.utils.formatDate(new Date(), 'MM/dd', true);      // UTC version

formatMoney(number)

Format as currency with 2 decimal places and thousand separators.

1234567.89
Thyme.utils.formatMoney(1234567.89);  // "1,234,567.89"

formatBytes(bytes)

Human-readable byte sizes (B, KiB, MiB, GiB, TiB...).

0
1024
1048576
1073741824
Thyme.utils.formatBytes(1024);          // "1 KiB"
Thyme.utils.formatBytes(1048576);       // "1 MiB"
Thyme.utils.formatBytes(1073741824);    // "1 GiB"

formatSeconds(seconds)

Format seconds to compact d h m s string.

3661
90061
Thyme.utils.formatSeconds(3661);   // "1h 1m 1s"
Thyme.utils.formatSeconds(90061);  // "1d 1h 1m 1s"

parseDuration(string)

Parse HH:mm:ss.SSS to milliseconds.

01:30:00
00:05:30.500
Thyme.utils.parseDuration('01:30:00');      // 5400000 (1.5 hours)
Thyme.utils.parseDuration('00:05:30.500');  // 330500