Display
The small pieces: avatar, breadcrumb, progress, skeleton, tooltip. None of them introduce a new idea, which is why they are one page rather than five.
Reference
sk-avatar
An image if there is one, initials if there is not - never a generic silhouette, because initials tell two rows apart at a glance and a grey person icon does not. Initials are set at 40% of the circle so they hold their proportion at every size.
<span class="sk-avatar" data-size="sm">
<img src="…" alt="Ada Lovelace">
</span>| Attribute | Type | Default | Notes |
|---|---|---|---|
data-size | "xs" | "sm" | "lg" | "xl" | md | 1.5 / 2 / 2.5 / 3 / 4rem |
data-shape | "square" | circle | Conventional for organisations and projects, as opposed to people. |
data-overflow | "true" | - | The "+3" chip that ends a stack. |
sk-avatar-group | wrapper | - | Overlaps the stack. The ring is a shadow, not a border, so it does not eat into the image. |
sk-breadcrumb-list
Separators are real elements marked aria-hidden, not CSS ::after content. Generated content is announced by several screen readers - turning a trail into "Home slash Settings slash Billing" - and it cannot be hidden from the accessibility tree, because you cannot put an attribute on a pseudo-element.
<nav aria-label="Breadcrumb">
<ol class="sk-breadcrumb-list">
<li class="sk-breadcrumb-item"><a class="sk-breadcrumb-link" href="/">Home</a></li>
<li class="sk-breadcrumb-item">
<span class="sk-breadcrumb-separator" aria-hidden="true">…</span>
<span class="sk-breadcrumb-current" aria-current="page">Billing</span>
</li>
</ol>
</nav>| Attribute | Type | Default | Notes |
|---|---|---|---|
aria-current | "page" | - | On the last crumb, which is where you are and therefore not a link. |
sk-progress
A native <progress>: it carries its own role, value semantics and indeterminate state, none of which a div with a width has. Styling costs three pseudo-elements that cannot share a rule - an unknown selector invalidates the whole list, so ::-webkit- and ::-moz- are written separately.
<progress class="sk-progress" value="40" max="100" aria-labelledby="x"></progress>| Attribute | Type | Default | Notes |
|---|---|---|---|
value | number | - | Omit entirely for the indeterminate state - a sliding band rather than a filled bar, so it cannot be misread as "almost done". |
data-status | "success" | "warning" | "danger" | accent | |
data-size | "sm" | "lg" | md | 4 / 8 / 12px |
aria-labelledby | id | - | A bare progress bar announces a number with no subject. |
sk-skeleton
Always aria-hidden. A screen reader user gets nothing from "grey box, grey box, grey box" - the real announcement belongs in the live region that reports the result. The shimmer is a highlight over a flat base, so a skeleton with animation suppressed is still visibly a placeholder rather than blank.
<span class="sk-skeleton" data-shape="text" aria-hidden="true"></span>
<span class="sk-skeleton-lines" aria-hidden="true">…</span>| Attribute | Type | Default | Notes |
|---|---|---|---|
data-shape | "text" | "circle" | "block" | bar | Sized in em, so a text skeleton is the height of the line it stands in for. |
data-size | "sm" | "md" | "lg" | "xl" | - | For a circle standing in for an avatar, which should be the avatar's size and not a line of text. |
data-busy | "true" | - | Shimmers. Opt-in, matching Dot: a placeholder still pulsing after the request died is a lie the interface keeps telling. Turn it off when the wait ends, however it ends. |
data-motion | "essential" | - | Emitted with data-busy. Under reduced motion the sweep becomes an opacity pulse - a sweep across a screenful of blocks is what the preference is for, but freezing it makes a loading screen indistinguishable from a broken one. |
sk-tooltip
Shown by :hover and :focus-within, so it works on keyboard focus as well as pointer. It lives in a positioned wrapper rather than the top layer - a popover would escape ancestor clipping, but showing one needs a script, since popovertarget is click-only and there is no declarative hover trigger. The consequence, stated rather than hidden: a tooltip inside a scroll container is clipped by it.
<span class="sk-tooltip-wrap">
<button class="sk-button" aria-describedby="tip">…</button>
<span class="sk-tooltip" id="tip" role="tooltip">Archive</span>
</span>| Attribute | Type | Default | Notes |
|---|---|---|---|
data-side | "bottom" | top | |
aria-describedby | tooltip id | - | On the control, not the tooltip. Without it the tooltip is drawn but never announced. |
sk-input[list] + datalist
Native suggestions, zero JavaScript. The browser supplies filtering, keyboard handling, scrolling and the mobile presentation - all of which a scripted combobox reimplements and usually gets partly wrong. A datalist is a hint, not a constraint: use a Select when the answer must come from the list.
<input class="sk-input" id="country" list="country-list" autocomplete="off">
<datalist id="country-list">
<option value="New Zealand"></option>
</datalist>| Attribute | Type | Default | Notes |
|---|---|---|---|
list | datalist id | - | No styled options, no custom option rendering, no multi-select, no async loading. Those need the scripted ARIA combobox - the first component here that would require shipping JavaScript. |
Examples
Avatar
Image, initials, and the sizes.
Show markup
<div class="sk-flex sk-flex-col sk-gap-5">
<div class="sk-flex sk-flex-wrap sk-items-center sk-gap-3">
<span class="sk-avatar" data-size="xs">
<span aria-hidden="true">
AL
</span>
<span class="sk-visually-hidden">
Ada Lovelace
</span>
</span>
<span class="sk-avatar" data-size="sm">
<span aria-hidden="true">
AL
</span>
<span class="sk-visually-hidden">
Ada Lovelace
</span>
</span>
<span class="sk-avatar">
<span aria-hidden="true">
AL
</span>
<span class="sk-visually-hidden">
Ada Lovelace
</span>
</span>
<span class="sk-avatar" data-size="lg">
<span aria-hidden="true">
AL
</span>
<span class="sk-visually-hidden">
Ada Lovelace
</span>
</span>
<span class="sk-avatar" data-size="xl">
<span aria-hidden="true">
AL
</span>
<span class="sk-visually-hidden">
Ada Lovelace
</span>
</span>
</div>
<div class="sk-flex sk-flex-wrap sk-items-center sk-gap-3">
<span class="sk-avatar">
<span aria-hidden="true">
AL
</span>
<span class="sk-visually-hidden">
Ada Lovelace
</span>
</span>
<span class="sk-avatar">
<span aria-hidden="true">
GH
</span>
<span class="sk-visually-hidden">
Grace Hopper
</span>
</span>
<span class="sk-avatar">
<span aria-hidden="true">
AT
</span>
<span class="sk-visually-hidden">
Alan Turing
</span>
</span>
<span class="sk-avatar">
<span aria-hidden="true">
KJ
</span>
<span class="sk-visually-hidden">
Katherine Johnson
</span>
</span>
<span class="sk-avatar" data-shape="square">
<span aria-hidden="true">
SG
</span>
<span class="sk-visually-hidden">
Soft Kiwi Games
</span>
</span>
</div>
</div>Avatar group
The ring uses the surface colour, so a stack reads correctly on whatever it sits on.
Show markup
<span class="sk-avatar-group" role="group" aria-label="Team members">
<span class="sk-avatar" data-size="sm">
<span aria-hidden="true">
AL
</span>
<span class="sk-visually-hidden">
Ada Lovelace
</span>
</span>
<span class="sk-avatar" data-size="sm">
<span aria-hidden="true">
GH
</span>
<span class="sk-visually-hidden">
Grace Hopper
</span>
</span>
<span class="sk-avatar" data-size="sm">
<span aria-hidden="true">
AT
</span>
<span class="sk-visually-hidden">
Alan Turing
</span>
</span>
<span class="sk-avatar" data-size="sm">
<span aria-hidden="true">
KJ
</span>
<span class="sk-visually-hidden">
Katherine Johnson
</span>
</span>
<span class="sk-avatar" data-overflow="true" data-size="sm">
<span aria-hidden="true">
+3
</span>
<span class="sk-visually-hidden">
3 more
</span>
</span>
</span>Breadcrumb
Show markup
<div class="sk-flex sk-flex-col sk-gap-4">
<nav aria-label="Breadcrumb">
<ol class="sk-breadcrumb-list">
<li class="sk-breadcrumb-item">
<a class="sk-breadcrumb-link" href="/">
Home
</a>
</li>
<li class="sk-breadcrumb-item">
<span class="sk-breadcrumb-separator" aria-hidden="true">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6">
</path>
</svg>
</span>
<a class="sk-breadcrumb-link" href="/settings">
Settings
</a>
</li>
<li class="sk-breadcrumb-item">
<span class="sk-breadcrumb-separator" aria-hidden="true">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6">
</path>
</svg>
</span>
<span class="sk-breadcrumb-current" aria-current="page">
Billing
</span>
</li>
</ol>
</nav>
<nav aria-label="Project">
<ol class="sk-breadcrumb-list">
<li class="sk-breadcrumb-item">
<a class="sk-breadcrumb-link" href="/">
Home
</a>
</li>
<li class="sk-breadcrumb-item">
<span class="sk-breadcrumb-separator" aria-hidden="true">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6">
</path>
</svg>
</span>
<a class="sk-breadcrumb-link" href="/p">
Projects
</a>
</li>
<li class="sk-breadcrumb-item">
<span class="sk-breadcrumb-separator" aria-hidden="true">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6">
</path>
</svg>
</span>
<a class="sk-breadcrumb-link" href="/p/1">
Kiwi Kart
</a>
</li>
<li class="sk-breadcrumb-item">
<span class="sk-breadcrumb-separator" aria-hidden="true">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6">
</path>
</svg>
</span>
<a class="sk-breadcrumb-link" href="/p/1/b">
Builds
</a>
</li>
<li class="sk-breadcrumb-item">
<span class="sk-breadcrumb-separator" aria-hidden="true">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="m9 18 6-6-6-6">
</path>
</svg>
</span>
<span class="sk-breadcrumb-current" aria-current="page">
Build 412
</span>
</li>
</ol>
</nav>
</div>Progress
Show markup
<div class="sk-flex sk-flex-col sk-gap-5">
<div class="sk-progress-field">
<div class="sk-progress-head">
<span id="Uploading-progress-label">
Uploading
</span>
<span class="sk-progress-value">
40%
</span>
</div>
<progress class="sk-progress" value="40" max="100" aria-labelledby="Uploading-progress-label">
</progress>
</div>
<div class="sk-progress-field">
<div class="sk-progress-head">
<span id="Files-progress-label">
Files
</span>
<span class="sk-progress-value">
3 of 12 files
</span>
</div>
<progress class="sk-progress" value="3" max="12" aria-labelledby="Files-progress-label">
</progress>
</div>
<div class="sk-progress-field">
<div class="sk-progress-head">
<span id="Storage-progress-label">
Storage
</span>
<span class="sk-progress-value">
92%
</span>
</div>
<progress class="sk-progress" value="92" max="100" data-status="warning" aria-labelledby="Storage-progress-label">
</progress>
</div>
<div class="sk-progress-field">
<div class="sk-progress-head">
<span id="Complete-progress-label">
Complete
</span>
<span class="sk-progress-value">
100%
</span>
</div>
<progress class="sk-progress" value="100" max="100" data-status="success" aria-labelledby="Complete-progress-label">
</progress>
</div>
<div class="sk-progress-field">
<div class="sk-progress-head">
<span class="sk-progress-value">
60%
</span>
</div>
<progress class="sk-progress" value="60" max="100" data-size="sm">
</progress>
</div>
</div>Indeterminate
No value attribute. A band that slides rather than a bar that fills.
Show markup
<div class="sk-flex sk-flex-col sk-gap-4">
<div class="sk-progress-field">
<div class="sk-progress-head">
<span id="Working-progress-label">
Working
</span>
</div>
<progress class="sk-progress" max="100" aria-labelledby="Working-progress-label">
</progress>
</div>
<progress class="sk-progress" max="100" data-size="lg">
</progress>
</div>Skeleton
Busy and static side by side. The last line of the paragraph is short, which is most of what makes it read as text rather than as bars.
Busy: true - waiting on somethingstatic - an empty slot, or a request that has stoppedShow markup
<div class="sk-flex sk-flex-col sk-gap-5">
<div class="sk-flex sk-flex-col sk-gap-2">
<code class="sink-caption">
Busy: true - waiting on something
</code>
<div class="sk-card">
<div class="sk-card-body">
<div class="sk-flex sk-gap-3">
<span class="sk-skeleton" aria-hidden="true" data-shape="circle" data-size="lg" data-busy="true" data-motion="essential">
</span>
<div class="sk-flex sk-flex-col sk-gap-2 sk-flex-1">
<span class="sk-skeleton-lines" aria-hidden="true">
<span class="sk-skeleton" data-shape="text" data-busy="true" data-motion="essential">
</span>
<span class="sk-skeleton" data-shape="text" data-busy="true" data-motion="essential">
</span>
<span class="sk-skeleton" data-shape="text" data-busy="true" data-motion="essential">
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="sk-flex sk-flex-col sk-gap-2">
<code class="sink-caption">
static - an empty slot, or a request that has stopped
</code>
<div class="sk-card">
<div class="sk-card-body">
<div class="sk-flex sk-gap-3">
<span class="sk-skeleton" aria-hidden="true" data-shape="circle" data-size="lg">
</span>
<div class="sk-flex sk-flex-col sk-gap-2 sk-flex-1">
<span class="sk-skeleton-lines" aria-hidden="true">
<span class="sk-skeleton" data-shape="text">
</span>
<span class="sk-skeleton" data-shape="text">
</span>
<span class="sk-skeleton" data-shape="text">
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="sk-card">
<span class="sk-skeleton" aria-hidden="true" data-shape="block" data-busy="true" data-motion="essential">
</span>
<div class="sk-card-body">
<span class="sk-skeleton-lines" aria-hidden="true">
<span class="sk-skeleton" data-shape="text" data-busy="true" data-motion="essential">
</span>
<span class="sk-skeleton" data-shape="text" data-busy="true" data-motion="essential">
</span>
</span>
</div>
</div>
</div>Tooltip
Hover or tab to them. The keyboard case is the one most tooltip implementations forget.
Show markup
<div class="sk-flex sk-flex-wrap sk-items-center sk-gap-5">
<span class="sk-tooltip-wrap">
<button class="sk-button" type="button" data-variant="neutral" data-size="md" aria-describedby="tip-archive" aria-label="Open in a new tab">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M14 4h6v6">
</path>
<path d="M20 4 11 13">
</path>
<path d="M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4">
</path>
</svg>
</button>
<span class="sk-tooltip" id="tip-archive" role="tooltip">
Open in a new tab
</span>
</span>
<span class="sk-tooltip-wrap">
<button class="sk-button" type="button" data-variant="danger" data-size="md" aria-describedby="tip-delete" aria-label="Delete permanently">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M4 7h16">
</path>
<path d="M10 11v6">
</path>
<path d="M14 11v6">
</path>
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12">
</path>
<path d="M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2">
</path>
</svg>
</button>
<span class="sk-tooltip" id="tip-delete" role="tooltip">
Delete permanently
</span>
</span>
<span class="sk-tooltip-wrap">
<button class="sk-button" type="button" data-variant="quiet" data-size="md" aria-describedby="tip-below">
Below
</button>
<span class="sk-tooltip" id="tip-below" role="tooltip" data-side="bottom">
This one opens downward
</span>
</span>
</div>Combobox
Native suggestions. Type a letter.
Suggestions only - any value is allowed.
Show markup
<div class="sk-flex sk-flex-col sk-gap-4">
<div class="sk-field">
<label class="sk-label" for="country">
Country
</label>
<input class="sk-input" id="country" name="country" type="text" list="country-list" value="" placeholder="Start typing" autocomplete="off" data-size="md" aria-invalid="false" aria-describedby="country-hint">
<datalist id="country-list">
<option value="New Zealand">
</option>
<option value="Netherlands">
</option>
<option value="Norway">
</option>
<option value="Nigeria">
</option>
<option value="Nepal">
</option>
</datalist>
<p class="sk-hint" id="country-hint">
Suggestions only - any value is allowed.
</p>
</div>
</div>