npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@metadream/thyme-ui

v0.1.12

Published

A lightweight UI component library built with native Web Components, zero dependencies.

Downloads

1,648

Readme

Thyme UI

A lightweight UI component library built with native Web Components (Custom Elements + Shadow DOM), zero third-party dependencies.

https://metadream.github.io/thyme-ui — live demo

Installation

CDN (recommended)

<script src="https://unpkg.com/@metadream/thyme-ui"></script>

This single script registers all components and exposes the Thyme global API.

Build from source

npm install        # installs terser
npm run build      # outputs docs/thyme.min.js

Components

All components follow these conventions:

  • Native-like .value, .name, .checked, .type properties for uniform form handling
  • Date format: yyyy-mm-dd throughout
  • Attributes are reactive — changes after render are reflected automatically

<th-button>

A button with four visual variants, loading state, ripple effect, and optional link mode.

Attributes

| Attribute | Type | Default | Description | |------------|-----------|------------|-------------| | variant | string | "filled" | One of: filled, tonal, outlined, ghost | | size | string | — | "small", "large", or omit for medium | | disabled | boolean | — | Disables the button | | loading | boolean | — | Shows spinner, disables interaction | | href | string | — | When set, renders an <a> instead of <button> | | target | string | — | Link target (only when href is set) | | rel | string | — | Link rel (only when href is set) | | download | string | — | Link download (only when href is set) | | type | string | — | Button type attribute ("submit", "reset", "button") | | name | string | — | Button name | | value | string | — | Button value | | form | string | — | Associated form ID | | autofocus| boolean | — | Auto-focus on mount |

Slots

| Slot | Description | |---------|-------------| | default | Button label text | | "icon"| Icon element. When only icon is present (no text), the button renders as a square icon-only button |

CSS Shadow Parts

| Part | Element | |-----------|---------| | button | The inner <button> or <a> | | content | <span> wrapping the slots | | loader | Loading spinner <span> |

Examples

<th-button>Filled</th-button>
<th-button variant="tonal">Tonal</th-button>
<th-button variant="outlined">Outlined</th-button>
<th-button variant="ghost">Ghost</th-button>

<th-button size="small">Small</th-button>
<th-button size="large">Large</th-button>

<th-button loading>Saving...</th-button>
<th-button disabled>Disabled</th-button>

<!-- Link mode -->
<th-button href="https://example.com" target="_blank">Link</th-button>

<!-- Icon-only -->
<th-button><svg slot="icon">...</svg></th-button>

<th-field>

A form field with label, input/textarea/date picker, built-in validation, and error display.

Attributes

| Attribute | Type | Default | Description | |----------------|-----------|-----------|-------------| | label | string | — | Field label text | | type | string | "text" | One of: text, email, number, date, textarea | | value | string | — | Current input value | | name | string | — | Field name for form serialization | | placeholder | string | — | Placeholder text | | required | boolean | — | Marks as required (shows * in label) | | disabled | boolean | — | Disables the input | | readonly | boolean | — | Makes input read-only | | minlength | number | — | Minimum text length | | maxlength | number | — | Maximum text length | | rows | number | 3 | Textarea row count (only when type="textarea") | | min | string | — | Minimum value (for number/date types) | | max | string | — | Maximum value (for number/date types) | | pattern | string | — | Regex pattern for validation | | autocomplete | string | — | Autocomplete hint | | autofocus | boolean | — | Auto-focus on mount | | error | string | — | Sets an error message (overrides validation). Remove attr to clear |

Note: When type="date", the built-in input is type="text" with maxlength="10" (for yyyy-mm-dd format) and a custom calendar popup. Validation checks the date format, real date existence, and optional min/max constraints.

Properties

| Property | Type | Get/Set | Description | |----------|---------|---------|-------------| | .value | string | get/set | Current input value | | .name | string | get | Field name attribute | | .disabled | boolean | get/set | Whether the field is disabled | | .readonly | boolean | get/set | Whether the field is read-only | | .required | boolean | get/set | Whether the field is required |

Methods

| Method | Returns | Description | |-------------------------------|----------|-------------| | .checkValidity() | boolean | Returns whether the input passes validation | | .reportValidity() | boolean | Checks validity and shows error message if invalid | | .setCustomValidity(msg) | void | Sets a custom validation error. Pass empty string to clear | | .focus() | void | Focuses the inner input/textarea |

Events (native, delegated from inner <input>/<textarea>)

| Event | Description | |----------|-------------| | input | Fires on every value change (bubbles) | | change | Fires when value is committed (bubbles) | | invalid| Fires when validation fails |

Slots

| Slot | Description | |----------|-------------| | default | Replaces the built-in <input>/<textarea> with a custom element (the custom element must have .value and support the expected attributes). When type="textarea", light DOM text content is consumed as the initial value | | "label"| Replaces the label text |

CSS Shadow Parts

| Part | Element | |-------------|---------| | field | Outer container | | label | <label> element | | input | The <input> or <textarea> | | error | Error tooltip <span> | | date-btn | Calendar toggle button (only when type="date") | | calendar | Date picker popup (only when type="date") |

Examples

<!-- Text input -->
<th-field label="Name" name="username" placeholder="Enter your name"></th-field>

<!-- Email with validation -->
<th-field label="Email" type="email" name="email" required></th-field>

<!-- Number -->
<th-field label="Age" type="number" name="age" min="0" max="150"></th-field>

<!-- Date picker -->
<th-field label="Date" type="date" name="date"></th-field>

<!-- Textarea with slot content consumed as initial value -->
<th-field label="Bio" type="textarea" name="bio" rows="5">
  <p>Existing content</p>
</th-field>

<!-- Pre-filled value -->
<th-field label="City" value="Shanghai"></th-field>

<!-- With error -->
<th-field label="Password" type="password" error="Too short"></th-field>

<!-- Disabled -->
<th-field label="Readonly" value="Immutable" disabled></th-field>

<!-- Using slot for label -->
<th-field name="email">
    <span slot="label">Email Address</span>
</th-field>

<th-check>

Checkbox or radio button with custom styling.

Attributes

| Attribute | Type | Default | Description | |------------|-----------|--------------|-------------| | type | string | "checkbox" | Either "checkbox" or "radio" | | checked | boolean | — | Whether the element is checked | | disabled | boolean | — | Disables interaction | | name | string | — | Group name (radio buttons with same name auto-uncheck each other) | | value | string | — | Value submitted with form |

Properties

| Property | Type | Get/Set | Description | |------------|---------|---------|-------------| | .checked | boolean | get/set | Checked state | | .disabled| boolean | get | Disabled state | | .type | string | get | "checkbox" or "radio" | | .name | string | get | Name attribute | | .value | string | get | Value attribute |

Events

| Event | Detail | Description | |----------|---------------------------------|-------------| | change | { checked: boolean, value: string } | Fires on toggle (bubbles) |

Slots

| Slot | Description | |---------|-------------| | default | Label text displayed next to the indicator |

Examples

<th-check checked>Remember me</th-check>
<th-check type="radio" name="gender" value="male">Male</th-check>
<th-check type="radio" name="gender" value="female">Female</th-check>
<th-check disabled>Disabled</th-check>

<!-- Programmatic -->
<script>
    const cb = document.querySelector('th-check');
    cb.checked = true;
    cb.addEventListener('change', (e) => console.log(e.detail));
</script>

<th-switch>

A toggle switch.

Attributes

| Attribute | Type | Description | |------------|-----------|-------------| | checked | boolean | Toggle state | | disabled | boolean | Disables interaction | | value | string | Value returned when checked |

Properties

| Property | Type | Get/Set | Description | |------------|---------------|---------|-------------| | .checked | boolean | get/set | Toggle state | | .disabled| boolean | get | Disabled state | | .value | string|number | get | Returns attribute value or 1 when checked, undefined when unchecked |

Events

| Event | Detail | Description | |----------|---------------------------|-------------| | change | { checked: boolean } | Fires on toggle (bubbles) |

Examples

<th-switch checked>Notifications</th-switch>
<th-switch disabled>Unavailable</th-switch>

<th-select>

A custom select dropdown built from <option> children.

Attributes

| Attribute | Type | Description | |---------------|-----------|-------------| | label | string | Label text shown to the left | | value | string | Currently selected value | | placeholder | string | Placeholder text when no option is selected | | disabled | boolean | Disables the select | | name | string | Field name for form serialization | | required | boolean | Marks as required |

Properties

| Property | Type | Get/Set | Description | |------------|---------|---------|-------------| | .value | string | get/set | Currently selected value | | .disabled| boolean | get/set | Disabled state | | .name | string | get | Name attribute | | .checkValidity() | boolean | — | Returns whether validation passes | | .reportValidity()| boolean | — | Checks validity and shows error if invalid | | .focus() | void | — | Focuses the trigger element |

Events

| Event | Detail | Description | |----------|---------------------------------|-------------| | change | { value: string, text: string } | Fires when an option is selected (bubbles) |

Children

Uses standard <option> elements:

<option value="cn">China</option>
<option value="us" selected>United States</option>

The selected attribute on an <option> sets the initial value.

CSS Shadow Parts

| Part | Element | |-----------|---------| | select | Outer container | | label | <label> element | | trigger | Clickable trigger bar | | value | Value display <span> | | panel | Dropdown panel | | error | Error message <span> |

Keyboard navigation

| Key | Action | |----------------|--------| | Enter / Space | Open dropdown, or confirm highlighted option | | Escape | Close dropdown | | ArrowDown | Highlight next option (opens if closed) | | ArrowUp | Highlight previous option (opens if closed) |

Examples

<th-select label="Country" name="country">
    <option value="cn">China</option>
    <option value="us" selected>United States</option>
    <option value="jp">Japan</option>
</th-select>

<!-- With placeholder -->
<th-select label="City" placeholder="Select a city...">
    <option value="shanghai">Shanghai</option>
    <option value="beijing">Beijing</option>
</th-select>

<!-- Programmatic -->
<script>
    const sel = document.querySelector('th-select');
    sel.value = 'jp';
    console.log(sel.value); // 'jp'
    sel.addEventListener('change', (e) => console.log(e.detail));
</script>

<th-dialog>

A modal dialog with overlay, title, body, and footer slot.

Attributes

| Attribute | Type | Default | Description | |------------|-----------|---------|-------------| | open | boolean | — | Whether the dialog is visible | | title | string | — | Dialog title text | | closable | boolean | true | Whether Escape key can close the dialog | | width | number | — | Fixed dialog width in pixels (overrides CSS default) |

Methods

| Method | Description | |----------|-------------| | .open() | Opens the dialog (sets open attribute) | | .close() | Closes the dialog with animation (removes open attribute) |

Slots

| Slot | Description | |-----------|-------------| | default | Dialog body content | | "footer"| Footer buttons. When empty, the footer is hidden. Buttons are styled automatically |

CSS Shadow Parts

| Part | Element | |-----------|---------| | overlay | Fixed full-screen overlay | | dialog | Dialog panel | | title | Title bar | | body | Body content area | | footer | Footer area |

Behavior

  • Body scroll is prevented while open (wheel + touchmove)
  • Escape key closes the dialog (unless closable="false")
  • Clicking the overlay does NOT close the dialog

Examples

<th-dialog id="myDialog" title="Confirm">
    <p>Are you sure?</p>
    <button slot="footer">OK</button>
    <button slot="footer">Cancel</button>
</th-dialog>

<script>
    document.querySelector('#myDialog').open();
</script>

<th-toast>

A fixed-position toast notification. Auto-removes after a duration. Multiple toasts stack vertically.

Attributes

| Attribute | Type | Default | Description | |------------|---------|---------|-------------| | type | string | "info"| One of: info, warn, error, success | | duration | number | 3 | Display duration in seconds |

Methods

| Method | Description | |----------|-------------| | .close() | Closes the toast immediately and dispatches a close event |

Events

| Event | Detail | Description | |---------|--------|-------------| | close | — | Dispatched when the toast is closed (bubbles) |

Slots

| Slot | Description | |---------|-------------| | default | Toast message text |

CSS Shadow Parts

| Part | Element | |--------|---------| | toast| The toast <div> |

Styling

| Type | Background | Text | |-----------|-------------------|--------| | info | Dark gray (80%) | White | | warn | Yellow (80%) | Dark | | error | Red (80%) | White | | success | Green (80%) | White |

Examples

<th-toast type="success">Saved successfully!</th-toast>
<th-toast type="error" duration="5">Connection failed</th-toast>

Global API (self.Thyme)

After the script loads, Thyme is available globally.

Dialogs

Thyme.alert('Hello World');
Thyme.alert('File deleted', 'Warning');

// Returns Promise<boolean>
const ok = await Thyme.confirm('Delete this item?');
if (ok) { /* proceed */ }

Toast notifications

Thyme.info('Loading...');
Thyme.success('Done!', 2);           // 2 seconds
Thyme.warn('Low disk space', 5);     // 5 seconds
Thyme.error('Something broke', 10);  // 10 seconds

Locale / i18n

Thyme.locale = 'zh';  // switch to Chinese
Thyme.locale = 'en';  // switch to English

Form utilities

// Serialize a form scope to an object.
// Returns null if ANY field fails checkValidity() — the first
// invalid field receives focus and shows its error message.
const data = Thyme.form.getJsonObject('#my-form');
// or pass an element:
const data = Thyme.form.getJsonObject(document.querySelector('#my-form'));

// Serialize multiple form scopes to an array.
// Returns null if ANY scope's validation fails.
const all = Thyme.form.getJsonArray('.form-scope');
// or pass a NodeList:
const all = Thyme.form.getJsonArray(document.querySelectorAll('.form-scope'));

// Populate a form from an object
Thyme.form.setJsonObject('#my-form', { username: 'admin', age: 25 });

// Typical submit guard:
const data = Thyme.form.getJsonObject('#my-form');
if (data === null) return;  // validation failed, already focused
await fetch('/api/submit', { method: 'POST', body: JSON.stringify(data) });

Rules for serialization (getJsonObject / getJsonArray):

  • Only elements with a name attribute are included
  • Unchecked checkboxes / radio buttons / switches are skipped
  • Checkboxes with the same name or <select multiple> are collected into an array
  • contentEditable elements use trimmed innerHTML
  • User-input elements (native <input>/<textarea>, <th-field>) have their value trimmed and written back to the element
  • Selection elements (<select>, <th-select>, <th-check>, <th-switch>, radio buttons) are stored as-is
  • On any checkValidity() failure, getJsonObject returns null immediately — the first invalid field is focused and shows its error

setJsonObject populates elements by matching [name] attributes. Handles all the same element types including <select multiple>, checkbox arrays, radio groups, and contentEditable.

HTTP client

Thyme.http wraps fetch() with JSON handling, response type detection, and error reporting.

Methods

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

Parameters

| Param | Type | Description | |--------|----------|-------------| | url | string | Request URL | | data | any | Request body. Plain objects/arrays are JSON-stringified automatically. FormData, Blob, ArrayBuffer sent as-is. Omit for GET/DELETE | | opts | object | Additional fetch options merged into the request (signal, credentials, custom headers, etc.) |

Examples

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

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

// PUT
await Thyme.http.put('/api/users/1', { name: 'Bob' });

// PATCH
await Thyme.http.patch('/api/users/1', { name: 'Bob' });

// DELETE
await Thyme.http.delete('/api/users/1');

Custom fetch options

Pass native fetch() options through the third argument:

// Abort request
const controller = new AbortController();
Thyme.http.get('/api/search', { signal: controller.signal });
controller.abort();

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

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

Response types

The response body is parsed automatically based on Content-Type:

| Content-Type | Parsed as | |---------------------------|----------------| | text/* | response.text() | | application/json | response.json() | | Everything else | response.blob() |

Non-OK status codes (4xx/5xx) throw an Error with the parsed server message.

Error handling

try {
    await Thyme.http.get('/api/data');
} catch (e) {
    console.error(e.message); // server error message or "Unknown error"
}

Errors are also passed to Thyme.error() automatically.

Utility functions

Thyme.utils.delay(1000);                // Promise that resolves after 1s

Thyme.utils.nanoId();                   // Random 24-char ID (0-9a-zA-Z)
Thyme.utils.nanoId(8);                  // Random 8-char ID

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

Thyme.utils.formatDecimal(3.14159, 2);  // 3.14
Thyme.utils.formatMoney(1234567.89);    // "1,234,567.89"
Thyme.utils.formatBytes(1048576);       // "1 MiB"

Thyme.utils.parseDuration('01:30:00');  // 5400 (seconds)
Thyme.utils.formatSeconds(3661);        // "1h 1m 1s"

Thyme.utils.base64Encode('hello');      // "aGVsbG8="

Theming

Set these CSS custom properties on any ancestor element — they inherit through Shadow DOM:

:root {
    --th-primary: #3730a3;    /* Primary color, default: indigo-800 */
    --th-radius: 8px;         /* Border radius, default: 8px */
    --th-font-size: 14px;     /* Base font size */
    --th-line-height: 1.5;    /* Base line height */
}

All derived colors (hover, ripple, border, focus ring) are computed automatically from --th-primary via color-mix() — no additional variables needed.

Styling via ::part()

Each component exposes CSS shadow parts for granular styling:

th-button::part(button) {
    font-weight: 700;
}
th-field::part(input) {
    font-family: monospace;
}
th-dialog::part(dialog) {
    max-width: 500px;
}

Browser Support

Chrome 111+ / Firefox 128+ / Safari 16.2+ / Edge 111+.

Requires @property CSS at-rule and color-mix(). IE is not supported.


Architecture (for contributors)

See AGENTS.md for build process, file conventions, and internal architecture details.