๐Ÿ‡ฉ๐Ÿ‡ช DE

Glassmorphism UI Library

A complete CSS component library with glassmorphism aesthetics โ€“ built by Jungherz GmbH. Pure CSS, no dependencies, Dark & Light mode.

v1.40.0 CSS only Dark + Light 38 Components

Installation

Include one CSS file โ€“ done. Optionally add a theme override for custom brand colors.

<!-- Base Library --> <link rel="stylesheet" href="glasskit.css"> <!-- Optional: Theme Override --> <link rel="stylesheet" href="theme-override.css"> <!-- Dark Mode (Default) --> <html data-theme="dark"> <!-- Light Mode --> <html data-theme="light">

Design Tokens

All visual values are controlled via CSS Custom Properties.

Color Palette

TokenDark ValueUsage
--gl-color-primary#f5a623Primary color, buttons, active elements
--gl-color-primary-dark#d4692aGradient end value
--gl-color-primary-textvar(--gl-color-primary)Text and line icons in the primary colour: prose links, the active tab, the primary modal action, accent list rows, the range value, the accordion — since 1.26.0, see Primary as text
--gl-color-primary-on-glasscolor-mix(in srgb, var(--gl-color-primary-text) 60%, #ffffff)The primary as text on the glass of a card, a modal or a sheet: links, the range value, accent rows, the primary modal action (since 1.35.0); in light mode the primary text
--gl-focus-ringcolor-mix(in srgb, var(--gl-color-primary) 15%, #ffffff)The keyboard’s mark, one solid colour; --gl-border-focus and --gl-shadow-focus take it. In the light theme var(--gl-color-primary-on-surface) — since 1.40.0, see Focus marks
--gl-color-text#ffffffDefault text color
--gl-color-text-mutedrgba(255,255,255,0.65)Secondary text
--gl-color-success#34c759Success status
--gl-color-error#ff3b30Error status
--gl-color-warning#ffcc00Warning status
--gl-color-{state}-surface, -border, -on-surfacecolor-mix(…)Tinted fill, border and text of a state badge, derived from the state colour — for success, error and, since 1.18.0, warning

Glass Surfaces

TokenValueUsage
--gl-surface-1rgba(โ€ฆ, 0.08)Subtlest surface (status)
--gl-surface-2rgba(โ€ฆ, 0.10)Default (inputs, cards)
--gl-surface-3rgba(โ€ฆ, 0.14)Nav pills, badges
--gl-surface-4rgba(โ€ฆ, 0.16)Hover states
--gl-surface-5rgba(โ€ฆ, 0.22)Strong hover
--gl-surface-milk-strongrgba(โ€ฆ, 0.75)Secondary button

Gloss

TokenValueUsage
--gl-gloss-cardlinear-gradient(90deg, โ€ฆ)Highlight along the top edge of .glass-card--glow
--gl-gloss-modallinear-gradient(90deg, โ€ฆ)The same on .glass-modal
--gl-gloss-progresslinear-gradient(to bottom, โ€ฆ)Sheen on the progress fill
--gl-range-thumb-border2px solid rgba(255,255,255,0.5)Rim of the range thumb
--gl-gloss-dropletradial-gradient(…), radial-gradient(…)Highlight and glow in the drop of --droplet rows (since 1.27.0)

Since 1.24.0, the same in both themes. data-surface="solid" turns them off, together with the blur and the --gl-inset-* shine.

Spacing & Radii

TokenValueTokenValue
--gl-space-2xs4px--gl-radius-xs8px
--gl-space-xs8px--gl-radius-sm12px
--gl-space-sm12px--gl-radius-input14px
--gl-space-md16px--gl-radius-btn16px
--gl-space-lg20px--gl-radius-card24px
--gl-space-xl24px--gl-radius-full9999px
--gl-space-2xl32px--gl-radius-pill50%

Density

TokenDefaultCompactUsage
--gl-control-height52px40pxInput, select, search
--gl-control-padding-xunset → --gl-space-md--gl-space-smInner left/right padding of input, select, textarea
--gl-btn-height56px40pxButton
--gl-btn-height-sm44px32pxButton --sm
--gl-btn-height-lg64px48pxButton --lg
--gl-textarea-min-height120px88pxTextarea
--gl-list-item-min-height56px48pxList row
--gl-list-item-padding-yunset → --gl-space-md10pxList row, top and bottom
--gl-toggle-width × -height52 × 30px44 × 26pxToggle track
--gl-toggle-thumb-size22px18pxToggle thumb
--gl-check-size24px20pxCheckbox, radio
--gl-modal-action-height52px44pxModal actions

Theming (Light / Dark)

The theme is controlled via data-theme on the <html> element.

// Theme per JavaScript wechseln: function toggleTheme() { const html = document.documentElement; const current = html.getAttribute('data-theme'); html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark' ); }

Primary as text

The primary does two jobs. As a fill it carries white text: buttons, the checked toggle, the current step, the chosen day. As text it stands on the page or the glass itself: links in .glass-prose, the icon and label of the active tab, the primary modal action, accent list rows, the value of a range, the accordion trigger under the pointer and the chevron of an open section. Since 1.26.0 the second job has a token of its own, --gl-color-primary-text. Unset by a project, it is the primary, so nothing changes.

A brand that darkens its primary so that white button text passes 4.5:1 gets a tone too dark for text on a dark background. Lighten the text in the dark theme, and the buttons keep their colour:

:root, [data-theme="dark"] { --gl-color-primary-text: color-mix(in srgb, var(--gl-color-primary) 18%, #ffffff); } [data-theme="light"] { --gl-color-primary-text: var(--gl-color-primary); }

Write the light rule after the dark one: :root matches a page in the light theme too, and of two matching rules the later one wins. The right tone depends on the background, so measure it there. On GlassKit’s own dark background with #c25028 as primary, this mix takes a link on a glass card from 1.37:1 to 5.05:1.

Since 1.35.0 accent text on the glass of a card, a modal or a sheet takes --gl-color-primary-on-glass: in dark mode the primary text with 40 % white, because a card is brighter than the page. GlassKit’s own orange read 4.18:1 as a link on a card in dark mode; now it reads 5.54:1, and on the page it stays at 5.69:1. In light mode the token is the primary text. Links and chips of the page’s own on a card use it the same way, color: var(--gl-color-primary-on-glass). The bright top of a .glass-card--glow stays out of reach for accent text: put it lower in the card, or use the text colour there.

The active tab takes --gl-tab-bar-active-color for its icon and label where that is set, else --gl-color-primary-text. The spotlight of the floating bar lightens the active item more than any other surface, so a brand may need a lighter tone there, up to white. Set it per theme like the text colour; var(--gl-color-primary-text) in the light rule gives the default back. Glow and dot stay in the primary, the spotlight in --gl-tab-bar-spotlight-color.

Since 1.31.0 muted text, card text and icons reach WCAG AA by default, in both themes. The primary as text does not, on purpose: GlassKit’s own orange reads 2.43:1 as text on the light page, but a darker brand may pass already or need far less, and a fixed mix with black would darken every brand alike. Where the light theme keeps the orange, darken it there; --gl-color-primary-on-surface, the ink of the primary badge, reaches 5.8:1. White on the primary button is part of the look as well, 2.7:1 in the light theme and 2.0:1 in the dark one on the lightest stop of the gradient; --gl-color-on-primary switches it to a dark ink that reaches 4.6:1 or more on every stop:

[data-theme="light"] { --gl-color-primary-text: var(--gl-color-primary-on-surface); } /* Dark text on the primary button, in both themes */ :root, [data-theme="dark"], [data-theme="light"] { --gl-color-on-primary: color-mix(in srgb, var(--gl-color-primary) 17%, #000); }

Since 1.32.0 both ways have tokens of their own. To keep white text, darken the fill and leave the brand colour alone, which also draws focus rings, borders, the tab bar’s spotlight and the accent as text. --gl-btn-primary-bg is the whole background of the primary button, unset today’s gradient; --gl-color-primary-fill is the flat accent fill that carries text — the chosen day of date strip and calendar, the number of the current step —, unset the primary. On GlassKit’s own orange, white reaches 4.5:1 from 75 % primary in the light theme and from 65 % in the dark one, and the gradient below stays at or above that on every point:

/* White text on a darker fill: the button, the chosen days, the current step */ :root, [data-theme="dark"] { --gl-color-primary-fill: color-mix(in srgb, var(--gl-color-primary) 65%, #000); } [data-theme="light"] { --gl-color-primary-fill: color-mix(in srgb, var(--gl-color-primary) 75%, #000); } :root, [data-theme="dark"], [data-theme="light"] { --gl-btn-primary-bg: linear-gradient(135deg, var(--gl-color-primary-fill), color-mix(in srgb, var(--gl-color-primary-dark) 80%, #000)); }

On the chosen day of the date strip the weekday is now as strong as the number (since 1.32.0); faded, it stayed below 4.5:1 even on a fill the number reaches it on.

The dark ink reaches the radio now as well: its dot follows --gl-color-on-primary unless --gl-radio-dot-color is set, as the checkbox tick does (since 1.32.0). Where the brand colour is set at run time, the browser can pick the ink: contrast-color() gives black or white, whichever stands out more against the primary — black for GlassKit’s orange in both themes, 5.5:1 or more on every stop of the button. The 17 % ink stays the fallback where a browser does not know it yet:

:root, [data-theme="dark"], [data-theme="light"] { --gl-color-on-primary: color-mix(in srgb, var(--gl-color-primary) 17%, #000); } @supports (color: contrast-color(red)) { :root, [data-theme="dark"], [data-theme="light"] { --gl-color-on-primary: contrast-color(var(--gl-color-primary)); } }

contrast-color() looks at the primary alone. With a fill of your own, or a gradient that ends much darker, measure the other end as well.

Focus marks

Since 1.40.0 every control shows where the keyboard is, in one solid colour: --gl-focus-ring. Press Tab through these:

Buttons, pills and the theme toggle draw it as a 2 px outline that stands 2 px clear of them, and the actions of a modal inside the action, where the modal would cut it off. Until 1.39.0 these had no mark at all. Every other part takes the ring of --gl-shadow-focus or --gl-border-focus, now 2 px wide. Both were tints of the primary and read below 3:1, which a focus mark needs against what lies beside it (WCAG 1.4.11). Both take the solid colour now, and it follows the brand:

/* The defaults */ :root, [data-theme="dark"] { --gl-focus-ring: color-mix(in srgb, var(--gl-color-primary) 15%, #ffffff); } [data-theme="light"] { --gl-focus-ring: var(--gl-color-primary-on-surface); }

GlassKit’s orange reads 3:1 or more on the page and on glass. What lies behind the glass decides how bright it gets, and a darker brand can fall short on the brightest spot, a glow card in the middle of the page in dark mode, with glass on it. A colour of your own goes into --gl-focus-ring: keep it solid and measure it there. A project that set --gl-border-focus or --gl-shadow-focus itself keeps its own ring until it drops them, and an outline of its own on :focus-visible is no longer needed.

In forced colors, Windows’ high contrast mode, the browser drops box-shadow and with it every ring. There each part shows an outline in the text colour, inside the rows of lists, accordions and menus, which clip what lies outside them.

Right to left

Since 1.20.0 the components follow the writing direction. Set dir="rtl" on <html> or on any container: paddings, insets and borders are logical properties, so labels and hints, the search icon, list dividers, popover anchors, tab badges, modal actions, the toast’s close button and the alignment of accordion, list, table and prose all mirror. Where two values have to move together — the select’s chevron and the room kept for it, the toggle’s thumb and its travel, the progress fill’s gradient — the rule sits under :dir(rtl): Chrome and Edge from 120, Safari from 16.4, Firefox from 49. An older browser keeps those parts left to right instead of tearing them apart.

Printing

Print dialogs leave out background graphics by default, and some states are drawn with a background only. Since 1.21.1 those parts keep their colours on paper — the checked box and its tick, the chosen radio, the toggle’s track, the progress fill, the current step, the chosen day of date strip and calendar, the slider thumb and the tone dots — while the rest of the page prints economically. Nothing to set. A dark page prints its light text and surfaces on white paper; a page meant for printing switches to the light theme for it:

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'); });

Density (Compact)

Admin screens, desktop layouts and narrow forms want denser controls. Since 1.21.0 the control sizes are density tokens, and data-density="compact" on the <html> element swaps in a smaller set: fields and buttons 40px high instead of 52 and 56, smaller toggles, checkboxes and radios, denser list rows and modal actions. Colours, type and spacing stay as they are. Without the attribute every control keeps the size it has always had.

Default
Compact

The preview sets the attribute on its right column. On a page it belongs on <html>, next to data-theme: an element further down that carries data-theme="dark" re-declares the default sizes.

<html data-theme="dark" data-density="compact"> // Switch at runtime: document.documentElement.setAttribute('data-density', 'compact'); document.documentElement.removeAttribute('data-density'); /* One token for both densities – or for compact only: */ :root { --gl-btn-height: 48px; } [data-density='compact'] { --gl-btn-height: 36px; }

The toggle's thumb inset and travel — in either writing direction — and the first-line alignment of toggle, checkbox and radio are derived from the tokens, so any size a project sets stays aligned. The rules read every size with today's value as fallback, like --gl-modal-max-width, so a page whose token sheet predates them keeps the old sizes.

Surfaces (Solid, Tint)

Not every screen wants glass: admin screens, forms on a plain background, designs without it. Since 1.24.0 data-surface="solid" on the <html> element makes every surface opaque and turns blur and gloss off. Each surface takes the colour the glass shows over the theme’s middle background, --gl-color-bg-mid: cards, fields and buttons keep their order, and a re-branded background carries over. The inner shine (--gl-inset-*), the highlight along the top edge of the glow card and the modal, the sheen on the progress fill and the rim of the range thumb are off. Borders, drop shadows, the tints of badges and chips and the dimming overlay stay as they are. Without the attribute nothing changes.

A page with a background of its own — a brand colour, a photo, a pattern — takes data-surface="tint" instead (since 1.25.0). A surface mixed over --gl-color-bg-mid would stand out there as a foreign block, so the surfaces in the flow of the page become see-through tints of the text colour, nothing blurred: a field on a card stays as see-through as the card, and its contrast follows the text. What lies over other content keeps the solid values — modal, sheet, popover, toast and the tab bars, with the fields and rows inside them — and so does the secondary button, a light plate for dark ink. Blur and gloss are off as with solid; a blur behind dialogs stays where a project sets --gl-blur-overlay.

Glass
Solid
Tint

The preview sets the attribute on its second and third card. On a page it belongs on <html>, next to data-theme and data-density; a theme scope further down — an element with its own data-theme — stays solid. Everything runs through tokens, so a project tunes the preset as it tunes a theme. GlassKit Elements carries the attribute into its shadow roots, as it does with data-theme.

<html data-theme="dark" data-surface="solid"> // Switch at runtime: document.documentElement.setAttribute('data-surface', 'solid'); document.documentElement.removeAttribute('data-surface'); /* Tune it like a theme – here a darker card surface in dark mode: */ [data-surface='solid'] { --gl-surface-2: #17313d; } /* Both presets mix their opaque surfaces over --gl-color-bg-mid; the panels of the tint preset take them as --gl-solid-surface-1 โ€ฆ -5 and --gl-solid-card-glow-*. A brand sets the middle colour to its own: */ [data-surface='tint'] { --gl-color-bg-mid: #5c1310; }

The gloss is made of tokens now as well: --gl-gloss-card, --gl-gloss-modal, --gl-gloss-progress and --gl-range-thumb-border (see Design Tokens). Before 1.24.0 they were literals that a project’s own solid skin could not reach.

Background

The glass effect requires a colored background.

ClassDescription
.glass-bgFull background with aurora light effects
.glass-bg--has-tab-barModifier: bottom padding for tab bar
.glass-bg--has-tab-bar-floatingModifier: bottom padding for floating tab bar + accessory
<div class="glass-bg glass-bg--has-tab-bar"> <!-- Inhalte --> </div>

Pill-Buttons

Round icon buttons for navigation and actions.

ClassDescription
.glass-pillStandard glass circle (46ร—46px)
.glass-theme-togglePill with moon/sun icon switch

Tab-Bar

Fixed bottom navigation with glass background and optional badge. The items sit in the middle of the bar; on an iPhone with viewport-fit=cover they keep to the top, above the home indicator (since 1.28.0; before, they sat at the top everywhere, with 26 px empty below them on Android and the desktop).

<nav class="glass-tab-bar"> <button class="glass-tab-bar__item is-active"> <span class="glass-tab-bar__icon"><!-- SVG --></span> <span class="glass-tab-bar__label">Start</span> </button> <button class="glass-tab-bar__item"> <span class="glass-tab-bar__icon"> <!-- SVG --> <span class="glass-tab-bar__badge">3</span> </span> <span class="glass-tab-bar__label">Contracts</span> </button> </nav>

Icons and tokens

Since 1.28.0 every state colours the icon box, and an SVG strokes in currentColor. So the icon can be anything that takes a colour — a font icon such as Material Icons, an <img> drawn as a mask, an element of your own — and it follows active and hover, the lens of the drop and the drag. A font icon takes the box’s 24 px unless its own class sets a size. Give it aria-hidden="true": its text would join the tab’s name. Six tokens, all unset, so nothing changes until a project sets them:

TokenUnsetEffect
--gl-tab-bar-color--gl-icon-mutedIcon and label of the inactive tabs
--gl-tab-bar-hover-color--gl-icon-defaultIcon and label of a hovered tab; set it as well when the colour above is lighter
--gl-tab-bar-label-size10pxSize of the labels
--gl-tab-bar-icon-size24pxSize of the icon box and of an SVG in it (since 1.33.0); a compact bar takes 18 px
--gl-tab-bar-active-weight--gl-font-weight-mediumWeight of the active tab’s label (since 1.33.0); every label is medium unless this is set
--gl-tab-bar-z-index900Stacking of the fixed bar and of the floating dock
<button class="glass-tab-bar__item"> <span class="glass-tab-bar__icon"> <span class="material-icons" aria-hidden="true">home</span> </span> <span class="glass-tab-bar__label">Home</span> </button> :root { --gl-tab-bar-color: var(--gl-icon-strong); --gl-tab-bar-hover-color: var(--gl-color-text); --gl-tab-bar-label-size: 12px; }

Since 1.33.0 the items move only what a state changes: colour, surface, shadow, opacity and the press (transition-property: color, background-color, box-shadow, opacity, transform). With all, a bar that changed its layout — upright, floating, a rail — let padding, width and height glide for 250 ms, and an item measured in that time was not yet its size.

A dot without a count

Since 1.38.0 .glass-tab-bar__badge--dot says “something needs you here” without a number — an open payment at the account tab: a 10 px dot at the icon’s corner in the plain error red, with a 2 px ring in the colour behind the bar that sets it off the icon. In the sidebar it stands at the row’s end, as the count does. The dot says nothing to a screen reader: put the reason into the item’s name. Tokens set the count and the dot alike; the sidebar keeps its place at the row’s end and takes size and ring.

TokenUnsetEffect
--gl-tab-bar-badge-size16pxHeight and least width of the count; the dot is 10 px unless set
--gl-tab-bar-badge-top-4pxFrom the icon’s top; the dot -2 px
--gl-tab-bar-badge-end-8pxFrom the icon’s end; the dot -3 px
--gl-tab-bar-badge-ring-width0pxA ring around it; the dot 2 px
--gl-tab-bar-badge-ring-color--gl-color-bg-midThe ring’s colour, the page’s middle background
<button class="glass-tab-bar__item" aria-label="Account, a payment is open"> <span class="glass-tab-bar__icon"> <!-- SVG --> <span class="glass-tab-bar__badge glass-tab-bar__badge--dot"></span> <!-- no text --> </span> <span class="glass-tab-bar__label">Account</span> </button>

Sidebar, rail and top bar

Since 1.32.0 the same navigation goes where the layout needs it, not only along the bottom edge. The items stay as they are — icon, label, badge, the active state, the drop — and only the bar’s layout changes. --vertical makes it a column for a sidebar: placed by the page’s layout, a grid column for example, and as high as its container, one item per row with the icon before the label at body size and the badge at the row’s end. The active row stands on a surface of its own, --gl-tab-bar-active-bg where that is set, else --gl-surface-3. With --rail as well, the column shows the icons alone, --gl-tab-bar-rail-width (72 px) wide: the label stays the item’s name for assistive technology, hidden from view, so show it in a tooltip.

<div class="app"> <!-- display: grid; grid-template-columns: 220px 1fr; height: 100dvh --> <nav class="glass-tab-bar glass-tab-bar--vertical" aria-label="Sections"> <a class="glass-tab-bar__item is-active" href="/"> <span class="glass-tab-bar__icon"><!-- SVG --></span> <span class="glass-tab-bar__label">Home</span> </a> <a class="glass-tab-bar__item" href="/invoices"> <span class="glass-tab-bar__icon"><!-- SVG --><span class="glass-tab-bar__badge">3</span></span> <span class="glass-tab-bar__label">Invoices</span> </a> </nav> <main>โ€ฆ</main> </div> <!-- Icons alone --> <nav class="glass-tab-bar glass-tab-bar--vertical glass-tab-bar--rail" aria-label="Sections">โ€ฆ</nav>

With --start or --end the column is fixed to that edge of the viewport instead, full height and clear of that edge’s safe area; start and end follow the writing direction. That is the side the thumbs reach on a large phone held sideways or a foldable held open, where the system moves its own controls to the edge as well. The page keeps that side free, as it keeps the bottom free for the bar there. The items stay when the layout changes, so the page switches the classes — on a media query, or by the user’s choice:

<nav class="glass-tab-bar" id="tabs" aria-label="Sections">โ€ฆ</nav> <script> const tabs = document.getElementById('tabs'); const side = matchMedia('(orientation: landscape) and (min-width: 700px)'); const layout = () => { tabs.classList.toggle('glass-tab-bar--vertical', side.matches); tabs.classList.toggle('glass-tab-bar--start', side.matches); // the page keeps the bar's side free document.body.style.paddingInlineStart = side.matches ? `${tabs.offsetWidth}px` : ''; }; side.addEventListener('change', layout); layout(); </script>

--top lays a horizontal bar out for the top of a page: in the flow — put it in a sticky header —, its border at the bottom, no room for the home indicator, and from a viewport of 640 px the icon beside the label.

<header style="position: sticky; top: 0; z-index: 10"> <nav class="glass-tab-bar glass-tab-bar--top" aria-label="Sections">โ€ฆ</nav> </header>

Every bar shows a ring for the keyboard now, --gl-shadow-focus on the focused item; until 1.32.0 the items had none. An item may also be a link, <a class="glass-tab-bar__item" href="โ€ฆ">, so a view opens in a new tab as well; it looks like the button. In GlassKit Elements the layouts are attributes: <glk-tab-bar orientation="vertical"> with rail (or rail="labels") and dock="start" or "end", and placement="top"; slot="leading" and "trailing" fill the places before and after the items.

ClassDescription
.glass-tab-bar--verticalColumn for a sidebar, placed by the page’s layout and as high as its container; scrolls on its own where the items do not fit
.glass-tab-bar--railWith --vertical: icons alone, --gl-tab-bar-rail-width (72 px) wide, the label hidden from view, the badge at the icon’s corner
.glass-tab-bar--start, --endWith --vertical: fixed to that edge of the viewport, full height, clear of the safe area; mirrored right to left
.glass-tab-bar--topHorizontal bar for the top of a page, in the flow, its border at the bottom; the icon beside the label from 640 px
a.glass-tab-bar__itemAn item as a link, without underline
.glass-tab-bar--labelsWith --rail: the label under the icon; the items fill the rail’s width and are at least --gl-tab-bar-rail-item-height high, rounded by --gl-tab-bar-item-radius (since 1.33.0, see A rail with labels)
--floating with --vertical and --railA rail of its own glass: as high as its container, with the corners of a card; with --start or --end as well, at that edge of the viewport, full height less --gl-tab-bar-floating-inset at both ends, that far from its edge, clear of the safe areas (since 1.33.0)
.glass-tab-bar__leading, __trailingPlaces before and after the items: above and below them in a column, the trailing one at the far end; before and after them in a row (since 1.33.0)
.glass-tab-bar__accessory-itemThe accessory over its name, .glass-tab-bar__accessory-label (since 1.33.0)

In the column a long label is cut with an ellipsis and ends before the badge: while a badge shows, the row keeps --gl-tab-bar-badge-room free at its end.

TokenUnsetEffect
--gl-tab-bar-rail-width72pxWidth of the rail
--gl-tab-bar-badge-room32pxRoom kept at the end of a sidebar row while it shows a badge, so the label ends before it
--gl-tab-bar-active-bg--gl-surface-3 in a columnThe active row’s surface; on the bottom bar unset means none
--gl-tab-bar-rail-item-height62pxLeast height of an item in a rail with labels (since 1.33.0)
--gl-tab-bar-item-radiusa capsule; 16px in a rail with labelsCorners of the floating items, of a labelled rail’s items and of the drop (since 1.33.0)
--gl-tab-bar-floating-inset16pxDistance of a floating rail at an edge from that edge and from the viewport’s ends (since 1.33.0)
--gl-tab-bar-rail-radius--gl-radius-cardCorners of a floating rail, docked or not (since 1.33.0)

A compact top bar

Three tokens since 1.33.0 shape the items without a change to GlassKit’s global ones: --gl-tab-bar-icon-size sizes the icons, --gl-tab-bar-item-radius rounds the drop and the floating items — unset a capsule —, and --gl-tab-bar-active-weight sets the active label’s weight. A top bar drawn as a compact track, like a segmented control, takes the control’s 18 px icons and 12 px corners:

<nav class="glass-tab-bar glass-tab-bar--top glass-tab-bar--droplet" aria-label="Sections" style="--gl-tab-bar-icon-size: 18px; --gl-tab-bar-item-radius: 12px; --gl-tab-bar-active-weight: var(--gl-font-weight-semibold)">โ€ฆ</nav>

A rail with labels

Since 1.33.0 --labels next to --rail keeps the label under the icon, as on the floating bar: a desktop rail that names its items. The items fill the rail’s width, --gl-tab-bar-rail-width, and are at least --gl-tab-bar-rail-item-height (62 px) high; --gl-tab-bar-item-radius rounds them and the drop, unset 16 px, the corners of a button. A long label ends in an ellipsis, and the badge stays at the icon’s corner.

__leading and __trailing are places inside the bar before and after the items: in a column above them — a mark, the accessory — and at its far end — a foot such as the account; in a row before and after them. .glass-tab-bar__accessory-item stands the accessory over its name, __accessory-label. The name is the button’s aria-label as well, so the visible one is aria-hidden, and a click on it should press the button.

With --floating the rail is a pane of its own glass, docked or not: as high as its container, with the corners of a card, --gl-tab-bar-rail-radius, and room around its items — in the flow, a grid column of the page, as in the preview. With --start or --end as well, it stands at that edge of the viewport instead: as high as the viewport less --gl-tab-bar-floating-inset (16 px) at both ends, that far from its edge, clear of the safe areas and mirrored right to left. The compact upright pill of the vertical dock, without --rail, stays a capsule.

<nav class="glass-tab-bar glass-tab-bar--floating glass-tab-bar--vertical glass-tab-bar--start glass-tab-bar--rail glass-tab-bar--labels glass-tab-bar--droplet" aria-label="Sections" style="--gl-tab-bar-rail-width: 96px"> <div class="glass-tab-bar__leading"> <img src="mark.svg" alt="" width="40" height="40"> <div class="glass-tab-bar__accessory-item"> <button class="glass-tab-bar__accessory glass-tab-bar__accessory--accent" aria-label="Assistant"><!-- SVG --></button> <span class="glass-tab-bar__accessory-label" aria-hidden="true">Assistant</span> </div> </div> <a class="glass-tab-bar__item is-active" href="/"> <span class="glass-tab-bar__icon"><!-- SVG --></span> <span class="glass-tab-bar__label">Home</span> </a> <!-- more items --> <div class="glass-tab-bar__trailing"><!-- the account --></div> </nav> /* The page keeps the rail's side free: its width and the inset on both sides */ main { padding-inline-start: calc(96px + 2 * 16px); }

In GlassKit Elements the rail is <glk-tab-bar floating orientation="vertical" dock="start" rail="labels">, with the mark and a <glk-tab-accessory show-label> in slot="leading" and the foot in slot="trailing".

Tab-Bar โ€“ Floating

Pill-shaped, centered, floating variant inspired by iOS 26 Liquid Glass. Pair with an optional .glass-tab-bar__accessory capsule (e.g. search, compose) โ€” sits next to the bar with its own glass material. The active tab gets a soft radial Spotlight halo instead of the underline dot, in --gl-tab-bar-spotlight-color (the primary). Its icon and label take --gl-tab-bar-active-color where that is set, else --gl-color-primary-text (since 1.26.0, see Primary as text).

<div class="glass-tab-bar-dock"> <nav class="glass-tab-bar glass-tab-bar--floating"> <button class="glass-tab-bar__item is-active"> <span class="glass-tab-bar__icon"><!-- SVG --></span> <span class="glass-tab-bar__label">Home</span> </button> <!-- more items --> </nav> <button class="glass-tab-bar__accessory" aria-label="Search"> <!-- SVG --> </button> </div>

Accessory accent variants

Add --accent, --success, or --error to highlight the accessory with a colored capsule (filled background, white icon). Since 1.28.0 the icon colour sits on the button, so a font icon in the accessory follows hover and the variants like an SVG, and every variant keeps its icon colour on hover.

ClassDescription
.glass-tab-bar-dockWrapper: fixed bottom-center, holds bar + accessory
.glass-tab-bar-dock--accessory-leftModifier: place accessory on the left
.glass-tab-bar--floatingModifier on .glass-tab-bar: pill shape, max-content width
.glass-tab-bar__accessoryStandalone glass capsule next to the bar
.glass-tab-bar__accessory--accentFilled with primary color
.glass-tab-bar__accessory--successFilled with success color
.glass-tab-bar__accessory--errorFilled with error color
.glass-bg--has-tab-bar-floatingBackground padding helper for the floating variant
.glass-tab-bar--dropletModifier: the active item sits on a drop of glass that glides to the next one (since 1.27.0, see Droplet)
.glass-tab-bar-dock--verticalModifier: the dock upright at the end edge, vertically centred; the bar inside takes --vertical (since 1.32.0)
.glass-tab-bar-dock--startWith --vertical: at the start edge instead

Upright at a side

Since 1.32.0 the floating bar also stands upright at a side, where a thumb rests on a phone held sideways. .glass-tab-bar-dock--vertical on the dock and --vertical on the bar stack the items as they stand on the bottom edge, icon over label, with the accessory below them (above with --accessory-left), vertically centred at the end edge; .glass-tab-bar-dock--start moves it to the other one. Both keep clear of the safe area and follow the writing direction. With --droplet the drop runs up and down:

<div class="glass-tab-bar-dock glass-tab-bar-dock--vertical"> <nav class="glass-tab-bar glass-tab-bar--floating glass-tab-bar--vertical"> <!-- the items --> </nav> <button class="glass-tab-bar__accessory" aria-label="Search"><!-- SVG --></button> </div>

Active item surface

Since 1.27.0 the active item can stand on a surface of its own, like the selected tab of iOS 26. Four tokens, all unset, so nothing changes until a project sets them: --gl-tab-bar-active-bg and --gl-tab-bar-active-shadow draw a capsule behind the active item, --gl-tab-bar-spotlight-opacity: 0 turns the floating bar’s spotlight off, and --gl-tab-bar-active-glow: none the glow on the icon, which reads as a blur on a light capsule.

:root { --gl-tab-bar-active-bg: color-mix(in srgb, var(--gl-color-text) 14%, transparent); --gl-tab-bar-active-shadow: 0 2px 8px rgba(0, 0, 0, 0.12), var(--gl-inset-subtle); --gl-tab-bar-spotlight-opacity: 0; --gl-tab-bar-active-glow: none; }

A nearly opaque white capsule, rgba(255, 255, 255, 0.9), lets a dark brand primary stay the active tab’s colour in the dark theme, where the spotlight would leave it unreadable. The capsule is the item’s background, so it is rounded like the item: a pill on the floating bar, 12px on the fixed one. Since 1.33.0 --gl-tab-bar-item-radius rounds the floating items and the drop together, unset a capsule: rounded squares in a rail, the 12 px of a segmented control in a compact top bar.

Droplet

Since 1.27.0 .glass-tab-bar--droplet makes the selection fluid. The active item sits on a drop of glass that floats a little above the bar. Tap another tab and the drop runs there like water on a pane: the leading edge goes first, the trailing one follows, the drop runs flatter on the way, settles with a small wobble, and the icons it passes grow as under a lens. A finger can also drag it along the bar; let go, and the tab below it is chosen. It is the most noticeable upgrade an app on the floating bar can take, and it changes nothing else: the markup stays, and where the script does not run the bar keeps its spotlight. Try it:

The stylesheet draws the drop. The motion comes from glasskit-droplet.js, an optional module in the same package without dependencies, about 4 KB minified and under 2 KB compressed. It follows .is-active, which the page keeps setting its own way, and clicks the tab a drag ends on, so the page’s own click handler makes the choice:

<nav class="glass-tab-bar glass-tab-bar--floating glass-tab-bar--droplet"> <!-- the items, the active one with .is-active --> </nav> <script type="module"> import { attachDroplets } from 'https://cdn.jsdelivr.net/npm/@jungherz-de/glasskit@1.40/glasskit-droplet.js'; attachDroplets(); // every --droplet row in the page; attachDroplet(row) for one </script>

The drop takes the active item’s surface where a project set one (--gl-tab-bar-active-bg, -shadow), else a light glass of its own; its highlight is --gl-gloss-droplet. data-surface="solid" and "tint" make it opaque like every other surface, and with prefers-reduced-motion it moves without travelling. A transform on an ancestor — an app that zooms in on start, a popover that opens from scale(0.96) — leaves it on its item, at rest and while the transform runs (since 1.28.0). Segmented controls take the same drop. In GlassKit Elements it is one attribute: <glk-tab-bar floating droplet>.

In a column — a sidebar, the upright floating bar — the drop runs along y (since 1.32.0). The script reads the row’s flex-direction or its aria-orientation="vertical", sets data-droplet-axis="y" and the directions "down" and "up", and a finger drags the drop up and down while a sideways swipe goes to the page. A horizontal bar at the top of a page, static or floating, runs it as the bottom bar does. Try the sidebar:

Droplet timing

Since 1.32.0 the motion is tokens, read by the stylesheet and by glasskit-droplet.js alike. They are inherited, so they reach <glk-tab-bar droplet> and <glk-segmented droplet> in GlassKit Elements too. Unset, they keep the drop as it was: it comes to rest about 830 ms after a tap.

TokenUnsetEffect
--gl-droplet-duration0.3sThe leading edge, and the edges across the way
--gl-droplet-easingcubic-bezier(0.3, 1.2, 0.5, 1)Its curve
--gl-droplet-trail-duration0.5sThe trailing edge, pulled after it
--gl-droplet-trail-delay40msHow long the trailing edge waits
--gl-droplet-trail-easingcubic-bezier(0.5, 1.5, 0.45, 0.92)Its curve, with the overshoot
--gl-droplet-lift0.86How flat the drop runs on the way; 1 squashes nothing
--gl-droplet-lift-duration280msHow long it runs flat
--gl-droplet-settle-duration0.55sThe spring back into shape
--gl-droplet-settle-easingcubic-bezier(0.34, 1.8, 0.5, 1)Its curve, the wobble at the end
--gl-droplet-lens-duration0.4sThe item below the drop growing as under a lens
--gl-droplet-lens-easingcubic-bezier(0.34, 1.6, 0.5, 1)Its curve

A product that keeps its motion within 240 ms ease-out lets the drop arrive in 240 ms without a tail, in the tab bar and the segmented control alike. With the lift’s duration at 0ms the drop does not deepen its shadow on the way either, which would otherwise settle only after about 600 ms:

:root { --gl-droplet-duration: 240ms; --gl-droplet-easing: ease-out; --gl-droplet-trail-duration: 240ms; --gl-droplet-trail-delay: 0ms; --gl-droplet-trail-easing: ease-out; --gl-droplet-lift: 1; --gl-droplet-lift-duration: 0ms; --gl-droplet-settle-duration: 0ms; --gl-droplet-lens-duration: 240ms; --gl-droplet-lens-easing: ease-out; }

A finger dragging the drop keeps its own short timing, and with prefers-reduced-motion the drop still moves without travelling.

Title

Capture Contracts
& Upload

<h1 class="glass-title">Seitentitel</h1>

Card

Photograph your document, verify edges, and upload in batch.

<div class="glass-card glass-card--glow"> <div class="glass-card__icon"><!-- SVG --></div> <p class="glass-card__text">Text</p> </div>
ClassDescription
.glass-cardBase glass surface
.glass-card--glowLight โ†’ milky gradient + light streak
.glass-card__iconCentered icon wrapper
.glass-card__textDescription text

Buttons

ClassDescription
.glass-btnBase (56px, full width)
--primaryColored gradient
--secondaryMilky white
--tertiarySubtle glass
--sm44px height
--lg64px height
--autoAuto width
:disabled, [aria-disabled="true"]Dimmed to 0.45, not-allowed cursor, no hover (since 1.21.2)
svg.glass-icon--fillDeliberately filled icon (e.g. brand logos)

On the glass of a modal or a sheet a tertiary button is one step darker in dark mode, --gl-dialog-control, and takes no blur of its own (since 1.35.0): its muted label read 3 to 4:1 there.

A disabled button — disabled, or aria-disabled="true" on an a.glass-btn, which cannot be disabled — is dimmed, shows the not-allowed cursor and no longer lifts on hover (since 1.21.2; before, it looked exactly like an enabled one). The link still navigates unless the page stops it. The same holds for pill, tab-bar accessory, modal actions, interactive list rows and the calendar’s month arrows; toggle, checkbox and radio dim their control and label, select, textarea and range dim like the input.

Buttons have hooks of their own since 1.31.0. --gl-btn-radius rounds them, unset --gl-radius-btn, which lists, the status card, toasts, the accordion and the segmented control share: --gl-btn-radius: 9999px on :root gives pill-shaped buttons and leaves those as they are. --gl-btn-padding-x is the room beside the label, unset 24 px, the padding --auto had alone; any other button had the browser’s 6 px, so a small one that took its width from its text, in a row of actions, touched the text with its edges. --sm reads --gl-btn-radius-sm and --gl-btn-padding-x-sm first, then the hooks above, and keeps its 12 px radius while none is set, so one value can shape every size.

Since 1.32.0 --gl-btn-primary-bg replaces the whole background of --primary, as the toggle and the radio have theirs; unset, it is the gradient from --gl-color-primary over -mid to -dark. A product that keeps white text starts the gradient darker this way and leaves the brand colour alone; the recipe is under Primary as text.

Badge

Standard Aktiv Fertig Offen Fehler
<span class="glass-badge glass-badge--primary">Aktiv</span>

Variants: --primary, --success, --warning (since 1.18.0) and --error. Each draws its fill, border and text from the state colour’s -surface, -border and -on-surface tokens, so re-colouring the state colour moves the whole badge.

Chips

A badge doubles as a filter chip. --interactive carries the affordance — pointer cursor, hover tint, focus ring — and --selected says which chip is on. Use a real <button> and put the state in aria-pressed, so the row works by keyboard and is announced as a toggle.

--selected deepens the colour the badge already carries instead of overruling it, so a status chip stays in its own colour when it is picked.

In dark mode a chosen badge says its label in the text colour (since 1.35.0): the accent lightened for dark surfaces read 2.2 to 3:1 on a chip over glass. The border and the tint show the selection; --gl-badge-selected-color sets the label’s colour. On the glass of a modal or a sheet a badge takes no blur of its own: the dialog blurs already, and Chromium counted its glow twice beneath a nested backdrop filter, so a chip came out 2.9:1 for white.

<button class="glass-badge glass-badge--interactive glass-badge--selected" aria-pressed="true">Active</button> <button class="glass-badge glass-badge--interactive" aria-pressed="false">Applied</button>
ClassDescription
--interactivePressable: cursor, hover tint, focus ring, press feedback
--selectedThe chip that is on — deepens the badge's own colour
--gl-badge-accentThe colour both states deepen. Set it per badge to give a chip a colour of its own.

Choice chips

Since 1.33.0 --md makes a chip to choose with, as in a sign-up form’s row of sectors: --gl-badge-md-height (36 px) high, in the small text of a form. With --interactive it takes taps on 44 px, 4 px above and below its edges, the target the other controls keep. Keep at least 8 px between the lines of a wrapping row, so the targets meet and do not overlap.

<button class="glass-badge glass-badge--md glass-badge--interactive glass-badge--selected" aria-pressed="true">Café &amp; bakery</button>
Class / tokenDescription
--mdA chip to choose with: 36 px high, the small text of a form; interactive, a tap target of 44 px
--gl-badge-md-heightIts height, unset 36px; the tap target grows to 44 px around it

In GlassKit Elements: <glk-badge interactive size="md">.

Avatar

S
M
L
<div class="glass-avatar glass-avatar--lg">L</div>

Since 1.37.0 --gl-avatar-size sets any size, on the avatar or a container around it, and --gl-avatar-font-size the initials’ size with it. In the leading box of a list row an avatar takes the box’s size, 28 px, and 40 px in --lg, the avatar of a contact list: 44 px was cut by the box, 32 px looked lost in it. The token on the avatar itself still decides.

<span class="glass-list__leading glass-list__leading--lg"><span class="glass-avatar">LH</span></span> <!-- 40px --> .contacts { --gl-avatar-size: 36px; --gl-avatar-font-size: 14px; }

Divider


<hr class="glass-divider">

Status Notice

No documents captured yet.

--gl-status-color sets the message’s colour (since 1.35.0), muted text unless set. A word in <b> inside, such as a package name, takes the text colour with a rule of the page’s own. Since 1.36.0 the message has no margin of its own. The browser’s 1em above and below it made the card 82 px high instead of 54 px wherever no reset of the page reached the <p>: on a page without one, in the shadow root of <glk-status>.

<div class="glass-status"> <svg viewBox="0 0 24 24"><!-- Icon --></svg> <p>No documents captured yet.</p> </div>

List

Grouped settings-style container for stacked rows. Items support a leading icon, title + optional subtitle, and a trailing element (chevron, value, button). Dividers between items are drawn automatically — no extra markup needed.

Settings style (icon + title + subtitle + trailing)

  • iOS 26.4 Update
    2.1 GB · Available now
  • Apple One
    Family — renews Apr 28
    $22.95
  • Notifications
  • View all subscriptions
<ul class="glass-list"> <li class="glass-list__item glass-list__item--interactive"> <span class="glass-list__leading"><svg>...</svg></span> <div class="glass-list__content"> <div class="glass-list__title">iOS 26.4 Update</div> <div class="glass-list__subtitle">2.1 GB · Available now</div> </div> <div class="glass-list__trailing"><svg>...</svg></div> </li> <!-- More itemsโ€ฆ dividers appear automatically --> <li class="glass-list__item glass-list__item--center">View all</li> </ul>

Compact menu (centered text, no icons)

  • Share document
  • Duplicate
  • Delete
<ul class="glass-list"> <li class="glass-list__item glass-list__item--center glass-list__item--interactive">Share document</li> <li class="glass-list__item glass-list__item--center glass-list__item--interactive">Duplicate</li> <li class="glass-list__item glass-list__item--center glass-list__item--interactive glass-list__item--danger">Delete</li> </ul>

Grouped sections with large icons

Recommendations
  • Review downloaded media
    Save up to 1.38 GB. Review all videos and audio files downloaded on your device and remove them as needed.
  • AusweisApp Bund
    Version 2.5.1
    24 MB
Audiobooks
  • Ghost in the Machine
    Partially watched
    237.1 MB
  • View all downloads
<div class="glass-list__section-header">Recommendations</div> <ul class="glass-list"> <li class="glass-list__item glass-list__item--interactive"> <span class="glass-list__leading glass-list__leading--lg"><svg>...</svg></span> <div class="glass-list__content"> <div class="glass-list__title">Review downloaded media</div> <div class="glass-list__subtitle glass-list__subtitle--wrap">Multi-line description...</div> </div> <div class="glass-list__trailing"> <span class="glass-list__value">24 MB</span> <svg>...</svg> </div> </li> <li class="glass-list__item glass-list__item--accent glass-list__item--center">View all</li> </ul>

Class reference

ClassDescription
.glass-listGrouped surface container
.glass-list--flushEdge-to-edge variant (no side borders / radius): reaches both edges of its container, pulled out by --gl-list-flush-inset — the card’s and the modal body’s padding inside them, else 20 px (since 1.29.1)
.glass-list--bareStrips background, border & shadow — for embedding inside .glass-popover or .glass-card.
.glass-list__itemList row (use <li>, <a>, or <button>)
.glass-list__section-headerSection label above a .glass-list — uppercase, small, muted
.glass-list__item--interactiveAdds hover / focus / active states; for the keyboard a 2 px ring in --gl-border-focus inside the row (since 1.32.0; before, only a tint showed the focus)
.glass-list__item--centerCentered single-text variant with ellipsis truncation
.glass-list__item--dangerRed text for destructive actions
.glass-list__item--accentPrimary-colored text for accent actions
.glass-list__leadingLeading slot (28×28, holds an icon SVG); --gl-list-leading-width widens it (since 1.36.0, below)
.glass-list__leading--lgLarge leading slot (40×40, rounded square, supports <img>); the same token widens it
.glass-list__contentFlexible middle slot — takes remaining width, enables truncation
.glass-list__titlePrimary text (medium weight, one line with ellipsis unless --gl-list-title-white-space lets it wrap)
.glass-list__subtitleSecondary text (small, muted, one line with ellipsis unless --gl-list-subtitle-white-space lets it wrap)
.glass-list__title-rowHolds the title and __title-trailing — a badge, a mark —, which moves under the title where the row is too narrow (since 1.35.0)
.glass-list__extraBelow the subtitle: a link, a line of chips (since 1.35.0)
.glass-list__subtitle--wrapMulti-line subtitle (up to 3 lines, clamped with ellipsis; --gl-list-subtitle-lines sets the limit)
.glass-list__trailingTrailing slot — chevron, value, button, badge
.glass-list__valueMuted text inside .glass-list__trailing (e.g. file size)
.glass-list__item.is-flashFlashes once, 1.2 s in --gl-flash-color, for an item that changed elsewhere (since 1.32.0)
.glass-list__item.is-selectedThe chosen row of a list beside its detail: a pill of raised glass inside the row, --gl-list-item-selected-bg (unset --gl-surface-4), the title semibold, the icon in the accent; with aria-current="true" (since 1.34.0, below)

Dividers between rows are rendered automatically via ::after. Items with a leading icon get an icon-aligned inset; items without — no __leading, or one carrying the hidden attribute (since 1.20.0) — get the standard padding inset. A hidden __leading or __trailing takes no room. The last item never has a divider. SVG icon convention: 24px for leading, 18px for trailing, stroke: currentColor, stroke-width: 2.

Since 1.32.0 an item that changed elsewhere — on another device, by an undo — can flash once, so the eye finds it among the others: .is-flash tints it for 1.2 s in --gl-flash-color, unset 30 % of the primary, and under prefers-reduced-motion nothing flashes. Take the class away on animationend, so the next change flashes again.

<li class="glass-list__item is-flash">โ€ฆ</li> <!-- changed elsewhere, flashes once -->

Flush

--flush takes the list from edge to edge of its container, without side borders and radius. The list grows by the container’s padding on both sides, --gl-list-flush-inset: .glass-card and .glass-modal__body set it to their own padding, elsewhere it is 20 px (--gl-space-lg) unless the container sets it. Since 1.29.1 — before, the list kept its width and only moved to the start.

Customer

  • Payments
  • Invoices
  • Tickets
<div class="glass-card"> <ul class="glass-list glass-list--flush">โ€ฆ</ul> </div> /* In a container of your own with 16px padding */ .my-panel { padding: 16px; --gl-list-flush-inset: 16px; }

Wrapping text

Title and subtitle keep to one line, cut with an ellipsis, and --wrap clamps the subtitle at three lines. Since 1.23.0 a project decides otherwise once, on :root, or for one list or one row: --gl-list-title-white-space and --gl-list-subtitle-white-space take nowrap (the default) or normal, and --gl-list-subtitle-lines sets the clamp of --wrap — 3 unless set, none shows every line. The tokens are inherited, so they reach <glk-list-item> in GlassKit Elements as well. A word wider than the column, such as an e-mail address, breaks where it has to instead of running out of the row; text still breaks at spaces. For breaks at syllables, set hyphens on the list or the page: it is inherited too and follows the page’s lang.

:root { --gl-list-title-white-space: normal; --gl-list-subtitle-white-space: normal; --gl-list-subtitle-lines: none; } /* Breaks at syllables; short words stay whole */ .glass-list, glk-list { hyphens: auto; -webkit-hyphens: auto; hyphenate-limit-chars: 10 4 4; }

Room beside the title

A row may say more than a title (since 1.35.0). .glass-list__title-row holds the title and __title-trailing beside it — a badge such as “AI”, a mark —, which moves under the title where the row is too narrow while the title keeps its ellipsis. __extra below the subtitle takes a link (“Undo”) or a line of chips. --gl-list-title-size, --gl-list-title-weight and --gl-list-subtitle-size set a row’s type, on the list or one row.

  • Opening hours
    AI
    Changed today, 09:41
  • News
    Changed yesterday
<div class="glass-list__content"> <div class="glass-list__title-row"> <div class="glass-list__title">Opening hours</div> <span class="glass-list__title-trailing"><span class="glass-badge glass-badge--primary">AI</span></span> </div> <div class="glass-list__subtitle">Changed today, 09:41</div> <div class="glass-list__extra"><a href="โ€ฆ">Undo</a></div> </div>

Since 1.36.0 --gl-list-leading-width widens __leading for more than an icon — a time and a chip before the text of a history row. The box was 28 px wide, 40 px with --lg, which cuts off what does not fit. The divider follows the token. It takes a length; for a box laid out otherwise, --gl-list-item-divider-inset says where the line starts.

  • 09:41
    Opening hours: Saturday 7:00 to 14:00
    Opening hours · Lena Hoffmann
  • 08:15
    News: Summer party
    News · Jonas Weber
<ul class="glass-list" style="--gl-list-leading-width: 84px"> <li class="glass-list__item"> <span class="glass-list__leading glass-list__leading--lg"><!-- a time and a chip --></span> <div class="glass-list__content">โ€ฆ</div> </li> </ul>

The chosen row

A compact list next to an editor marks the entry that is open (since 1.34.0): .is-selected on the row lays a pill of raised glass into it, as the chosen tree row and the active row of a sidebar have, with the title semibold and the icon in the accent. aria-current="true" tells assistive technology which entry it is — aria-selected instead where the list is a listbox. Hover and press leave the chosen row as it is, and the keyboard ring stays around it. --gl-list-item-selected-bg sets the pill’s surface, unset --gl-surface-4.

Since 1.35.0 tokens shape the chosen row otherwise: --gl-list-item-selected-inset (3 px --gl-space-xs), -radius (--gl-radius-sm), -shadow (edge light and a button’s shadow), -color (the heading colour) and -weight (semibold). 0, 0 and none for inset, radius and shadow make a flat tint over the whole row. Since 1.36.0 the title takes the row’s colour, the heading colour or -color; before, it kept its own text colour, and -color changed nothing on it.

  • Home
  • Opening hours
    Open in the editor
  • News
<li class="glass-list__item glass-list__item--interactive is-selected" aria-current="true">โ€ฆ</li>

The divider starts under the text: after the leading icon, or at the row’s padding without one. --gl-list-item-divider-inset (since 1.34.0) sets where it starts for rows laid out otherwise. A row with a drag handle before its icon lets the line run from its padding, since the handle’s width is the page’s, until the token says where the text begins.

  • News
    Component
  • Opening hours
    Component
  • Team
    Component
<ul class="glass-list" style="--gl-list-item-divider-inset: 90px"> <li class="glass-list__item"> <span class="glass-sort__handle">โ€ฆ</span> <!-- the grip first --> <span class="glass-list__leading">โ€ฆ</span> <div class="glass-list__content">โ€ฆ</div> </li> </ul>

Tree

Levels that open and close (since 1.32.0): the structure of an app, menus with submenus, folders. A row per item — a twisty that turns when the item opens, an icon, the label, something at the end such as a badge —, indented by --gl-tree-indent (20 px) per level; the level is --gl-tree-level on the row. The item’s children follow in a __group. The tree has no surface of its own: put it in a card or a sidebar.

  • My app
    • Main menu 3
      • Home
      • Services
    • Archive
<ul class="glass-tree" role="tree" aria-label="App structure"> <li role="treeitem" aria-level="1" aria-expanded="true" aria-selected="false"> <div class="glass-tree__row is-expanded" style="--gl-tree-level: 1"> <span class="glass-tree__twisty" aria-hidden="true"><!-- chevron SVG --></span> <span class="glass-tree__icon" aria-hidden="true"><!-- SVG --></span> <span class="glass-tree__label">Main menu</span> <span class="glass-tree__trailing"><span class="glass-badge">3</span></span> </div> <ul class="glass-tree__group" role="group"> <li role="treeitem" aria-level="2" aria-selected="true"> <div class="glass-tree__row is-leaf is-selected" style="--gl-tree-level: 2"> <span class="glass-tree__twisty" aria-hidden="true"><!-- chevron SVG --></span> <span class="glass-tree__label">Services</span> </div> </li> </ul> </li> </ul>

The roles are those of the WAI-ARIA tree view: role="tree" with a name on the list, "treeitem" on each item with aria-level, aria-expanded on the items with children and aria-selected, "group" on each __group; hidden on a closed one. The states are classes on the row. The tree is one tab stop: the arrow keys move between the items — Down and Up, Right opens an item or goes to its first child, Left closes it or goes to its parent — and the row of the item that has the focus takes .is-focus-visible, or :focus-visible where the row itself is focused. That keyboard is GlassKit Elements’ <glk-tree>, mirrored right to left like the twisty here, with events before an item opens, closes or is chosen, and children loaded when an item first opens.

Two states come from outside the tree. .is-error marks an item whose children could not load: the twisty turns into a “!” in the error colour, and opening the item again tries once more; GlassKit Elements sets it with load-error. .is-flash lights a row up for 1.2 s when it changed elsewhere — on another device, by an undo —, so the eye finds it among the others; --gl-flash-color tints it, unset 30 % of the primary, and under prefers-reduced-motion nothing flashes. Take the class away on animationend, so the next change flashes again:

<div class="glass-tree__row is-error" style="--gl-tree-level: 2">โ€ฆ</div> <!-- the children could not load --> <div class="glass-tree__row is-flash" style="--gl-tree-level: 2">โ€ฆ</div> <!-- changed elsewhere, flashes once --> row.classList.add('is-flash'); row.addEventListener('animationend', () => row.classList.remove('is-flash'), { once: true });
ClassDescription
.glass-treeThe list; no surface of its own
__rowOne item’s row, --gl-tree-row-height (36 px) high, indented by --gl-tree-indent (20 px) for each level of --gl-tree-level past the first
__twistyThe chevron; turns with .is-expanded, keeps its room but hides with .is-leaf, mirrored right to left
__iconThe icon before the label, --gl-tree-icon-size (18 px) square, its SVG too (since 1.34.0) — 28px for a tinted chip as list rows show; an empty one takes no room
__labelThe label, one line with ellipsis
__trailingAt the row’s end: a badge, a status, an action
__groupThe item’s children, below its row; hidden while closed
.is-expanded, .is-leafOn the row: open, or without children
.is-selectedThe chosen row: raised glass like the active row of a sidebar, semibold, its icon in the accent
.is-loadingChildren on their way: the twisty turns into a small spinner (with aria-busy="true" on the item)
.is-errorThe children could not load: the twisty turns into a “!” in the error colour; opening the item again tries once more
.is-flashA flash of 1.2 s in --gl-flash-color (unset 30 % of the primary) on a row that changed elsewhere; none under prefers-reduced-motion
.is-disabledDimmed, not-allowed cursor
.is-focus-visibleThe keyboard ring inside the row, for a script that moves the focus

Sorting

The states of a drag that reorders a list or a tree (since 1.32.0). The dragged item stays where it was, faded (.is-sort-source), while a raised copy, .glass-sort__ghost, follows the pointer in the top layer. A line shows where the item lands, before or after another one (.is-sort-before, .is-sort-after on that one), indented in a tree to the level it lands on; an item that takes it inside lights up (.is-sort-inside), and one that refuses it shows the error colour (.is-sort-refused).

  • Home
  • Services
  • About us
  • Contact

In a tree the line is indented to the level the item lands on. Here all three states show at once; a drag shows one at a time.

  • Main menu
    • Home
    • Legal notice
<li class="glass-list__item is-sort-source">โ€ฆ</li> <!-- the dragged item --> <li class="glass-list__item is-sort-after">โ€ฆ</li> <!-- it lands after this one --> <div class="glass-tree__row is-sort-inside">โ€ฆ</div> <!-- or inside this one --> <div class="glass-sort__ghost" popover="manual"> <!-- follows the pointer --> <span class="glass-sort__handle"><!-- SVG --></span>Services </div>

Items side by side — tiles in a grid — take the line upright (since 1.34.0): .is-sort-inline with .is-sort-before puts it at the item’s start edge, with .is-sort-after at its end edge. A tile that takes the item inside lights up as a row does.

News
Team
Opening hours
Submenu
Gallery
<div class="tile is-sort-inline is-sort-before">โ€ฆ</div> <!-- it lands before this tile --> <div class="tile is-sort-inside">โ€ฆ</div> <!-- or inside this one -->

With a .glass-sort__handle in the item only the handle starts a drag, so a tap on the item still chooses it, and a finger on the handle drags at once, without the long press. The drag itself is GlassKit Elements’: <glk-list sortable> and <glk-tree sortable> reorder by mouse, by touch after a long press and by keyboard — Space picks an item up, the arrow keys move it, Space drops it, Escape cancels —, announce each step, take breadcrumbs as drop targets and let the items glide into place over --gl-sort-duration (180 ms) with --gl-sort-easing (ease-out). Under prefers-reduced-motion nothing glides.

ClassDescription
.is-sort-sourceThe dragged item where it was, faded to 40 %
.is-sort-before, .is-sort-afterA 2 px line in the primary above or below the item, indented to --gl-tree-level
.is-sort-inlineWith .is-sort-before or .is-sort-after: the line stands upright at the item’s start or end edge, for items side by side — tiles in a grid (since 1.34.0)
.is-sort-insideThe item takes the dragged one inside: tinted, with a ring — since 1.34.0 also over the hover of an interactive row and over the chosen state
.is-sort-refusedThe item does not take it: the error surface and ring; a line turns red
.glass-sort__ghostThe raised copy that follows the pointer; [popover] in the top layer
.glass-sort__handleThe grip: only it starts a drag, and a finger on it drags at once

Toast

Visible via .is-visible. Variants: --success, --error, --warning.

With an action (since 1.19.0): __action is a small pill in the toast’s own tone — primary on a plain toast, the state colour on a variant — and __close the × at the end; give it an aria-label. Both have a 44 px hit area. A toast with an action may grow to 480 px, the text wraps, the buttons never shrink; it should stay until one of them is used. The toast sits --gl-toast-top from the top (56 px unless set) — set it below your header, and add env(safe-area-inset-top) with viewport-fit=cover. It stays at the top also with an action: one place for every toast.

Document uploaded successfully!
A new version is ready
<div class="glass-toast glass-toast--success"> <svg class="glass-toast__icon">...</svg> <span class="glass-toast__text">Success!</span> </div> <!-- with an action --> <div class="glass-toast is-visible" role="status"> <svg class="glass-toast__icon">...</svg> <span class="glass-toast__text">A new version is ready</span> <button class="glass-toast__action">Reload</button> <button class="glass-toast__close" aria-label="Close"><!-- ร— SVG --></button> </div>
ClassDescription
.glass-toast, .is-visibleFixed at the top, faded in while visible
--success, --error, --warningColour the icon; tint the action in the state colour
__icon, __text22 px stroked icon, message (wraps and yields)
__actionOne action: pill in the toast’s tone, 44 px hit area, never shrinks (since 1.19.0)
__closeThe ×, 28 px with a 44 px hit area (since 1.19.0)
--gl-toast-topDistance from the top, default var(--gl-space-4xl) (since 1.19.0)

In the top layer (since 1.39.0): a toast lies below a modal or a sheet, which the browser puts into the top layer, whatever its z-index — under the blurred overlay, unreadable. Give it popover="manual" and call showPopover() — again on every show, after hidePopover(), so it lies above a dialog opened since. .glass-toast[popover] undoes the popover’s own box; place and look stay the toast’s. Keep it open while it is hidden: it stays invisible and lets the pointer through, and as a live region it keeps being read. Beside a modal it can be seen, but, as everything outside the dialog, not used or announced. GlassKit Elements’ <glk-toast> does this.

<div class="glass-toast glass-toast--error" role="status" popover="manual">โ€ฆ</div> // before every show: in again, on top if (toast.matches(':popover-open')) toast.hidePopover(); toast.showPopover(); toast.classList.add('is-visible');

Popover

Anchored dropdown / menu container. Wrap a trigger and a .glass-popover in a .glass-popover-anchor. Visibility is controlled via the .is-open class — toggling it requires a tiny bit of JavaScript.

  • Share
  • Duplicate
  • Delete
<div class="glass-popover-anchor"> <button class="glass-btn glass-btn--secondary" onclick="togglePop(this)">Open menu</button> <div class="glass-popover"> <ul class="glass-list glass-list--bare"> <li class="glass-list__item glass-list__item--interactive">Share</li> <li class="glass-list__item glass-list__item--interactive">Delete</li> </ul> </div> </div> <script> // Note: do NOT name the function `togglePopover` — collides // with the native HTMLElement.togglePopover() API method. function togglePop(btn) { btn.nextElementSibling.classList.toggle('is-open'); } </script>

In the top layer

Since 1.32.0 a popover can open in the top layer. Give it the popover attribute and open it with showPopover(): it stands above every stacking context, a box with overflow: hidden around it no longer cuts it off, and while closed it takes no room, where the class version still widened the page by its box. The browser does not place it next to the trigger, though. In the top layer the popover is position: fixed: a script measures the trigger, writes left and top in viewport pixels, flips the popover to the other side where the viewport leaves no room and keeps it there while the page scrolls; data-side="top" makes it grow from its bottom edge when it opens above the trigger. GlassKit Elements’ <glk-popover> does all of this, and the tooltip and the menu open the same way. Try it: the preview cuts off whatever leaves its box, but not the popover in the top layer; a click outside or Escape closes it.

Above every stacking context: the preview’s overflow: hidden does not cut it off.

<button class="glass-btn glass-btn--secondary glass-btn--auto" id="filter-button">Filter</button> <div class="glass-popover" id="filters" popover="manual">โ€ฆ</div> <script> const button = document.getElementById('filter-button'); const pop = document.getElementById('filters'); button.addEventListener('click', () => { if (pop.matches(':popover-open')) return pop.hidePopover(); pop.showPopover(); const r = button.getBoundingClientRect(); pop.style.left = `${r.left}px`; pop.style.top = `${r.bottom + 8}px`; }); </script>

Class reference

ClassDescription
.glass-popover-anchorPositioning context for the popover (wraps trigger + popover)
.glass-popoverFloating glass surface, hidden until .is-open
.glass-popover.is-openVisible state — fade + scale animation
.glass-popover--topOpens upward (above the trigger)
.glass-popover--startAligns left edge with trigger
.glass-popover--endAligns right edge with trigger
.glass-popover[popover]In the top layer, position: fixed: opened with showPopover(), placed by script at viewport coordinates, no room while closed (since 1.32.0)
[data-side="top"]With [popover]: grows from its bottom edge, for a popover above its trigger

Note: name your toggle function anything except togglePopover — that name collides with the native HTMLElement.togglePopover() method from the HTML Popover API and inline onclick handlers will throw a NotSupportedError.

Tooltip

A short name or hint for a control, on hover and on keyboard focus (since 1.32.0): an icon-only button, the items of a rail, a row action. Small text on the popover’s glass with its edge light, tinted far enough to read over anything (6.3:1 or more, white under a dark tooltip included), at most --gl-tooltip-max-width (240 px) wide; longer text wraps. Give it role="tooltip"; the control points to it with aria-describedby, or with aria-labelledby where the tooltip is the control’s only name.

<button class="glass-pill" aria-labelledby="tip-archive"><!-- SVG --></button> <div class="glass-tooltip" id="tip-archive" role="tooltip" popover="hint">Archive</div> <button class="glass-btn glass-btn--secondary glass-btn--sm glass-btn--auto" aria-describedby="tip-share">Share link</button> <div class="glass-tooltip" id="tip-share" role="tooltip" popover="hint">Anyone with the link โ€ฆ</div>

It opens in the top layer like the popover, with popover="hint" (a browser without hint takes it as manual), placed by script next to its control and flipped or shifted to stay in the viewport. Without the attribute it is a plain block, as in the preview. WCAG 1.4.13 asks three things of a tooltip: the pointer can move onto it without it closing, Escape closes it without moving the focus, and it stays while the control is hovered or focused. GlassKit Elements’ <glk-tooltip> brings that behaviour: it shows after 400 ms on hover, at once on keyboard focus and on a long press by touch, while a tap never shows it.

TokenUnsetEffect
--gl-tooltip-max-width240pxThe widest it gets before the text wraps
--gl-tooltip-bg--gl-color-bg-dark at 94 %Its surface: the theme’s darkest background, light in the light theme
--gl-tooltip-color--gl-color-textIts text

A second line (since 1.39.0): .glass-tooltip__hint after the name says what a choice does, in the normal weight and the muted colour (--gl-tooltip-hint-color).

Instead of the previewA wider editor, the fields stay in the middle
<div class="glass-tooltip" popover="hint">Instead of the preview<span class="glass-tooltip__hint">A wider editor, the fields stay in the middle</span></div>

Input

Please enter a valid email address
EUR

Date and time fields — type="date", time, datetime-local, month — take the same class. They drop the native appearance (since 1.17.0): iOS drew them wider than their column, an empty one narrower than a filled one, with the value centred. Now they sit in their column like every other field, the value at the start; the native picker still opens.

<div class="glass-input-group"> <label class="glass-label">Label</label> <input class="glass-input" type="text"> <span class="glass-hint">Help text</span> </div>

Prefix and suffix

An icon, a currency, a unit or a button inside the field’s box (since 1.22.0): wrap the input in .glass-input-wrap and put __prefix and __suffix next to it. They lie on the field, centred, at its start and end — right to left they swap sides. The field’s padding grows by their width, --gl-input-prefix-size and --gl-input-suffix-size: 18px, an icon, unless set — set it for a wider unit. It follows the --prefix / --suffix modifier, or a visible affix where :has() is supported. A click on text, an icon or a disabled control reaches the field; enabled controls, links and elements with tabindex take their own, form-associated custom elements such as <glk-button> included (since 1.22.2). They take it even inside a container locked with pointer-events: none: lock it with inert, or disable them. <glk-input> in GlassKit Elements has prefix and suffix slots on this and measures the width itself.

<div class="glass-input-wrap glass-input-wrap--suffix" style="--gl-input-suffix-size: 30px"> <input class="glass-input" inputmode="decimal"> <span class="glass-input-wrap__suffix">EUR</span> </div>

A value or placeholder too long for the field ends in an ellipsis while the field is not being edited, and a .glass-label without text takes no room (both since 1.25.0). Date and time fields keep the browser’s own picker button; --gl-input-picker-icon: none hides it in Chromium — when the field’s prefix is the picker, for instance; call showPicker() from it then. It also hides the arrow of a field with a <datalist>.

At the end of the label row

Since 1.33.0 a field’s label row has a place at its end, for a link such as “Forgot password?” on the line of “Password”. Put .glass-input-group__label-end after the field in the markup and --label-end on the group: the group draws it on the label’s line, at its end, in the label’s small text, and the keyboard still reaches it after the field — e-mail, password, “Forgot password?”, sign in, the order that reads best in a sign-in form. It works the same around a textarea or a select; in GlassKit Elements, <glk-input>, <glk-textarea> and <glk-select> have a label-end slot for it.

Forgot password?
<div class="glass-input-group glass-input-group--label-end"> <label class="glass-label" for="password">Password</label> <input class="glass-input" id="password" type="password"> <a class="glass-input-group__label-end" href="/reset">Forgot password?</a> <!-- after the field --> </div>

Since 1.37.0 the place may hold a character counter, .glass-input-group__counter: “48/145” in the muted colour, and in the error colour with .is-over past the limit — a value set by script may be longer, maxlength only stops typing. Keep it up to date on input and tie it to the field with aria-describedby, so a screen reader reads it with the field. After a link in the same place it keeps its distance.

51/145
<div class="glass-input-group glass-input-group--label-end"> <label class="glass-label" for="meta">Meta description</label> <textarea class="glass-textarea" id="meta" maxlength="145" aria-describedby="meta-count"></textarea> <span class="glass-input-group__label-end glass-input-group__counter" id="meta-count">0/145</span> <!-- kept up to date on input --> </div>

Since 1.38.0 --gl-label-hidden: 1 on a group, or a container around fields, keeps each .glass-label as its field’s name and takes it out of sight — set from a container or media query, where an attribute cannot follow: fields in a table under one shared head only while the table is wide enough, with their labels above them when it turns into a list. It needs style queries on custom properties: Chrome 111, Safari 18 and Firefox 151. Without them the labels stay above their fields, every field 24 px higher — for older browsers put the rules of a visually hidden label on .glass-label, or on ::part(label) in GlassKit Elements, into the same query, as below. In GlassKit Elements it reaches into <glk-input>, <glk-select> and <glk-textarea>, whose label-hidden does the same for one field.

.week { container: week / inline-size; } @container week (min-width: 560px) { .day { --gl-label-hidden: 1; } /* labels out of sight, still the names */ /* older browsers: the same rules on the label */ .day .glass-label { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } }

Textarea

<textarea class="glass-textarea"></textarea>

A text area that cannot be edited has no resize handle (since 1.25.0). --gl-textarea-field-sizing: content lets text areas grow with their text in browsers with field-sizing; it is off by default, because it ignores rows.

:root { --gl-textarea-field-sizing: content; }

Select

<select class="glass-select"> <option>Please selectโ€ฆ</option> </select>

The chevron is --gl-select-chevron, an SVG drawn in each theme’s muted text colour — since 1.20.0; before, it stayed white in the light theme and all but vanished. Set the token to your own url(…) to replace it. Right to left, the chevron and the room kept for it move to the left.

A placeholder is the native pattern: a first option with an empty value, disabled and hidden, chosen while nothing is. It cannot be picked, and with required the form does not submit while it shows. Since 1.37.0 the field says it in the placeholder colour, as an input does; a first real option standing in for “nothing chosen” could not be chosen, since picking the option already shown changes nothing.

<select class="glass-select"> <option value="" disabled hidden selected>Choose a kindโ€ฆ</option> <option>Facebook</option> </select>

Toggle Switch

<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">Label</span> </label>

The input is invisible, but it lies on top of the track and takes the click itself (since 1.20.0): a test tool that clicks the switch by its role reaches it, where the track used to draw over it and take the click. It covers the track only — the label text, which may hold a link, stays the label’s. A __label left empty takes no room, so a switch without text keeps no 12 px gap for one. Both hold for the checkbox and the radio button too. Right to left, the thumb starts on the right.

The track is --gl-toggle-track-bg when off and --gl-toggle-track-bg-checked when on (since 1.25.0; unset, --gl-surface-2 and the primary gradient): a stronger track for a page without glass, where the surface can be hard to see.

Where the switch stands (since 1.39.0): .glass-toggle--label-start puts the text before it, and .glass-toggle--full spreads the control over its row, so with both the text stands at the row’s start and the switch at its end, as in the settings of iOS — mirrored right to left. The whole row is the label, so a click anywhere on it switches. .glass-checkbox--label-start, --full and the same for .glass-radio do it for checkbox and radio. In a list row the switch goes into __trailing instead, with the row’s title as its name.

<label class="glass-toggle glass-toggle--label-start glass-toggle--full"> <input class="glass-toggle__input" type="checkbox"> <span class="glass-toggle__track"><span class="glass-toggle__thumb"></span></span> <span class="glass-toggle__label">Notifications</span> </label>

Checkbox

As with the toggle, the input lies on top of the box and an empty __label takes no room (since 1.20.0).

Radio Button

As with the toggle, the input lies on top of the circle and an empty __label takes no room (since 1.20.0).

Fill, rim and dot are tokens (since 1.25.0): --gl-radio-bg and --gl-radio-bg-checked, --gl-radio-border and --gl-radio-border-checked (whole border values, so give both the same width), --gl-radio-dot-size (10px) and --gl-radio-dot-color (unset the ink on the primary, --gl-color-on-primary, white unless a project switches it — since 1.32.0; before, always white). A ring in the text colour that turns the accent colour when checked:

:root { --gl-radio-bg: transparent; --gl-radio-bg-checked: transparent; --gl-radio-border: 2px solid color-mix(in srgb, var(--gl-color-text) 60%, transparent); --gl-radio-border-checked: 2px solid var(--gl-color-primary); --gl-radio-dot-size: 50%; --gl-radio-dot-color: var(--gl-color-primary); }

Range Slider

75%

Progress Bar

Standard68%
Success100%
ClassDescription
.glass-progressBase container
--sm / --lgTrack height: 4px / 12px
--success / --errorColor variants

Accordion

The detailed answer to this question.
The answer to question two.

An open item takes the height of its content, however long, and slides to it: __content is a grid of one row that goes from 0fr to 1fr (since 1.30.0). Until then it ended at 500 px and cut off the rest, and the slide ran at the speed of that cap rather than of the content. Content that grows while the item is open grows with it. Under prefers-reduced-motion it opens without the slide.

Since 1.39.0 the trigger shows a ring for the keyboard, inset 0 0 0 2px var(--gl-border-focus) inside it, rounded with the item, as list and tree rows have; before, it had none. .glass-accordion__summary after the title holds a summary of the item, the values of a closed entry: in the room up to the chevron, on one line with an ellipsis, small and muted, inside the button, so a click on it, Enter and Space open the item as on the title. It takes only the room the title leaves. Hide it while the item is open where the fields show the values themselves.

The fields of the entry.
<button class="glass-accordion__trigger"> <span>Details of the news</span> <span class="glass-accordion__summary">Summer party ยท 31.10.2026 ยท Lena Hoffmann</span> <span class="glass-accordion__trigger-icon">โ€ฆ</span> </button>

Skeleton

Loading placeholder: shimmer lines standing in for text that has not arrived. Set each line's width; the shimmer follows the theme and holds still under prefers-reduced-motion. Mark the region aria-busy="true" so assistive tech knows it is loading.

<div class="glass-skeleton" aria-busy="true"> <div class="glass-skeleton__line glass-skeleton__line--title" style="width:55%"></div> <div class="glass-skeleton__line"></div> <div class="glass-skeleton__line" style="width:80%"></div> </div>
ClassDescription
.glass-skeletonStack of lines, 10px apart
.glass-skeleton__lineOne shimmer line (12px); width via inline style
.glass-skeleton__line--titleTaller line (18px) standing in for a heading

Table

A plain <table> on glass: hairline rows, a muted header, numbers right-aligned in tabular figures. The .glass-table-wrap wrapper scrolls sideways where the columns do not fit. Put the class on the table itself — from a shadow root, ::slotted() cannot reach the cells, so this is document-level CSS by design.

DateItemAmount
2026-09-18Invoice #10421,240.00
2026-09-19Refund−80.00
2026-09-20Pendingโ€“
<div class="glass-table-wrap"> <table class="glass-table"> <thead><tr><th>Date</th><th class="glass-table__num">Amount</th></tr></thead> <tbody><tr><td>2026-09-18</td><td class="glass-table__num">1,240.00</td></tr></tbody> </table> </div>
ClassDescription
.glass-table-wrapOptional horizontal scroll container
.glass-tableThe table: hairline rows, hover highlight
.glass-table__numRight-aligned, tabular figures — on th and td
.glass-table__mutedSecondary text colour on a cell

Sorting, a sticky header, row actions

Since 1.32.0 the table brings what a list of invoices or users needs. A header cell that sorts holds a __sort button with the arrow and carries aria-sort: "ascending", "descending" or "none". The page sorts the rows, often on the server; the classes only show the state. --sticky on the table and .glass-table-wrap--sticky on its wrap keep the header in view: the wrap scrolls both ways, at most --gl-table-max-height (60vh) high, and the header cells stick to its top on --gl-table-header-bg. Cells with __actions hold the row’s actions — a menu, icon buttons with tooltips — and stay at the row’s end while the table scrolls sideways; once it does, --scrolls on the table gives them a surface of their own, and a .glass-table-frame around the wrap shades each edge where more columns wait (data-more-start, data-more-end). Sort the columns, scroll the table, make the window narrow:

Status Plan
2026-1048Northwind Bakery2026-10-271,240.00OpenBusiness yearly
2026-1047Harbor Dental2026-10-2689.00PaidStarter monthly
2026-1046Bluebell Florist2026-10-252,310.50PaidBusiness monthly
2026-1045Kestrel Studio2026-10-24412.80OverdueStarter yearly
2026-1044Morning Light Yoga2026-10-2375.00PaidStarter monthly
2026-1043North Bike Works2026-10-22640.00OpenBusiness yearly
2026-1042Weber & Roth Law2026-10-213,480.00PaidBusiness yearly
<div class="glass-table-frame"> <div class="glass-table-wrap glass-table-wrap--sticky"> <table class="glass-table glass-table--sticky"> <thead><tr> <th aria-sort="descending"> <button class="glass-table__sort" type="button">Invoice <svg class="glass-table__sort-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V5M6 11l6-6 6 6"/></svg> </button> </th> <th class="glass-table__num" aria-sort="none"><!-- button as above --></th> <th class="glass-table__actions" aria-label="Actions"></th> </tr></thead> <tbody> <tr> <td>2026-1048</td> <td class="glass-table__num">1,240.00</td> <td class="glass-table__actions"><!-- a menu button --></td> </tr> </tbody> </table> </div> </div> /* Compact icon buttons in the action cells */ .glass-table__actions { --gl-btn-padding-x-sm: 8px; --gl-btn-height-sm: 32px; }

A few lines of script set --scrolls and the frame’s attributes from the wrap’s scroll position, as in the preview. GlassKit Elements’ <glk-table> does all of it around a native table in its light DOM, which keeps its semantics, copy and paste, find in page and markup rendered on the server: data-sort on a header cell adds the button and aria-sort, a click emits a cancelable glk-table-sort, and sticky-header and list-below switch the layouts.

As cards

--list turns each row into a card for narrow places, with the column’s name before each value from data-label on the cells. The actions move to the card’s top end, numbers keep their tabular figures. The header stays for assistive technology, hidden from view, and the labels are generated content without a text of their own, so a screen reader does not hear the column twice. A table that no longer lays out as a table can lose its semantics, so set its roles explicitly. <glk-table list-below="640px"> switches by its own width, not the viewport’s, and writes labels and roles itself.

InvoiceCustomerAmountStatus
2026-1048Northwind Bakery1,240.00Open
2026-1047Harbor Dental89.00Paid
<table class="glass-table glass-table--list" role="table"> <thead role="rowgroup">โ€ฆ</thead> <!-- th role="columnheader" --> <tbody role="rowgroup"> <tr role="row"> <td role="cell" data-label="Invoice">2026-1048</td> <td class="glass-table__num" role="cell" data-label="Amount">1,240.00</td> <td class="glass-table__actions" role="cell">โ€ฆ</td> </tr> </tbody> </table>

Empty

While no row is there, __empty below the table holds an empty state and the header stays; hidden takes it away again.

InvoiceCustomerAmountStatus

No invoices

Nothing matches this filter.

<div class="glass-table-wrap"> <table class="glass-table"><thead>โ€ฆ</thead><tbody></tbody></table> <div class="glass-table__empty"> <div class="glass-empty">โ€ฆ</div> </div> </div>
ClassDescription
__sort, __sort-iconButton filling a header cell that sorts by its column, and its arrow: up for ascending, down for descending, faint while unsorted
th[aria-sort]The state: ascending, descending, none
--sticky + .glass-table-wrap--stickyThe header sticks to the top of a wrap that scrolls both ways, at most --gl-table-max-height high
__actionsCell for row actions, at the row’s end, kept there while the table scrolls sideways
--scrollsSet while the table scrolls sideways: the actions on a surface of their own, with a shadow
.glass-table-frameAround the wrap; shades the start or end edge with data-more-start, data-more-end
__emptyBelow the table in its wrap, the room for an empty state
--listEach row a card, the labels from data-label, the actions at its top end
TokenUnsetEffect
--gl-table-cell-padding9px 10pxThe room in the body cells
--gl-table-header-padding8px 10pxThe room in the header cells
--gl-table-max-height60vhHow high a sticky wrap gets before it scrolls
--gl-table-header-bg--gl-color-bg-mid at 94 %The sticky header’s surface, and that of the actions while the table scrolls sideways

Prose

Running text the app did not write by hand — rendered Markdown, help and info pages. One class on the container styles headings, paragraphs, lists, links, code, quotes, images and tables. Document-level by design: the content is light DOM, and ::slotted() stops at the first level.

Getting started

Include glasskit.css, set data-theme on <html>, done. Everything else is optional — see the design tokens.

  • Dark mode is the default
  • Light mode via data-theme="light"
The glass effect needs a coloured background to show.
<article class="glass-prose"> <!-- rendered Markdown: h1โ€“h3, p, ul/ol, a, code, pre, blockquote, img, hr, table --> </article>

Segmented

A small, exclusive choice as one control — traffic light, morning / afternoon, mode. Buttons inside a role="group"; the chosen one carries aria-pressed="true", and the styling hangs on exactly that attribute, so state and semantics cannot drift apart. The tone modifiers put a dot in a state colour before the label. Try it: click or Tab to a button and press Space. More options than fit? Without a modifier the row stays one line and runs past the edge; --scroll lets it scroll sideways, --wrap breaks it into lines. Both combine with --full.

<div class="glass-segmented glass-segmented--full" role="group" aria-label="Status"> <button class="glass-segmented__item glass-segmented__item--success" aria-pressed="true"> <span class="glass-segmented__dot"></span>Green </button> <button class="glass-segmented__item" aria-pressed="false">Help page</button> </div>
ClassDescription
.glass-segmentedThe group: inset glass track, buttons inside
--fullButtons share the width
--scrollOne row that scrolls sideways when the options do not fit, scrollbar hidden (since 1.17.0)
--wrapBreaks the row into lines when the options do not fit (since 1.17.0)
__itemOne option; aria-pressed="true" marks the chosen one, disabled dims it
__item--success / --warning / --errorTone: sets --gl-segmented-tone for the dot
__dotThe dot before the label, in the tone colour
__iconAn icon before the label, in the option’s colour (since 1.28.0)
__badgeA count after the label, in the tone’s colour where one is set; inside __icon at the icon’s corner (since 1.29.0)
__badge--primary, --success, --warning, --errorThe count in a colour of its own, whatever the option’s tone, without the dot (since 1.31.0)
--iconsIcons alone, square options; the names go into aria-label (since 1.28.0)
__icon--colorA coloured logo keeps its own fills and strokes; without it an SVG strokes as a line icon (since 1.35.0)
--dropletThe chosen option sits on a drop of glass that glides to the next one (since 1.27.0)

Icons

Since 1.28.0 an option can show an icon in .glass-segmented__icon, before its label: an SVG strokes in the option’s colour, any other element — a font icon, an <img> mask — takes that colour, and a font icon the box’s 18 px. Under the drop the icon grows like the tone dot. --icons makes a row of icons alone, a light / dark / auto switch for instance: the options become square, and each takes its name as aria-label, and as title for a tooltip. Mark the icon box aria-hidden="true", so a font icon’s text does not join the name.

A coloured logo as an icon — the mark of an image source — takes .glass-segmented__icon--color (since 1.35.0) and keeps its own fills and strokes; before, its shapes inherited the line icon’s stroke and came out outlined in the text colour. --gl-segmented-bg sets the track (since 1.35.0); on the glass of a modal or a sheet in dark mode it is a well, --gl-dialog-well, with the chosen option on --gl-dialog-raised.

<div class="glass-segmented glass-segmented--icons glass-segmented--droplet" role="group" aria-label="Theme"> <button class="glass-segmented__item" aria-pressed="true" aria-label="Light" title="Light"> <span class="glass-segmented__icon" aria-hidden="true"><!-- SVG or font icon --></span> </button> <!-- Dark, Auto --> </div>

Chosen option surface

Since 1.28.0 the chosen option’s surface, shadow and text colour are tokens, like the tab bar’s active item: on a dark brand background the light surface left a white label below 4.5:1. All three are unset; the drop takes the same surface and shadow, so one pair of values serves both looks. The shadow sits in shadow lists: turn it off with a zero shadow, not none.

TokenUnsetEffect
--gl-segmented-active-bg--gl-surface-5 (the drop: a light glass gradient)Surface of the chosen option and of the drop
--gl-segmented-active-shadow--gl-shadow-btn (the drop: its own)Their shadow
--gl-segmented-active-color--gl-color-text-headingLabel and icon of the chosen option, and of the option below a dragged drop
:root { --gl-segmented-active-bg: var(--gl-color-primary); --gl-segmented-active-color: var(--gl-color-on-primary); }

Counts

Since 1.29.0 an option can say how much waits behind it — open invoices, new tickets — with .glass-segmented__badge after its label: a small pill in tabular figures, inline, so a scrolling row and the drop measure the option with it. The tone dot says that something is there, the badge how much. Without a tone it is neutral, a tint of the text colour over the state scrim; with one it takes the state tint of the badges. Put inside .glass-segmented__icon it sits at the icon’s corner, as on the tab bar — the place for an --icons row, where it covers the icon and is opaque. Its number is no name: give the option an aria-label that says it, “Tickets, 1 open”.

<button class="glass-segmented__item glass-segmented__item--warning" aria-pressed="false" aria-label="Tickets, 1 open"> <span class="glass-segmented__dot"></span>Tickets<span class="glass-segmented__badge">1</span> </button> <!-- In an --icons row: at the icon's corner --> <span class="glass-segmented__icon" aria-hidden="true"><!-- SVG --><span class="glass-segmented__badge">3</span></span>

The neutral pill has two tokens, both colours and unset by default: --gl-segmented-badge-bg, the tint over the scrim (unset: 12 % of the text colour), and --gl-segmented-badge-color (unset: the text colour). Measured in Chromium, WebKit and Firefox, the figures reach 5.6:1 or more in the dark theme and 4.7:1 or more in the light one, on the chosen option and beside it, neutral and in every tone.

Since 1.31.0 a count can carry a colour of its own, whatever the option’s tone: .glass-segmented__badge--primary, --success, --warning and --error give it the look of the badge variants — the state tint over a doubled scrim, a hairline, the readable text colour — without the dot a tone puts before the label. Where the option has a tone as well, the count’s own colour wins. Its size is a token too: --gl-segmented-badge-height (unset 16 px, also its least width) and --gl-segmented-badge-font-size (unset 11 px). --gl-segmented-badge-border draws a hairline in that colour around the neutral count, inside, so the count keeps its size. Measured in Chromium, WebKit and Firefox, the four colours reach 4.7:1 or more in both themes, inline and at the icon’s corner, on the chosen option and beside it.

<button class="glass-segmented__item" aria-pressed="true" aria-label="Open, 4 new"> Open<span class="glass-segmented__badge glass-segmented__badge--error">4</span> </button>

Droplet

Since 1.27.0 .glass-segmented--droplet gives the chosen option the drop of the tab bar: it runs to a new option, stretches on the way and settles, the text it passes lights up, and a finger can drag it. Disabled options are skipped. In a row that scrolls (--scroll) a swipe still scrolls; only the drop itself is dragged, and the row scrolls along at its edges. The drop follows aria-pressed; attachDroplets() from glasskit-droplet.js starts it, as above.

<div class="glass-segmented glass-segmented--scroll glass-segmented--droplet" role="group" aria-label="Area"> <button class="glass-segmented__item" aria-pressed="true">General</button> <!-- more options --> </div>

Steps

Progress through a short flow: numbered circles joined by hairlines, done ones on the success surface (with an SVG check), the current one in the primary colour (--gl-color-primary-fill where a project sets one, since 1.32.0) and marked aria-current="step".

  1. Day
  2. Slot
  3. 3Dog
  4. 4Confirm

The list is a size container: narrower than 360 px only the current step keeps its label, the others show their number; before that, labels shorten with an ellipsis. In the narrow layout the circles keep their size and the lines give way first, down to 2 px, before the current label shortens (since 1.28.0): seven steps keep a current label of 88 px whole from a 310 px column on, and the row never runs past its edge. Keyed to the block's own width, not the viewport — here in a 300 px frame:

  1. Day
  2. 2Choose a slot
  3. 3Dog
  4. 4Confirm
<ol class="glass-steps"> <li class="glass-steps__item glass-steps__item--done"> <span class="glass-steps__num"><!-- check SVG --></span><span class="glass-steps__label">Day</span> </li> <li class="glass-steps__line" aria-hidden="true"></li> <li class="glass-steps__item glass-steps__item--current" aria-current="step"> <span class="glass-steps__num">2</span><span class="glass-steps__label">Slot</span> </li> </ol>
ClassDescription
.glass-stepsThe list; a size container, so give it a width inside a flex row
__item, __item--done, __item--currentOne step and its states
__numThe circle: number, or an SVG check when done
__labelThe label; hidden below 360 px except on the current step
__lineHairline connector between steps

Sheet

The bottom sheet is the mobile sibling of the modal: an overlay that fades and a panel that rises from the bottom edge, only the top corners rounded, a grip, safe-area padding. Its own block, not a modal modifier, because layout, entry motion and gesture differ. --inline embeds the panel in the flow without an overlay; the overlay form needs a little JavaScript, shown below and mirrored by <glk-sheet> in GlassKit Elements. Since 1.30.0 the overlay may be a native <dialog>, as the modal’s: opened with showModal() it lies in the top layer, above every fixed or transformed element of the page whatever its z-index, and the page behind it is inert. dialog.glass-sheet-overlay undoes the dialog’s own box and backdrop, and clips without being a scroll container (since 1.39.0: overflow: clip — with hidden a focus inside the panel while it slid in scrolled the overlay, and the panel stood at its place at once or swung past it and back); show with showModal() and then .is-active, hide by removing .is-active and calling close() after transitionend. <glk-sheet> does this.

Rebook to …

Sheet content: a list, text, a form. This one is inline.
<div class="glass-sheet-overlay" hidden> <div class="glass-sheet" role="dialog" aria-modal="true"> <div class="glass-sheet__grip"></div> <h2 class="glass-sheet__title">Rebook to …</h2> <div class="glass-sheet__body">…</div> <div class="glass-sheet__actions">…</div> </div> </div> // show: remove hidden, force a reflow, add .is-active next frame // hide: remove .is-active, set hidden after transitionend (fallback 400 ms)
ClassDescription
.glass-sheet-overlayFixed overlay: scrim, blur, panel aligned to the bottom; starts at opacity 0
.is-activeOverlay visible, panel risen — toggled by script
.glass-sheetThe panel: milky glass, top corners rounded, bottom safe-area padding
--inlineIn the flow, no overlay, all corners rounded
__grip, __title, __body, __actionsGrip bar, heading, body, action column
__head, __headingThe head: takes the title’s place and spacing; __heading holds __eyebrow, the title and __subtitle (since 1.35.0). With hidden it takes no room (since 1.36.0)
__eyebrowA short line above the title, small capitals in the muted colour
__subtitleBelow the title: a badge, a line of text, small in the muted colour (since 1.36.0)
__leading, __trailingBefore and after the title: a mark or an icon chip, a badge
__closeA round glass close button, 32 px with a 44 px target, at the head’s end; --gl-sheet-close-size, --gl-sheet-close-icon-size (16 px) and --gl-sheet-close-icon-stroke (2) resize it and its glyph (since 1.36.0)
.glass-sheet-overlay--start, --endThe panel at that side, full height, from the side, without grip, actions side by side (since 1.35.0)
.glass-sheet-overlay--modelessNo scrim and no blur; only the panel takes the pointer — for a sheet opened with show()

The head

Since 1.35.0 the head holds more than the title: __eyebrow above it (“Restore”), __leading before it (a 56 px icon chip), __trailing after it (a badge) and __close at its end, a round glass button. __head takes the title’s place and spacing; a title alone in it stands as it did without one. Since 1.36.0 __subtitle stands below the title, in __heading: a badge there leaves the title its width, where one in __trailing squeezed it in a narrow sheet. Three tokens resize the close button and its glyph — --gl-sheet-close-size (32 px), --gl-sheet-close-icon-size (16 px) and --gl-sheet-close-icon-stroke (2) —, and an empty head with hidden takes no room; before, it kept its 8 px.

Restore

Version from 09:41

1× in your app
Restoring replaces the current version.
<div class="glass-sheet__head"> <span class="glass-sheet__leading"><!-- an icon chip --></span> <div class="glass-sheet__heading"> <span class="glass-sheet__eyebrow">Restore</span> <h2 class="glass-sheet__title">Version from 09:41</h2> </div> <span class="glass-sheet__trailing"><span class="glass-badge">1ร— in your app</span></span> <button class="glass-sheet__close" aria-label="Close"><!-- ร— --></button> </div>

Picture gallery

1× in your app
The badge stands below the title, which keeps its width. The close button is plain, 44 px with a 20 px glyph.
<div class="glass-sheet__heading"> <h2 class="glass-sheet__title">Picture gallery</h2> <span class="glass-sheet__subtitle"><span class="glass-badge glass-badge--success">1× in your app</span></span> </div> .my-sheet { --gl-sheet-close-size: 44px; --gl-sheet-close-icon-size: 20px; --gl-sheet-close-icon-stroke: 1.8; } .my-sheet .glass-sheet__close:not(:focus-visible) { background: transparent; border-color: transparent; box-shadow: none; }

At a side, modeless

.glass-sheet-overlay--end or --start (since 1.35.0) puts the panel at that side of the window: as high as the window less --gl-sheet-side-inset (16 px) at both ends and that far from its edge, --gl-sheet-side-width (440 px) wide, with a card’s corners. It slides in from that side, mirrored right to left, has no grip, and its actions stand side by side at its foot — a side panel on a desktop. --modeless leaves the page usable: no scrim and no blur, only the panel takes the pointer. Open that dialog with show() instead of showModal(): the page is not inert, its chosen row stays in view, and a modal opened later lies above the sheet. --gl-sheet-z-index (1000) stacks it. Since 1.36.0 --gl-sheet-side-inset-block-start and --gl-sheet-side-inset-block-end set the top and the bottom on their own — below a fixed top bar, say — and fall back to --gl-sheet-side-inset, which stays the side’s and the way in.

<dialog class="glass-sheet-overlay glass-sheet-overlay--end"> <!-- showModal(), then .is-active --> <div class="glass-sheet">โ€ฆ</div> </dialog> <dialog class="glass-sheet-overlay glass-sheet-overlay--end glass-sheet-overlay--modeless"> <!-- show() --> <div class="glass-sheet">โ€ฆ</div> </dialog> /* Below a fixed bar of 68px at 16px from the top */ .my-side-sheet { --gl-sheet-side-inset: 24px; --gl-sheet-side-inset-block-start: 100px; }

Dark glass

The glass of a modal or a sheet is brighter than the page, most at its top. In dark mode, since 1.35.0, the glow of both starts at the card’s middle one, and the controls on them take values of their own where the page set none: a segmented control lies in a well with its chosen option one step darker, a tertiary button is one step darker, badges and tertiary buttons take no blur of their own, and accent text takes --gl-color-primary-on-glass. At the top of a tall sheet in dark mode, white text on badges and segments read 2.9 to 4.7:1 in 1.34.0 and reads 4.7 to 7.7:1 now, in Chromium, WebKit and Firefox. Light mode keeps its glow and controls. --gl-sheet-bg and --gl-modal-bg replace the whole surface.

TokenDark valueUsage
--gl-dialog-glow-topvar(--gl-card-glow-mid)Where the glow of a modal and a sheet starts
--gl-dialog-wellrgba(0, 0, 0, 0.10)The track of a segmented control on dialog glass
--gl-dialog-raisedvar(--gl-surface-3)Its chosen option
--gl-dialog-controlvar(--gl-surface-1)A tertiary button on dialog glass
--gl-badge-selected-colorvar(--gl-color-text)The label of a chosen badge, anywhere

All five are unset in light mode. A segmented control or a badge whose tokens the page sets keeps them on dialog glass too.

Empty state

For empty lists and result pages: a centred column with a round icon plate, a title, a short muted text and room for one action or several, side by side.

No bookings yet

Pick a day and a dog — the team is looking forward to it.

<div class="glass-empty"> <span class="glass-empty__icon"><!-- SVG --></span> <p class="glass-empty__title">No bookings yet</p> <p class="glass-empty__text">…</p> <div class="glass-empty__action">…</div> </div>
ClassDescription
.glass-emptyCentred column
__icon48 px round plate, 24 px stroked icon inside (::slotted(svg) twin for Elements)
__title, __textHeading and muted text (max 38 ch)
__actionRoom for one action or several: side by side with 8 px between them, wrapping and centred when narrow (since 1.17.0)

Date strip

A row of day chips that scrolls sideways — a booking horizon, the days around today. Each chip is a <button> with the weekday, the day number and a dot that a tone modifier lights up; without a tone the dot is transparent, so chips with and without a mark keep the same height. The chosen chip carries aria-pressed="true", as in the segmented control; :disabled or aria-disabled="true" dims a day that cannot be chosen. The scrollbar is hidden. Give each button an aria-label with the full date — the visible “Tu 22” is no name. <glk-date-strip> in GlassKit Elements builds this from dates, with arrow keys and the chosen chip centred.

<div class="glass-date-strip" role="group" aria-label="Pick a day"> <button class="glass-date-strip__day glass-date-strip__day--today" aria-pressed="false" aria-label="Tuesday, 22 September 2026"> <span class="glass-date-strip__wd">Tu</span><span class="glass-date-strip__num">22</span> <span class="glass-date-strip__mark glass-date-strip__mark--success"></span> </button> <button class="glass-date-strip__day" aria-pressed="true" aria-label="…">…</button> <button class="glass-date-strip__day" aria-pressed="false" disabled>…</button> </div>
ClassDescription
.glass-date-stripScrolling row; hidden scrollbar; 4 px padding so its own overflow does not clip the focus ring
__dayChip button, 44 px minimum; [aria-pressed="true"] chosen, on --gl-color-primary-fill where set, else the primary (since 1.32.0), :disabled / [aria-disabled="true"] dimmed
__day--todayUnderlines the number
__wd, __numWeekday in small caps, day number
__mark6 px dot; transparent without a tone
__mark--primary, --success, --warning, --errorTone of the dot via --gl-date-strip-tone; on the chosen chip every tone becomes the ink on primary (--gl-date-strip-ink)

Calendar

One month: a title between two round nav buttons, a weekday row and 42 square day buttons with up to three dots beneath the number. The chosen day carries aria-pressed="true", --today draws a warm border, --other dims a day of the neighbouring month, :disabled or aria-disabled="true" dims a day outside the allowed range — the second keeps it focusable for arrow keys. It is a role="group" of buttons named with their full date, not a role="grid". <glk-calendar> in GlassKit Elements builds it, with keyboard navigation and Intl names.

September 2026
<div class="glass-calendar"> <div class="glass-calendar__head"> <button class="glass-calendar__nav" aria-label="Previous month"><!-- SVG chevron --></button> <div class="glass-calendar__title">September 2026</div> <button class="glass-calendar__nav" aria-label="Next month">…</button> </div> <div class="glass-calendar__grid" role="group" aria-label="Pick a day"> <span class="glass-calendar__wd" aria-hidden="true">Mo</span> <!-- × 7 --> <button class="glass-calendar__day glass-calendar__day--other" aria-pressed="false" aria-label="Monday, 31 August 2026"> <span>31</span><span class="glass-calendar__marks"></span> </button> <button class="glass-calendar__day glass-calendar__day--today" aria-pressed="true" aria-label="…"> <span>22</span><span class="glass-calendar__marks"><span class="glass-calendar__mark glass-calendar__mark--warning"></span></span> </button> <!-- 42 cells --> </div> </div>
ClassDescription
.glass-calendarBlock; __head with __title (ellipsis when tight) and two __nav buttons, 32 px round, with an SVG chevron
__gridSeven columns, 4 px gap
__wdWeekday header cell, muted small caps
__daySquare day button; [aria-pressed="true"] chosen, on --gl-color-primary-fill where set, else the primary (since 1.32.0), --today warm border, --other dimmed neighbour month, :disabled / [aria-disabled="true"] dimmed and unpickable
__marks, __mark5 px dots beneath the number, up to three; __mark--primary / --success / --warning / --error via --gl-calendar-tone, the ink on the chosen day via --gl-calendar-ink

Image picker

Choosing one image with a preview: a 72 px plate — the picture with object-fit: cover, or a placeholder icon — and a column with the label, a hint and the actions, which are ordinary .glass-btn buttons; the block brings no button look of its own. --round makes the plate a circle for avatars; a long label wraps and never squeezes the plate. Any image, not only photos. <glk-image-picker> in GlassKit Elements adds the file dialog, EXIF rotation and resizing.

Photo of the dog JPG or PNG ยท resized to 1024 px before upload
Profile photo
<div class="glass-image-picker" role="group" aria-labelledby="photo-label"> <div class="glass-image-picker__preview glass-image-picker__preview--round"><img src="…" alt=""></div> <div class="glass-image-picker__meta"> <span class="glass-image-picker__label" id="photo-label">Profile photo</span> <span class="glass-image-picker__hint">JPG or PNG</span> <div class="glass-image-picker__actions"> <button class="glass-btn glass-btn--secondary glass-btn--sm glass-btn--auto">Change</button> <button class="glass-btn glass-btn--tertiary glass-btn--sm glass-btn--auto">Remove</button> </div> </div> </div>
ClassDescription
.glass-image-pickerRow: plate left, column right, 14 px gap
__preview72 px plate; img covers it, svg is the 28 px placeholder in the muted icon colour
__preview--roundCircle, for avatars
__metaColumn that yields (min-width: 0)
__label, __hintLabel in the look of .glass-label, wraps anywhere; small muted hint
__actionsWrapping row of .glass-btn buttons

Utilities

Stack (vertikal)

.gl-stack .gl-stack--xs (8px) .gl-stack--sm (12px) .gl-stack--md (16px) .gl-stack--lg (20px) .gl-stack--xl (24px)

Row (horizontal)

.gl-row .gl-row--xs (8px) .gl-row--sm (12px) .gl-row--md (16px)

Margin Top

.gl-mt-sm (12px) .gl-mt-md (16px) .gl-mt-lg (20px) .gl-mt-xl (24px)

Margin Bottom

.gl-mb-sm (12px) .gl-mb-md (16px) .gl-mb-lg (20px) .gl-mb-xl (24px)

Text

.gl-text-center .gl-text-muted .gl-text-sm

Layout

.gl-px (padding horizontal) .gl-w-full (width: 100%) .gl-flex-1 (flex: 1)

๐Ÿ“‹ All Classes at a Glance

ComponentClassesModifier / States
Background.glass-bg--has-tab-bar
Navigation.glass-navโ€“
Pill-Button.glass-pill:disabled
Theme-Toggle.glass-theme-toggleโ€“
Title.glass-titleโ€“
Card.glass-card--glow
Button.glass-btn--primary, --secondary, --tertiary, --sm, --lg, --auto, :disabled
Badge.glass-badge--primary, --success, --warning, --error, --interactive, --selected, --md
Avatar.glass-avatar--sm, --lg; --gl-avatar-size (since 1.37.0)
Divider.glass-dividerโ€“
Status.glass-statusโ€“
List.glass-list--flush, --bare, __item--interactive, __item--center, __item--danger, __item--accent, __leading--lg, __subtitle--wrap, __value, __section-header, .is-flash
Tree.glass-tree__row, __twisty, __icon, __label, __trailing, __group, .is-expanded, .is-leaf, .is-selected, .is-loading, .is-error, .is-flash, .is-disabled, .is-focus-visible, --gl-tree-level
Sorting.glass-sort__ghost, .glass-sort__handle.is-sort-source, .is-sort-before, .is-sort-after, .is-sort-inside, .is-sort-refused
Skeleton.glass-skeleton__line, __line--title
Table.glass-table-wrap, -wrap--sticky, -frame, __num, __muted, __sort, __sort-icon, [aria-sort], __actions, __empty, --sticky, --scrolls, --list
Prose.glass-proseโ€“
Segmented.glass-segmented--full, --scroll, --wrap, --icons, __item, __item--success/--warning/--error, __dot, __icon, __badge, [aria-pressed], --droplet
Steps.glass-steps__item, __item--done, __item--current, __num, __label, __line
Sheet.glass-sheet-overlay + .glass-sheet.is-active, --inline, __grip, __title, __body, __actions
Empty state.glass-empty__icon, __title, __text, __action
Date strip.glass-date-strip__day, __day--today, __wd, __num, __mark, __mark--primary/--success/--warning/--error, [aria-pressed], :disabled
Calendar.glass-calendar__head, __title, __nav, __grid, __wd, __day, __day--today, __day--other, __marks, __mark--primary/--success/--warning/--error, [aria-pressed], [aria-disabled]
Image picker.glass-image-picker__preview, __preview--round, __meta, __label, __hint, __actions
Popover.glass-popover--top, --start, --end, .is-open, [popover], [data-side="top"]
Tooltip.glass-tooltip[popover]; __hint, a second line (since 1.39.0)
Menu.glass-menu__item, __item--danger, __check, __icon, __label, __trailing, __separator, __group-label, [aria-checked], [aria-disabled], .is-checked, .is-focused, .is-focus-visible, [popover]
Input.glass-input--error, :disabled; .glass-input-wrap with __prefix / __suffix; .glass-input-group--label-end with __label-end and __counter (since 1.37.0)
Textarea.glass-textarea:disabled
Select.glass-select:disabled; a placeholder option in the placeholder colour (since 1.37.0)
Search.glass-searchโ€“
Toggle.glass-toggle:checked, :disabled; --label-start, --full (since 1.39.0)
Checkbox.glass-checkbox:checked, :disabled
Radio.glass-radio:checked, :disabled
Range.glass-range:disabled
Progress.glass-progress--sm, --lg, --success, --error
Modal.glass-modal-overlay.is-active
Toast.glass-toast--success, --error, --warning, .is-visible, __action, __close; [popover] in the top layer (since 1.39.0)
Tab-Bar.glass-tab-bar__item with .is-active, __icon, __label, __badge (--dot since 1.38.0), __leading, __trailing, --droplet, --vertical, --rail, --labels, --start, --end, --top
Tab-Bar Floating.glass-tab-bar-dock + .glass-tab-bar--floating__accessory, __accessory--accent/--success/--error, __accessory-item, __accessory-label, --accessory-left, -dock--vertical, -dock--start
Breadcrumbs.glass-breadcrumbs__list, __item, __crumb, __current, __overflow, __item--overflow, .is-drop-target, .is-drop-refused
Accordion.glass-accordion.is-open on items; __summary beside the title (since 1.39.0)

๐Ÿค– AI / LLM Reference

GlassKit ships with SKILL.md โ€“ a structured, machine-readable component reference designed for AI assistants, LLMs, and code-generation tools.

FeatureDescription
Copy-paste HTMLAll 38 components with exact nesting and BEM hierarchy
Design token tablesColors, surfaces, blur, radii, spacing, shadows, typography
State class mappingis-active, is-open, is-visible, :checked โ€“ where each belongs
Composition patterns6 full page layouts: Login, Dashboard, Form, Modal, Settings, Progress
Common mistakesFrequent errors and their corrections

Attach or reference SKILL.md when asking an AI to generate GlassKit HTML for production-ready output.

# File location glasskit/SKILL.md # Also included in the npm package npm install @jungherz-de/glasskit node_modules/@jungherz-de/glasskit/SKILL.md

๐Ÿงฉ The GlassKit Family

GlassKit is the CSS foundation of a three-layer family โ€“ three layers, one design language. For everything beyond single components, the sister projects are the intended path.

LayerProjectUse it for
CSS foundation๐ŸงŠ GlassKit (you are here)The design language: pure-CSS components, design tokens, dark & light.
App layerGlassKit ElementsDrop-in web components (glk-button, glk-card, โ€ฆ) for application UIs.
Website layerGlassKit WebBuilding a complete website โ€“ the official Astro template with sections, i18n and SEO on the GlassKit foundation.