Glassmorphism UI Library
A complete CSS component library with glassmorphism aesthetics โ built by Jungherz GmbH. Pure CSS, no dependencies, Dark & Light mode.
Installation
Include one CSS file โ done. Optionally add a theme override for custom brand colors.
Design Tokens
All visual values are controlled via CSS Custom Properties.
Color Palette
| Token | Dark Value | Usage |
|---|---|---|
--gl-color-primary | #f5a623 | Primary color, buttons, active elements |
--gl-color-primary-dark | #d4692a | Gradient end value |
--gl-color-primary-text | var(--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-glass | color-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-ring | color-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 | #ffffff | Default text color |
--gl-color-text-muted | rgba(255,255,255,0.65) | Secondary text |
--gl-color-success | #34c759 | Success status |
--gl-color-error | #ff3b30 | Error status |
--gl-color-warning | #ffcc00 | Warning status |
--gl-color-{state}-surface, -border, -on-surface | color-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
| Token | Value | Usage |
|---|---|---|
--gl-surface-1 | rgba(โฆ, 0.08) | Subtlest surface (status) |
--gl-surface-2 | rgba(โฆ, 0.10) | Default (inputs, cards) |
--gl-surface-3 | rgba(โฆ, 0.14) | Nav pills, badges |
--gl-surface-4 | rgba(โฆ, 0.16) | Hover states |
--gl-surface-5 | rgba(โฆ, 0.22) | Strong hover |
--gl-surface-milk-strong | rgba(โฆ, 0.75) | Secondary button |
Gloss
| Token | Value | Usage |
|---|---|---|
--gl-gloss-card | linear-gradient(90deg, โฆ) | Highlight along the top edge of .glass-card--glow |
--gl-gloss-modal | linear-gradient(90deg, โฆ) | The same on .glass-modal |
--gl-gloss-progress | linear-gradient(to bottom, โฆ) | Sheen on the progress fill |
--gl-range-thumb-border | 2px solid rgba(255,255,255,0.5) | Rim of the range thumb |
--gl-gloss-droplet | radial-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
| Token | Value | Token | Value |
|---|---|---|---|
--gl-space-2xs | 4px | --gl-radius-xs | 8px |
--gl-space-xs | 8px | --gl-radius-sm | 12px |
--gl-space-sm | 12px | --gl-radius-input | 14px |
--gl-space-md | 16px | --gl-radius-btn | 16px |
--gl-space-lg | 20px | --gl-radius-card | 24px |
--gl-space-xl | 24px | --gl-radius-full | 9999px |
--gl-space-2xl | 32px | --gl-radius-pill | 50% |
Density
| Token | Default | Compact | Usage |
|---|---|---|---|
--gl-control-height | 52px | 40px | Input, select, search |
--gl-control-padding-x | unset → --gl-space-md | --gl-space-sm | Inner left/right padding of input, select, textarea |
--gl-btn-height | 56px | 40px | Button |
--gl-btn-height-sm | 44px | 32px | Button --sm |
--gl-btn-height-lg | 64px | 48px | Button --lg |
--gl-textarea-min-height | 120px | 88px | Textarea |
--gl-list-item-min-height | 56px | 48px | List row |
--gl-list-item-padding-y | unset → --gl-space-md | 10px | List row, top and bottom |
--gl-toggle-width × -height | 52 × 30px | 44 × 26px | Toggle track |
--gl-toggle-thumb-size | 22px | 18px | Toggle thumb |
--gl-check-size | 24px | 20px | Checkbox, radio |
--gl-modal-action-height | 52px | 44px | Modal actions |
Theming (Light / Dark)
The theme is controlled via data-theme on the <html> element.
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:
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:
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:
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:
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:
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:
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.
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.
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.
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.
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.
| Class | Description |
|---|---|
.glass-bg | Full background with aurora light effects |
.glass-bg--has-tab-bar | Modifier: bottom padding for tab bar |
.glass-bg--has-tab-bar-floating | Modifier: bottom padding for floating tab bar + accessory |
Pill-Buttons
Round icon buttons for navigation and actions.
| Class | Description |
|---|---|
.glass-pill | Standard glass circle (46ร46px) |
.glass-theme-toggle | Pill 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).
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:
| Token | Unset | Effect |
|---|---|---|
--gl-tab-bar-color | --gl-icon-muted | Icon and label of the inactive tabs |
--gl-tab-bar-hover-color | --gl-icon-default | Icon and label of a hovered tab; set it as well when the colour above is lighter |
--gl-tab-bar-label-size | 10px | Size of the labels |
--gl-tab-bar-icon-size | 24px | Size 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-medium | Weight of the active tab’s label (since 1.33.0); every label is medium unless this is set |
--gl-tab-bar-z-index | 900 | Stacking of the fixed bar and of the floating dock |
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.
| Token | Unset | Effect |
|---|---|---|
--gl-tab-bar-badge-size | 16px | Height and least width of the count; the dot is 10 px unless set |
--gl-tab-bar-badge-top | -4px | From the icon’s top; the dot -2 px |
--gl-tab-bar-badge-end | -8px | From the icon’s end; the dot -3 px |
--gl-tab-bar-badge-ring-width | 0px | A ring around it; the dot 2 px |
--gl-tab-bar-badge-ring-color | --gl-color-bg-mid | The ring’s colour, the page’s middle background |
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.
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:
--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.
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.
| Class | Description |
|---|---|
.glass-tab-bar--vertical | Column 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--rail | With --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, --end | With --vertical: fixed to that edge of the viewport, full height, clear of the safe area; mirrored right to left |
.glass-tab-bar--top | Horizontal 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__item | An item as a link, without underline |
.glass-tab-bar--labels | With --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 --rail | A 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, __trailing | Places 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-item | The 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.
| Token | Unset | Effect |
|---|---|---|
--gl-tab-bar-rail-width | 72px | Width of the rail |
--gl-tab-bar-badge-room | 32px | Room 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 column | The active row’s surface; on the bottom bar unset means none |
--gl-tab-bar-rail-item-height | 62px | Least height of an item in a rail with labels (since 1.33.0) |
--gl-tab-bar-item-radius | a capsule; 16px in a rail with labels | Corners of the floating items, of a labelled rail’s items and of the drop (since 1.33.0) |
--gl-tab-bar-floating-inset | 16px | Distance 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-card | Corners 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:
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.
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).
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.
| Class | Description |
|---|---|
.glass-tab-bar-dock | Wrapper: fixed bottom-center, holds bar + accessory |
.glass-tab-bar-dock--accessory-left | Modifier: place accessory on the left |
.glass-tab-bar--floating | Modifier on .glass-tab-bar: pill shape, max-content width |
.glass-tab-bar__accessory | Standalone glass capsule next to the bar |
.glass-tab-bar__accessory--accent | Filled with primary color |
.glass-tab-bar__accessory--success | Filled with success color |
.glass-tab-bar__accessory--error | Filled with error color |
.glass-bg--has-tab-bar-floating | Background padding helper for the floating variant |
.glass-tab-bar--droplet | Modifier: 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--vertical | Modifier: the dock upright at the end edge, vertically centred; the bar inside takes --vertical (since 1.32.0) |
.glass-tab-bar-dock--start | With --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:
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.
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:
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.
| Token | Unset | Effect |
|---|---|---|
--gl-droplet-duration | 0.3s | The leading edge, and the edges across the way |
--gl-droplet-easing | cubic-bezier(0.3, 1.2, 0.5, 1) | Its curve |
--gl-droplet-trail-duration | 0.5s | The trailing edge, pulled after it |
--gl-droplet-trail-delay | 40ms | How long the trailing edge waits |
--gl-droplet-trail-easing | cubic-bezier(0.5, 1.5, 0.45, 0.92) | Its curve, with the overshoot |
--gl-droplet-lift | 0.86 | How flat the drop runs on the way; 1 squashes nothing |
--gl-droplet-lift-duration | 280ms | How long it runs flat |
--gl-droplet-settle-duration | 0.55s | The spring back into shape |
--gl-droplet-settle-easing | cubic-bezier(0.34, 1.8, 0.5, 1) | Its curve, the wobble at the end |
--gl-droplet-lens-duration | 0.4s | The item below the drop growing as under a lens |
--gl-droplet-lens-easing | cubic-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:
A finger dragging the drop keeps its own short timing, and with prefers-reduced-motion the drop still moves without travelling.
Breadcrumbs
The path through nested levels (since 1.32.0): My app › Main menu › Services › Online booking. A <nav aria-label="Breadcrumb"> around an <ol>; each crumb is a link, or a button, and the current level is plain text with aria-current="page". The separators are drawn by CSS, so assistive technology does not read them, and they turn around right to left. A crumb longer than --gl-breadcrumbs-crumb-width (200 px) is cut with an ellipsis, and where the row runs out of room every crumb gives way.
Where the path does not fit, __overflow stands for the middle levels: a button named like “2 more levels” that lists them in a menu, while the first crumb and the last two stay. Its <li> carries __item--overflow and keeps its width where the crumbs around it give way. GlassKit Elements’ <glk-breadcrumbs> folds and unfolds them by itself, measured on its own width, and shows a label that was cut in full in a tooltip.
A crumb can also take a dragged item into its level, for a sortable list or tree: .is-drop-target lights it up while the item is over it, .is-drop-refused shows that the level does not take it.
| Class | Description |
|---|---|
.glass-breadcrumbs | On the <nav>; gives way in a tight row (min-width: 0) |
__list | The <ol>: one row that does not wrap |
__item | The <li>; from the second on a chevron before it, mirrored right to left |
__crumb | Link or button to a level, muted, cut with an ellipsis beyond --gl-breadcrumbs-crumb-width (200 px) |
__current | The current level, plain text with aria-current="page", in the heading colour |
__overflow | With __crumb: the “…” button that stands for the folded levels |
__item--overflow | On the <li> of that button: keeps its width while the other crumbs give way |
.is-drop-target, .is-drop-refused | On a crumb while a dragged item is over it: it takes the item, or does not |
Title
Capture Contracts
& Upload
Card
Photograph your document, verify edges, and upload in batch.
| Class | Description |
|---|---|
.glass-card | Base glass surface |
.glass-card--glow | Light โ milky gradient + light streak |
.glass-card__icon | Centered icon wrapper |
.glass-card__text | Description text |
Buttons
| Class | Description |
|---|---|
.glass-btn | Base (56px, full width) |
--primary | Colored gradient |
--secondary | Milky white |
--tertiary | Subtle glass |
--sm | 44px height |
--lg | 64px height |
--auto | Auto width |
:disabled, [aria-disabled="true"] | Dimmed to 0.45, not-allowed cursor, no hover (since 1.21.2) |
svg.glass-icon--fill | Deliberately 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
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.
| Class | Description |
|---|---|
--interactive | Pressable: cursor, hover tint, focus ring, press feedback |
--selected | The chip that is on — deepens the badge's own colour |
--gl-badge-accent | The 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.
| Class / token | Description |
|---|---|
--md | A chip to choose with: 36 px high, the small text of a form; interactive, a tap target of 44 px |
--gl-badge-md-height | Its height, unset 36px; the tap target grows to 44 px around it |
In GlassKit Elements: <glk-badge interactive size="md">.
Avatar
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.
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>.
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 Update2.1 GB · Available now
-
Apple OneFamily — renews Apr 28$22.95
-
Notifications
- View all subscriptions
Compact menu (centered text, no icons)
- Share document
- Duplicate
- Delete
Grouped sections with large icons
-
Review downloaded mediaSave up to 1.38 GB. Review all videos and audio files downloaded on your device and remove them as needed.
-
AusweisApp BundVersion 2.5.124 MB
-
Ghost in the MachinePartially watched237.1 MB
- View all downloads
Class reference
| Class | Description |
|---|---|
.glass-list | Grouped surface container |
.glass-list--flush | Edge-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--bare | Strips background, border & shadow — for embedding inside .glass-popover or .glass-card. |
.glass-list__item | List row (use <li>, <a>, or <button>) |
.glass-list__section-header | Section label above a .glass-list — uppercase, small, muted |
.glass-list__item--interactive | Adds 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--center | Centered single-text variant with ellipsis truncation |
.glass-list__item--danger | Red text for destructive actions |
.glass-list__item--accent | Primary-colored text for accent actions |
.glass-list__leading | Leading slot (28×28, holds an icon SVG); --gl-list-leading-width widens it (since 1.36.0, below) |
.glass-list__leading--lg | Large leading slot (40×40, rounded square, supports <img>); the same token widens it |
.glass-list__content | Flexible middle slot — takes remaining width, enables truncation |
.glass-list__title | Primary text (medium weight, one line with ellipsis unless --gl-list-title-white-space lets it wrap) |
.glass-list__subtitle | Secondary text (small, muted, one line with ellipsis unless --gl-list-subtitle-white-space lets it wrap) |
.glass-list__title-row | Holds 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__extra | Below the subtitle: a link, a line of chips (since 1.35.0) |
.glass-list__subtitle--wrap | Multi-line subtitle (up to 3 lines, clamped with ellipsis; --gl-list-subtitle-lines sets the limit) |
.glass-list__trailing | Trailing slot — chevron, value, button, badge |
.glass-list__value | Muted text inside .glass-list__trailing (e.g. file size) |
.glass-list__item.is-flash | Flashes once, 1.2 s in --gl-flash-color, for an item that changed elsewhere (since 1.32.0) |
.glass-list__item.is-selected | The 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.
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.
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.
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.
- NewsChanged yesterday
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:41Opening hours: Saturday 7:00 to 14:00Opening hours · Lena Hoffmann
- 08:15News: Summer partyNews · Jonas Weber
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 hoursOpen in the editor
- News
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.
- NewsComponent
- Opening hoursComponent
- TeamComponent
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.
-
-
-
Home
-
Services
-
About us
- Team
-
-
Gallery Could not load
-
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:
| Class | Description |
|---|---|
.glass-tree | The list; no surface of its own |
__row | One 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 |
__twisty | The chevron; turns with .is-expanded, keeps its room but hides with .is-leaf, mirrored right to left |
__icon | The 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 |
__label | The label, one line with ellipsis |
__trailing | At the row’s end: a badge, a status, an action |
__group | The item’s children, below its row; hidden while closed |
.is-expanded, .is-leaf | On the row: open, or without children |
.is-selected | The chosen row: raised glass like the active row of a sidebar, semibold, its icon in the accent |
.is-loading | Children on their way: the twisty turns into a small spinner (with aria-busy="true" on the item) |
.is-error | The children could not load: the twisty turns into a “!” in the error colour; opening the item again tries once more |
.is-flash | A 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-disabled | Dimmed, not-allowed cursor |
.is-focus-visible | The 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.
-
-
Home
-
About us
-
Legal notice
-
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.
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.
| Class | Description |
|---|---|
.is-sort-source | The dragged item where it was, faded to 40 % |
.is-sort-before, .is-sort-after | A 2 px line in the primary above or below the item, indented to --gl-tree-level |
.is-sort-inline | With .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-inside | The 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-refused | The item does not take it: the error surface and ring; a line turns red |
.glass-sort__ghost | The raised copy that follows the pointer; [popover] in the top layer |
.glass-sort__handle | The grip: only it starts a drag, and a finger on it drags at once |
Modal
Centered dialog with blur overlay. Visibility is controlled via the .is-active class on the overlay element — toggling it requires a tiny bit of JavaScript.
Delete document?
This action cannot be undone.
Class reference
| Class | Description |
|---|---|
.glass-modal-overlay | Fullscreen blur overlay |
.glass-modal-overlay.is-active | Visible + animated |
dialog.glass-modal-overlay | The overlay as a native dialog, opened with showModal() (since 1.20.0) |
.glass-modal | Dialog container, at most --gl-modal-max-width wide (340px unless set) |
__header / __title | Title area |
__body | Content area |
__footer / __action | Action bar |
__action--primary | Primary action |
__action--danger | Dangerous action |
Note: the .is-active class belongs on the .glass-modal-overlay, not on .glass-modal itself. The overlay handles the fullscreen backdrop, and its :is-active state triggers both the backdrop fade-in and the inner dialog's scale-up animation.
As a native dialog
Since 1.20.0 the overlay may also be a <dialog>. Opened with showModal() it lies in the top layer; the page behind it is inert for the pointer, the keyboard and screen readers; focus moves in and goes back to where it came from on closing; Escape closes it. GlassKit undoes the dialog’s own box and backdrop, so it looks exactly like the <div> overlay. Name it by its title with aria-labelledby. GlassKit Elements’ <glk-modal> works this way.
While a modal dialog is open, everything outside it is inert — a toast shown then is neither clickable nor announced. Give feedback inside the dialog, or close it first.
Width
The panel is at most --gl-modal-max-width wide — 340 px unless set (since 1.20.0; before, a literal). A dialog with a form or a table can take more:
--gl-modal-bg replaces the panel’s surface (since 1.35.0). In dark mode the glow starts at --gl-dialog-glow-top, the card’s middle glow: on the brightest part of a tall dialog, white text on controls read 2.9 to 4.7:1 — see dark glass.
Blur behind the dialog
The dimmer behind a dialog, and behind a sheet, blurs with --gl-blur-overlay where a project sets it, else with --gl-blur-soft (since 1.25.0). The surface presets set the blur tokens to 0 but leave this one alone, so a page without glass can keep a blurred dimmer:
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.
| Class | Description |
|---|---|
.glass-toast, .is-visible | Fixed at the top, faded in while visible |
--success, --error, --warning | Colour the icon; tint the action in the state colour |
__icon, __text | 22 px stroked icon, message (wraps and yields) |
__action | One action: pill in the toast’s tone, 44 px hit area, never shrinks (since 1.19.0) |
__close | The ×, 28 px with a 44 px hit area (since 1.19.0) |
--gl-toast-top | Distance 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.
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.
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.
Class reference
| Class | Description |
|---|---|
.glass-popover-anchor | Positioning context for the popover (wraps trigger + popover) |
.glass-popover | Floating glass surface, hidden until .is-open |
.glass-popover.is-open | Visible state — fade + scale animation |
.glass-popover--top | Opens upward (above the trigger) |
.glass-popover--start | Aligns left edge with trigger |
.glass-popover--end | Aligns 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.
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.
| Token | Unset | Effect |
|---|---|---|
--gl-tooltip-max-width | 240px | The 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-text | Its 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).
Input
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.
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.
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.
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.
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.
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.
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.
Search
Toggle Switch
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.
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:
Range Slider
Progress Bar
| Class | Description |
|---|---|
.glass-progress | Base container |
--sm / --lg | Track height: 4px / 12px |
--success / --error | Color variants |
Accordion
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.
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.
| Class | Description |
|---|---|
.glass-skeleton | Stack of lines, 10px apart |
.glass-skeleton__line | One shimmer line (12px); width via inline style |
.glass-skeleton__line--title | Taller 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.
| Date | Item | Amount |
|---|---|---|
| 2026-09-18 | Invoice #1042 | 1,240.00 |
| 2026-09-19 | Refund | −80.00 |
| 2026-09-20 | Pending | โ |
| Class | Description |
|---|---|
.glass-table-wrap | Optional horizontal scroll container |
.glass-table | The table: hairline rows, hover highlight |
.glass-table__num | Right-aligned, tabular figures — on th and td |
.glass-table__muted | Secondary 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-1048 | Northwind Bakery | 2026-10-27 | 1,240.00 | Open | Business yearly | |
| 2026-1047 | Harbor Dental | 2026-10-26 | 89.00 | Paid | Starter monthly | |
| 2026-1046 | Bluebell Florist | 2026-10-25 | 2,310.50 | Paid | Business monthly | |
| 2026-1045 | Kestrel Studio | 2026-10-24 | 412.80 | Overdue | Starter yearly | |
| 2026-1044 | Morning Light Yoga | 2026-10-23 | 75.00 | Paid | Starter monthly | |
| 2026-1043 | North Bike Works | 2026-10-22 | 640.00 | Open | Business yearly | |
| 2026-1042 | Weber & Roth Law | 2026-10-21 | 3,480.00 | Paid | Business yearly |
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.
| Invoice | Customer | Amount | Status | |
|---|---|---|---|---|
| 2026-1048 | Northwind Bakery | 1,240.00 | Open | |
| 2026-1047 | Harbor Dental | 89.00 | Paid |
Empty
While no row is there, __empty below the table holds an empty state and the header stays; hidden takes it away again.
| Invoice | Customer | Amount | Status |
|---|
No invoices
Nothing matches this filter.
| Class | Description |
|---|---|
__sort, __sort-icon | Button 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--sticky | The header sticks to the top of a wrap that scrolls both ways, at most --gl-table-max-height high |
__actions | Cell for row actions, at the row’s end, kept there while the table scrolls sideways |
--scrolls | Set while the table scrolls sideways: the actions on a surface of their own, with a shadow |
.glass-table-frame | Around the wrap; shades the start or end edge with data-more-start, data-more-end |
__empty | Below the table in its wrap, the room for an empty state |
--list | Each row a card, the labels from data-label, the actions at its top end |
| Token | Unset | Effect |
|---|---|---|
--gl-table-cell-padding | 9px 10px | The room in the body cells |
--gl-table-header-padding | 8px 10px | The room in the header cells |
--gl-table-max-height | 60vh | How 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.
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.
| Class | Description |
|---|---|
.glass-segmented | The group: inset glass track, buttons inside |
--full | Buttons share the width |
--scroll | One row that scrolls sideways when the options do not fit, scrollbar hidden (since 1.17.0) |
--wrap | Breaks the row into lines when the options do not fit (since 1.17.0) |
__item | One option; aria-pressed="true" marks the chosen one, disabled dims it |
__item--success / --warning / --error | Tone: sets --gl-segmented-tone for the dot |
__dot | The dot before the label, in the tone colour |
__icon | An icon before the label, in the option’s colour (since 1.28.0) |
__badge | A 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, --error | The count in a colour of its own, whatever the option’s tone, without the dot (since 1.31.0) |
--icons | Icons alone, square options; the names go into aria-label (since 1.28.0) |
__icon--color | A coloured logo keeps its own fills and strokes; without it an SVG strokes as a line icon (since 1.35.0) |
--droplet | The 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.
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.
| Token | Unset | Effect |
|---|---|---|
--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-heading | Label and icon of the chosen option, and of the option below a dragged drop |
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”.
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.
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.
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".
- Day
- Slot
- 3Dog
- 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:
- Day
- 2Choose a slot
- 3Dog
- 4Confirm
| Class | Description |
|---|---|
.glass-steps | The list; a size container, so give it a width inside a flex row |
__item, __item--done, __item--current | One step and its states |
__num | The circle: number, or an SVG check when done |
__label | The label; hidden below 360 px except on the current step |
__line | Hairline 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 …
| Class | Description |
|---|---|
.glass-sheet-overlay | Fixed overlay: scrim, blur, panel aligned to the bottom; starts at opacity 0 |
.is-active | Overlay visible, panel risen — toggled by script |
.glass-sheet | The panel: milky glass, top corners rounded, bottom safe-area padding |
--inline | In the flow, no overlay, all corners rounded |
__grip, __title, __body, __actions | Grip bar, heading, body, action column |
__head, __heading | The 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) |
__eyebrow | A short line above the title, small capitals in the muted colour |
__subtitle | Below the title: a badge, a line of text, small in the muted colour (since 1.36.0) |
__leading, __trailing | Before and after the title: a mark or an icon chip, a badge |
__close | A 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, --end | The panel at that side, full height, from the side, without grip, actions side by side (since 1.35.0) |
.glass-sheet-overlay--modeless | No 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.
Version from 09:41
Picture gallery
1× in your appAt 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.
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.
| Token | Dark value | Usage |
|---|---|---|
--gl-dialog-glow-top | var(--gl-card-glow-mid) | Where the glow of a modal and a sheet starts |
--gl-dialog-well | rgba(0, 0, 0, 0.10) | The track of a segmented control on dialog glass |
--gl-dialog-raised | var(--gl-surface-3) | Its chosen option |
--gl-dialog-control | var(--gl-surface-1) | A tertiary button on dialog glass |
--gl-badge-selected-color | var(--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.
| Class | Description |
|---|---|
.glass-empty | Centred column |
__icon | 48 px round plate, 24 px stroked icon inside (::slotted(svg) twin for Elements) |
__title, __text | Heading and muted text (max 38 ch) |
__action | Room 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.
| Class | Description |
|---|---|
.glass-date-strip | Scrolling row; hidden scrollbar; 4 px padding so its own overflow does not clip the focus ring |
__day | Chip 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--today | Underlines the number |
__wd, __num | Weekday in small caps, day number |
__mark | 6 px dot; transparent without a tone |
__mark--primary, --success, --warning, --error | Tone 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.
| Class | Description |
|---|---|
.glass-calendar | Block; __head with __title (ellipsis when tight) and two __nav buttons, 32 px round, with an SVG chevron |
__grid | Seven columns, 4 px gap |
__wd | Weekday header cell, muted small caps |
__day | Square 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, __mark | 5 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.
| Class | Description |
|---|---|
.glass-image-picker | Row: plate left, column right, 14 px gap |
__preview | 72 px plate; img covers it, svg is the 28 px placeholder in the muted icon colour |
__preview--round | Circle, for avatars |
__meta | Column that yields (min-width: 0) |
__label, __hint | Label in the look of .glass-label, wraps anywhere; small muted hint |
__actions | Wrapping 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
| Component | Classes | Modifier / 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__, .glass-sort__ | .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- | __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.
| Feature | Description |
|---|---|
| Copy-paste HTML | All 38 components with exact nesting and BEM hierarchy |
| Design token tables | Colors, surfaces, blur, radii, spacing, shadows, typography |
| State class mapping | is-active, is-open, is-visible, :checked โ where each belongs |
| Composition patterns | 6 full page layouts: Login, Dashboard, Form, Modal, Settings, Progress |
| Common mistakes | Frequent errors and their corrections |
Attach or reference SKILL.md when asking an AI to generate GlassKit HTML for production-ready output.
๐งฉ 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.
| Layer | Project | Use it for |
|---|---|---|
| CSS foundation | ๐ง GlassKit (you are here) | The design language: pure-CSS components, design tokens, dark & light. |
| App layer | GlassKit Elements | Drop-in web components (glk-button, glk-card, โฆ) for application UIs. |
| Website layer | GlassKit Web | Building a complete website โ the official Astro template with sections, i18n and SEO on the GlassKit foundation. |