Default Accent Subtle
Default Large
On dark
Sending message
/*
* Dots — foundation
*
* Inline loading indicator: three dots pulsing in sequence. CSS-only.
* Smaller and quieter than Spinner — designed to live inside running
* text or button labels.
*
* <span class="dots" role="status" aria-label="Loading">
* <span class="dots__item"></span>
* <span class="dots__item"></span>
* <span class="dots__item"></span>
* </span>
*/
@layer malevich.components {
.dots {
--dots-size: var(--space-inset-element-s);
--dots-gap: var(--space-inset-element-s);
--dots-color: var(--color-ink-regular);
--dots-duration: 1200ms;
display: inline-flex;
align-items: center;
gap: var(--dots-gap);
vertical-align: middle;
}
.dots__item {
inline-size: var(--dots-size);
block-size: var(--dots-size);
border-radius: 50%;
background-color: var(--dots-color);
opacity: 0.3;
animation: malevich-dots-pulse var(--dots-duration) ease-in-out infinite;
}
.dots__item:nth-child(1) { animation-delay: 0ms; }
.dots__item:nth-child(2) { animation-delay: 200ms; }
.dots__item:nth-child(3) { animation-delay: 400ms; }
.dots.-accent {
--dots-color: var(--color-accent);
}
.dots.-subtle {
--dots-color: var(--color-ink-subtle);
}
.dots.-inverse {
--dots-color: var(--color-ink-inverse);
}
.dots.-l {
--dots-size: var(--space-inset-element-m);
--dots-gap: var(--space-inset-element-m);
}
@keyframes malevich-dots-pulse {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.dots__item {
animation: none;
opacity: 0.6;
}
}
}
This component is pure CSS — no JavaScript required.
# Dots — AGENTS.md
> Auto-generated from `dots.docs.md` + the modifier applicability matrix.
> Edit those source files; this file regenerates on build.
## Identity
- **Component:** `dots`
- **Layer:** foundations
- **Status:** stable
- **Last updated:** 2026-05-18
## Summary
- Inline next to a label that's loading: "Sending…".
## Variants
| Variant | Class | Use for |
|-----------|---------------------|----------------------------------------|
| Default | `.dots` | Ink-regular dots, default size |
| Accent | `.dots.-accent` | Accent-colored, for emphasis |
| Subtle | `.dots.-subtle` | De-emphasized, for muted contexts |
| Inverse | `.dots.-inverse` | On dark / accent surfaces |
| Large | `.dots.-l` | Larger dots and gap |
## Modifier applicability
Per ADR-0007. Modifiers attach via `data-{category}="value"` attributes.
| Category | Accepts |
|---|---|
| `background` | — |
| `surface` | — |
| `effect` | — |
| `shader` | — |
| `rhythm` | — |
| `motion` | `none`, `standard` |
| `behavior` | — |
## Anatomy
```html
<span class="dots" role="status" aria-label="Loading">
<span class="dots__item"></span>
<span class="dots__item"></span>
<span class="dots__item"></span>
</span>
<!-- Inside running text -->
<p>
Sending message
<span class="dots -subtle" role="status" aria-label="Loading">
<span class="dots__item"></span>
<span class="dots__item"></span>
<span class="dots__item"></span>
</span>
</p>
```
## Tokens consumed
### From semantic tier
- `--color-ink-regular` — default dot color
- `--color-ink-subtle` — subtle variant
- `--color-ink-inverse` — inverse variant
- `--color-accent` — accent variant
- `--space-inset-element-s` — default size and gap
- `--space-inset-element-m` — large size and gap
### Component-tier (defined inline)
- `--dots-size` — overridable dot diameter
- `--dots-gap` — overridable gap between dots
- `--dots-color` — overridable color
- `--dots-duration` — overridable pulse cycle
## Accessibility contract
Authors set `role="status"` and `aria-label="Loading"` on the outer
`.dots` element so assistive technology announces the loading state.
The three `.dots__item` children are presentational; they do not need
ARIA attributes. They render purely as visual rhythm.
The component honors `prefers-reduced-motion: reduce` by removing the
pulse animation and showing the dots at a constant opacity.
## Guidance
### Do
- Use dots when the work is inline-scoped (a single label, a single
cell in a table).
- Use spinner when the work is layout-scoped (a whole section, a
modal body).
### Don't
- Don't combine dots with a spinner in the same loading context — pick
one indicator vocabulary per surface.
- Don't author more or fewer than three items. The component is a
fixed-shape primitive.