Drop-in Web Components for GlassKit CSS
36 vanilla JavaScript custom elements wrapping GlassKit's glassmorphism components.
Shadow DOM Β· Native form participation Β· Zero dependencies.
π Live Demo Β Β·Β π Documentation Β Β·Β π§ GlassKit CSS Β Β·Β π GlassKit Web
GlassKit Elements is a companion library to GlassKit CSS. It provides 36 Web Components (incl. the iOS 26-style floating Tab-Bar + Accessory) that encapsulate the verbose HTML markup required by GlassKit into simple, declarative custom elements.
It is the app layer of the GlassKit family β three layers, one design language: GlassKit is the pure-CSS foundation, GlassKit Elements wraps it into web components for application UIs, and GlassKit Web is the official Astro template for complete websites on top of the same foundation.
<!-- Before: 5 elements, 6 classes -->
<label class="glass-toggle">
<input class="glass-toggle__input" type="checkbox">
<span class="glass-toggle__track">
<span class="glass-toggle__thumb"></span>
</span>
<span class="glass-toggle__label">Dark Mode</span>
</label>
<!-- After: 1 element, 0 classes -->
<glk-toggle label="Dark Mode" checked></glk-toggle>| Feature | Details |
|---|---|
| π Shadow DOM | Style encapsulation via adoptedStyleSheets β no CSS leaking |
| π§© 36 Components | Buttons, cards, toggles, modals, accordions, lists, popovers, tab bars (incl. floating + accessory), and more |
| πͺΆ Lightweight | 156 KB minified / 28 KB gzipped (IIFE bundle, GlassKit's component rules included), no external dependencies |
| π¦ Three bundle formats | IIFE for <script>, minified IIFE for production, ESM for bundlers & tree-shaking |
| ποΈ Form Participation | Input, toggle, checkbox, radio, select β all work natively with <form> via ElementInternals: value, reset and, since 1.20.0, validity; <glk-button type="submit"> submits |
| π Theme Sync | Automatic dark/light mode sync via data-theme on <html>, compact controls via data-density |
| π± Mobile-first | Inherits GlassKit's mobile-optimized design with safe-area-inset support |
<!-- 1. GlassKit CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.21/glasskit.min.css">
<!-- 2. GlassKit Elements -->
<script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>npm install @jungherz-de/glasskit-elements @jungherz-de/glasskitimport '@jungherz-de/glasskit/glasskit.css';
import '@jungherz-de/glasskit-elements';// Only import what you need
import '@jungherz-de/glasskit-elements/components/glk-button.js';
import '@jungherz-de/glasskit-elements/components/glk-toggle.js';
// The base classes, for elements of your own (same module the components use)
import { GlkElement, GlkFormElement } from '@jungherz-de/glasskit-elements/base.js';The per-component files leave @jungherz-de/glasskit/glasskit-styles.js as an external import, so the GlassKit stylesheet exists once β a bundler resolves it from node_modules; a build-free project adds it to its import map next to the elements entries. Do not mix the <script> bundle with base.js: the bundle carries its own copy of the classes, so instanceof would fail across the two. With the bundle, take the class from it β GlassKitElements.GlkElement, or import { GlkElement } from '@jungherz-de/glasskit-elements' for the ESM bundle.
<!DOCTYPE html>
<html data-theme="dark">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.21/glasskit.min.css">
<script src="https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit-elements/dist/glasskit-elements.min.js"></script>
</head>
<body>
<glk-button variant="primary">Click me</glk-button>
<glk-toggle label="Notifications" checked></glk-toggle>
<glk-card glow>Hello GlassKit!</glk-card>
</body>
</html>| Component | Description | Key Attributes |
|---|---|---|
<glk-nav> |
Horizontal navigation bar | β |
<glk-pill> |
Circular icon button (46Γ46px) | label, disabled |
<glk-tab-bar> |
Bottom tab bar navigation | static, floating |
<glk-tab-item> |
Tab bar item | label, active, badge |
<glk-tab-dock> |
Wrapper for floating tab bar + optional accessory | accessory-left |
<glk-tab-accessory> |
Standalone glass capsule (search, composeβ¦) | label, variant (accent, success, error), disabled |
<glk-steps> |
Progress through a short flow | steps, current, label |
<glk-date-strip> |
Scrolling row of day chips with tone dots; arrow keys, chosen chip centred | start, days, value, today, marks, locale, label |
| Component | Description | Key Attributes |
|---|---|---|
<glk-card> |
Glass-effect content card | glow, fill |
<glk-badge> |
Inline status badge, or a filter chip with interactive |
variant (primary, success, warning, error), interactive, selected |
<glk-avatar> |
Circular avatar | size (sm, lg), src |
<glk-title> |
Styled heading | β |
<glk-divider> |
Horizontal divider | β |
<glk-status> |
Status notice | message |
<glk-empty> |
Empty state: icon, title, text, one action | title, text |
| Component | Description | Key Attributes |
|---|---|---|
<glk-button> |
Glass-styled button; type="submit" / "reset" act on its form |
variant (primary, secondary, tertiary), size (sm, md, lg, auto), disabled, type (button, submit, reset) |
All form components support name, value, disabled and participate in native <form> submission via ElementInternals β except <glk-image-picker>, which hands its result to glk-change instead: a data URL is no form value. Since 1.20.0 they also report their validity to the form (required, pattern, min, type="email" β¦ stop a submit with the browser's own message), their label names the field for screen readers, and a click on it, element.focus() or a <label for> lands in the field.
| Component | Description | Key Attributes |
|---|---|---|
<glk-input> |
Text input with label & hint | label, type, placeholder, hint, error, required, min, max, step, maxlength, pattern, autocomplete, inputmode |
<glk-textarea> |
Multi-line text input | label, rows, placeholder, required |
<glk-select> |
Dropdown select | label, required (children: <option>) |
<glk-search> |
Search input with icon | placeholder, label (accessible name) |
<glk-toggle> |
Switch toggle (role="switch" on its native checkbox) |
label, checked, disabled, required |
<glk-checkbox> |
Checkbox | label, checked, disabled, required |
<glk-radio> |
Radio button | label, name, value, checked, required (per group) |
<glk-range> |
Range slider | label, min, max, value, step |
<glk-segmented> |
Small exclusive choice as one control; form-associated | options (JSON), value, full, overflow (scroll, wrap), label, name |
<glk-calendar> |
One month with a day to pick; keyboard navigation, Intl names; form-associated |
month, value, today, min, max, marks, locale, week-start, label, name |
<glk-image-picker> |
One image with preview, resized on the client, EXIF rotation applied; not form-associated | src, label, hint, round, max, type, quality, accept |
| Component | Description | Key Attributes |
|---|---|---|
<glk-progress> |
Progress bar | value, label, variant (success, error), size (sm, lg) |
<glk-modal> |
Modal dialog β a native <dialog>: page behind inert, focus in and back |
open, title |
<glk-toast> |
Notification that hides itself, or an offer with one action and an Γ | message, variant (success, error, warning), duration, action-label, action-value, dismissible |
<glk-popover> |
Anchored dropdown / menu | open, placement (top, bottom, start, end) |
<glk-sheet> |
Bottom sheet, the mobile sibling of the modal | open, inline, title |
| Component | Description | Key Attributes |
|---|---|---|
<glk-accordion> |
Accordion container | β |
<glk-accordion-item> |
Collapsible section | title, open |
<glk-list> |
iOS-style grouped list | flush, bare |
<glk-list-item> |
List row (icon + title + subtitle + trailing) | title, subtitle, interactive, center |
Set data-theme on the <html> element β all components sync automatically:
<html data-theme="dark"> <!-- or "light" -->// Toggle theme
const html = document.documentElement;
const current = html.getAttribute('data-theme');
html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');Admin screens, desktop layouts and narrow forms want denser controls. Set data-density="compact" on <html>, next to data-theme:
<html data-theme="dark" data-density="compact">Fields and buttons become 40px high instead of 52 and 56, toggles, checkboxes and radios smaller, list rows and modal actions denser; removeAttribute('data-density') switches back at runtime. Without the attribute nothing changes.
Requires GlassKit CSS >= 1.21.0. The sizes are GlassKit's density tokens, so they reach every shadow root by inheritance, like the brand colors below β and so do your own:
[data-density='compact'] { --gl-btn-height: 36px; }. Withdata-glk-defaults="off"the preset comes fromglasskit.csson the page, like every other token. The global observer mirrorsdata-densityonto each element's theme wrapper as well, so a rule keyed on the attribute matches inside the shadow root too.
Print dialogs leave out background graphics by default. With GlassKit 1.21.1 the parts that carry a state keep their colours on paper β checkbox, radio, toggle, progress, the current step, the chosen day, the slider thumb, the tone dots β inside the elements as well. A dark page prints light text and surfaces on white paper; a page meant for printing switches to the light theme for it, and the elements follow:
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');
});Pass an icon as a direct child, not wrapped:
<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>Requires GlassKit CSS >= 1.10.0. The icon rules are descendant selectors, and a slotted icon stays in the light DOM β before 1.10.0 they never matched it, so button icons filled their container and list icons collapsed to 0Γ0. Wrapping the icon in a
<span slot="β¦">still does not work:::slotted()cannot reach inside an assigned node. Size it yourself if you must wrap it.
Override GlassKit's --gl-* tokens anywhere in the document. Every <glk-*> element
inherits them β no JavaScript, no per-element setup:
/* brand.css, loaded after glasskit.css */
:root, [data-theme='dark'] { --gl-color-primary: #2e9e8f; }
[data-theme='light'] { --gl-color-primary: #21786d; }Requires 1.9.0. Before that, every element re-declared the tokens inside its own shadow root, so document-level overrides were silently ignored in components while still applying to plain
.glass-*markup β projects came out half-branded. If you are upgrading from β€1.8.0 and already ship a brand stylesheet, expect your components to change to the colors you intended all along.
Token defaults are placed on the document once, inside @layer glasskit-defaults, so
pages that never load glasskit.css still work and your brand file always wins.
Since 1.20.0 that layer carries the --gl-* tokens only β GlassKit's color-scheme stays
off the page. Each element sets it on its own theme wrapper, so a select's list or a date
picker inside matches the element while the page keeps its scheme. (Before, importing the
elements turned the whole page to color-scheme: dark: unstyled text white, links light
blue, native controls dark.) The token names are still global, so a component of yours
that reads var(--gl-β¦, fallback) gets GlassKit's value. A page that declares the tokens
itself, or wants them on part of the page only, switches the defaults off:
<html data-glk-defaults="off"> <!-- watched: may be set or removed later -->- Shadow DOM with
adoptedStyleSheetsβ GlassKit'scomponentsSheetis shared across all component instances; token declarations stay on the document so branding can be inherited - Theme wrapper with
display: contentsβ layout-transparent<div>fordata-themeanddata-densityCSS selectors - Global
MutationObserverβ single observer watchesdata-themeanddata-densitychanges and syncs all instances GlkElementbase class β handles Shadow DOM setup, theme sync, attribute reflectionGlkFormElementextendsGlkElementβ addsElementInternalsfor native form participation, delegates focus to its field and reports the field's validity- Native semantics β labels tied to their fields, the toggle's
role="switch"on its native checkbox,<glk-modal>a native<dialog>,aria-expandedon accordion and popover triggers
Since 1.14.0 both base classes are exported, so a project element inherits the whole setup β open shadow root with GlassKit's stylesheet adopted (.glass-* classes work inside), theme wrapper following data-theme and data-density, listeners re-armed when the element moves, emit() for bubbling, composed events:
import { GlkElement } from '@jungherz-de/glasskit-elements'; // or 'β¦/base.js', or GlassKitElements.GlkElement from the CDN bundle
class DemoCounter extends GlkElement {
static get observedAttributes() { return ['count']; }
render() {
this._btn = this.createElement('button', ['glass-btn', 'glass-btn--secondary', 'glass-btn--sm', 'glass-btn--auto']);
this._wrapper.appendChild(this._btn);
}
setupEvents() {
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); }
onAttributeChanged() { this._btn.textContent = `Clicked ${this.count}Γ`; }
get count() { return Number(this.getAttribute('count')) || 0; }
set count(v) { this.setAttribute('count', v); }
}
customElements.define('demo-counter', DemoCounter);Hooks: render() builds into this._wrapper, setupEvents() / teardownEvents() run on every connect / disconnect, onAttributeChanged(name, old, value) after the first render. static get displayInline() β true for inline elements; static get observesLightDom() β true re-runs projectLightDom() whenever light-DOM children change. GlkFormElement adds setFormValue(), setValidity(), resetValue() / restoreValue(); since 1.20.0 it delegates focus (static get delegatesFocus()) and, when get _validityField() returns your native field, reports that field's validity on the host after every setFormValue() and attribute change.
glasskit-elements/
src/
index.js # Registers all components
base.js # GlkElement + GlkFormElement
components/
navigation/ # glk-nav, glk-pill, glk-tab-bar, glk-tab-item, glk-tab-dock, glk-tab-accessory
content/ # glk-card, glk-badge, glk-avatar, glk-title, ...
buttons/ # glk-button
forms/ # glk-input, glk-toggle, glk-checkbox, ...
feedback/ # glk-progress, glk-modal, glk-toast, glk-popover
containers/ # glk-accordion, glk-accordion-item, glk-list, glk-list-item
dist/
glasskit-elements.js # IIFE bundle (229 KB raw / 46 KB gzipped)
glasskit-elements.min.js # IIFE minified (156 KB raw / 28 KB gzipped)
glasskit-elements.esm.js # ES module bundle (221 KB raw / 45 KB gzipped)
components/ # one ESM file per element + base.js (GlkElement, GlkFormElement)
index.html # Landing page
docs.html # Documentation
showcase.html # Interactive showcase
de/ # German translations
| Browser | Support |
|---|---|
| Chrome | 111+ |
| Edge | 111+ |
| Safari | 16.4+ |
| Firefox | 113+ |
The floor is GlassKit's: its tokens are derived with color-mix(). The elements also need constructable stylesheets, ElementInternals, delegatesFocus, <dialog> and inert β all inside that floor. A few refinements come later and fall back quietly: first-line alignment of toggle, checkbox and radio (lh, Firefox 120), the divider of list rows without icon (:has(), Firefox 121), right-to-left mirroring of select chevron, toggle thumb and progress fill (:dir(), Chrome 120).
SKILL.md is a tag-based, machine-readable reference for LLMs and AI copilots. It contains copy-paste-ready HTML for all 36 elements, attribute / slot / event tables, composition patterns, and a common-mistakes section. It is the companion to the class-based SKILL.md in GlassKit CSS β use both together for complete coverage of the glass stack.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/new-component) - Commit your changes (
git commit -m 'Add new component') - Push to the branch (
git push origin feature/new-component) - Open a Pull Request
MIT β Copyright (c) 2026 Jungherz GmbH
See CHANGELOG.md for a detailed list of changes.
Built on π§ GlassKit CSS by Jungherz GmbH with lots of β€οΈ for detail.