Skip to content
Design system
Platform Home Open Console

Tactile Precision

Afara design system

A calm, precise interface language for cloud and object-storage infrastructure. Tokens, components and infrastructure patterns in plain HTML, CSS and JavaScript, built to be read by people and by AI coding agents.

acme-assets-prod Production
Storage used62 GB of 100 GB
Within limitWarns at 80%
Healthy Sample data

What is in the package

Tokens
348
Primitive
134
Semantic
126
Component
88
Themes
2
  • Tokens in the W3C Design Tokens (DTCG 2025.10) format, with CSS custom properties for a light and a dark theme, both contrast-checked.
  • About 90 components and patterns across actions, navigation, forms, data display, overlays, feedback, and 20 infrastructure components built for storage products.
  • This site: every demo is the real stylesheet running, with copyable HTML, the real CSS, and the tokens it reads.
  • Five example pages assembled only from the documented components, and an AI implementation guide with explicit rules.

Use it in a page

No build step is required. Link one stylesheet and the scripts you need; icons are an inline SVG sprite.

HTML
<!doctype html>
<html lang="en" data-theme="light">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Buckets · Afara</title>
  <link rel="stylesheet" href="styles/afara.css">
</head>
<body>
  <!-- inline the icon sprite (assets/icons/sprite.inline.html) as the first child of body -->
  <main class="af-container">…</main>
  <script src="scripts/theme.js"></script>
  <script src="scripts/icons.js"></script>
  <script src="scripts/component-interactions.js"></script>
  <script src="scripts/component-widgets.js"></script>
</body>
</html>

Start

Philosophy

Infrastructure should be a foundation, not a barrier. The interface has to feel dependable before it feels beautiful.

Tactile Precision

Afara's interface is quiet, exact and physical. Controls behave like well-made instruments: a button is a key that you press in, a field you type into is recessed, a panel is paper on a bench. Structure is drawn with thin, deliberate lines and tonal steps, never with glow, gradients or decorative shadow.

Precision is a promise as much as a look. Numbers align in tabular columns, units are stated, the resource you are acting on is always named, and a status is never inferred from colour alone.

Dependability over noise

Reliability, security and clarity outrank feature claims. No ambient animation, no decoration that implies infrastructure Afara does not operate.

Simple on the surface, serious underneath

Progressive disclosure: the common path is short; advanced controls are one step away, never hidden.

Accessible by design

Contrast, keyboard operation, focus and reduced motion are properties of every component, not an audit at the end.

Open and portable

Plain HTML, CSS and JavaScript. Tokens in an open standard. No framework lock-in.

Developer respect

Precise language, specific error messages and predictable behaviour are part of the design.

Four depth treatments

There are exactly four ways a surface can sit in space. Every component uses one of them.

Depth language
TreatmentWhat it looks likeUsed for
FlatHairline border, no shadowCards, tables, panels. About 90% of surfaces.
RecessedInset shadowInputs, code, meter tracks, wells.
RaisedLight top edge, hard 1px lower edgeButtons and other controls. Never cards.
FloatingSoft elevated shadow plus a borderMenus, popovers, drawers, dialogs, toasts.

Voice

Do write

“Upload files directly to your storage bucket using a short-lived, secure URL.”

“Upload failed: backup.tar.zst is larger than the 5 GB single-request limit. Use multipart upload.”

Don't write

“Experience the future of limitless, next-generation file management.”

“Something went wrong.” · “Success!” · “Unlimited”, “instant”, “zero downtime”, “the fastest”.

Source of truth: brand/brand.md v1.0. Where the brand document and this system disagree, the brand document wins and the disagreement is recorded in DESIGN_SYSTEM.md.

Foundations

Foundations

Three tiers of tokens, seven cascade layers, one naming scheme. Everything else in the system is built on these.

Token tiers

Token tiers
TierMeaningMay referenceExample
PrimitiveA raw value with no meaning attachedPrimitives only--prim-color-teal-600, --space-4
SemanticA role: what the value is forPrimitives and semantic tokens--color-accent-solid, --color-border-control
ComponentA decision for one componentSemantic tokens for colour; any non-colour token--input-border-focus, --modal-radius

Colour primitives are private (--prim-color-*) and are never used in component CSS. Components read semantic or component tokens, which is what makes the dark theme a designed theme rather than an inversion.

88 component tokens in 22 groups. Each one aliases a semantic token (colour) or a primitive (everything else).

button (17)
  • --button-height-xs
  • --button-height-sm
  • --button-height-md
  • --button-height-lg
  • --button-height-touch-xs
  • --button-height-touch-sm
  • --button-padding-x-xs
  • --button-padding-x-sm
  • --button-padding-x-md
  • --button-padding-x-lg
  • --button-gap
  • --button-radius
  • --button-font-size-xs
  • --button-font-size-sm
  • --button-font-size-md
  • --button-font-size-lg
  • --button-font-weight
input (13)
  • --input-height-sm
  • --input-height-md
  • --input-height-lg
  • --input-padding-x
  • --input-radius
  • --input-bg
  • --input-border
  • --input-border-hover
  • --input-border-focus
  • --input-border-invalid
  • --input-border-valid
  • --input-placeholder
  • --input-shadow
card (7)
  • --card-radius
  • --card-padding-sm
  • --card-padding-md
  • --card-padding-lg
  • --card-border
  • --card-border-hover
  • --card-gap
badge (4)
  • --badge-height
  • --badge-padding-x
  • --badge-radius
  • --badge-gap
table (6)
  • --table-row-height-comfortable
  • --table-row-height-compact
  • --table-row-height-dense
  • --table-header-height
  • --table-cell-padding-x
  • --table-border
modal (6)
  • --modal-radius
  • --modal-width-sm
  • --modal-width-md
  • --modal-width-lg
  • --modal-padding
  • --modal-shadow
drawer (3)
  • --drawer-width
  • --drawer-padding
  • --drawer-shadow
sheet (2)
  • --sheet-radius
  • --sheet-max-height
popover (3)
  • --popover-radius
  • --popover-min-width
  • --popover-shadow
tooltip (4)
  • --tooltip-radius
  • --tooltip-max-width
  • --tooltip-bg
  • --tooltip-text
toast (2)
  • --toast-width
  • --toast-radius
check (2)
  • --check-size
  • --check-radius
switch (3)
  • --switch-width
  • --switch-height
  • --switch-thumb
slider (2)
  • --slider-track
  • --slider-thumb
menu-item (2)
  • --menu-item-height
  • --menu-item-radius
tab (1)
  • --tab-height
meter (3)
  • --meter-height
  • --meter-height-lg
  • --meter-radius
progress (1)
  • --progress-height
pip (1)
  • --pip-size
avatar (3)
  • --avatar-size-sm
  • --avatar-size-md
  • --avatar-size-lg
file-row (1)
  • --file-row-icon-size
nav-item (2)
  • --nav-item-height
  • --nav-item-radius

Cascade layers

One stylesheet entry, styles/afara.css, declares the layer order. A later layer always wins regardless of selector specificity, so product code needs no !important.

CSS
@layer afara-reset, afara-base, afara-type, afara-layout,
       afara-components, afara-patterns, afara-utilities;
/* Unlayered product CSS beats every layer above. */

Naming

  • Classes: af- + block, __ element, -- modifier: .af-card__header, .af-button--primary.
  • State comes from native or ARIA attributes and data-*, never .is-* classes: aria-current="page", aria-expanded, [disabled], data-level="warning".
  • Variant-internal custom properties start with an underscore (--_bg) and are private to the component.
  • Only data may be set inline: --value, --threshold, --share. Design values are never inline.

Themes

Light is the default and is applied unless data-theme="dark" is present. Dark is a designed theme: surfaces step through a deep teal ramp, borders are re-balanced, and status colours are re-derived so every pair still meets contrast. The attribute works on any element, so a dark band can sit inside a light page.

Element-scoped themeThe same markup, rendered under each theme.
Theme-aware card

Surface, text, border and status colours all come from semantic tokens.

Warning
Theme-aware card

Surface, text, border and status colours all come from semantic tokens.

Warning
View code
HTML
<div class="af-card"><div class="af-card__body af-stack af-stack--3">
  <span class="t-component-title">Theme-aware card</span>
  <p class="doc-muted">Surface, text, border and status colours all come from semantic tokens.</p>
  <div class="af-cluster"><button class="af-button af-button--primary af-button--sm" type="button">Primary</button><button class="af-button af-button--secondary af-button--sm" type="button">Secondary</button><span class="af-badge af-badge--warning"><span class="af-pip" aria-hidden="true"></span>Warning</span></div>
</div></div>

Foundations

Colour

A restrained palette: deep teal and warm neutrals carry the interface; green, amber, red and blue are reserved for status. Accent colour marks what is actionable or selected, never decoration.

Rules

  • Colour never carries meaning alone. Every status has a distinct icon or pip shape and a word. Every selected state has a fill or rail plus weight.
  • Brand Teal is not a text colour on light surfaces. It is 2.95:1 on white, below the 4.5:1 text minimum. The system derives teal-600 and teal-700 for accent text and uses Brand Teal only for large fills and marks.
  • The decorative border is not a control boundary. The soft neutral border separates panels but is below 3:1, so inputs and checkboxes use --color-border-control (WCAG 1.4.11).
  • Components read semantic tokens. Raw primitives are never used in component CSS.

Primitive palette

Raw values. Generated from tokens/tokens.json.

Brand

  • teal#0BA89E
  • deep#073B37
  • forest#47704C
  • mint#5FD6C8
  • pale-mint#CDEBE7
  • white#FFFFFF

Neutral

  • 0#FFFFFF
  • 25#F7FAF9
  • 50#EEF4F2
  • 100#E4EEEB
  • 150#DCE8E5
  • 200#BCCFCA
  • 300#9DB5B0
  • 400#7F9893
  • 500#688580
  • 550#5A736E
  • 600#526B67
  • 700#3A524E
  • 800#1F403C
  • 900#073B37

Dusk (dark surfaces)

  • 50#F2FAF8
  • 100#CDEBE7
  • 200#B5D8D2
  • 300#9CC5BF
  • 400#5FA9A0
  • 500#2B7A72
  • 600#1B625B
  • 700#0F544E
  • 800#0B4944
  • 900#073B37
  • 950#05302D
  • 975#031F1D

Teal (accent)

  • 50#E4F5F2
  • 100#CDEBE7
  • 200#B3E2DC
  • 300#5FD6C8
  • 400#18BDB2
  • 450#10B5AA
  • 500#0BA89E
  • 600#0A8F86
  • 700#087068
  • 800#06524C

Green (success)

  • 50#EAF4EB
  • 200#B9D6BD
  • 300#8FD39B
  • 600#47704C
  • 800#2E5634
  • dark-bg#0A3A2B
  • dark-border#2F7A58

Amber (warning)

  • 50#FEF4DC
  • 200#F2D08A
  • 300#F2C265
  • 600#B45309
  • 800#7A4100
  • dark-bg#3D3010
  • dark-border#8A6A1F

Red (danger)

  • 50#FDECEA
  • 200#F2B8B2
  • 300#FFAFA6
  • 400#FFC2BB
  • 600#C42B1E
  • 700#B42318
  • 800#A3261B
  • 900#8F1D13
  • 950#3B0E0A
  • dark-bg#45201D
  • dark-border#9A4A42

Blue (information)

  • 50#E8F0FA
  • 200#B8CFEA
  • 300#A3CBF7
  • 600#2563A8
  • 800#1B4F8F
  • dark-bg#12334F
  • dark-border#3C6FA3

Chart (dark-mode series)

  • teal-dark#04A99C
  • amber-dark#CA7F00
  • blue-dark#5C93E3

Semantic colour

These are the tokens you use. The swatches are live: switch theme in the header and they change.

Surfaces

Surfaces colour tokens
TokenSwatchLightDarkRole
--color-canvas#F7FAF9#073B37Page background.
--color-surface#FFFFFF#0B4944Cards, tables, menus, form controls.
--color-surface-raised#FFFFFF#0F544ERaised tactile controls and floating layers (popovers, menus). Separated by border + shadow, not by fill, in light.
--color-surface-inset#EEF4F2#05302DRecessed wells: code blocks, meter tracks, segmented-control tracks, inset panels.
--color-surface-hover#F7FAF9#0F544ERow and list-item hover.
--color-surface-selected#E4F5F2#0F544ESelected row/item background.
--color-surface-inverse#073B37#CDEBE7Tooltips, toasts, code-on-dark panels, production env badge.
--color-surface-disabled#EEF4F2#05302D
--color-scrim#031F1D#000000Modal/drawer backdrop.
Text & links colour tokens
TokenSwatchLightDarkRole
--color-text#073B37#F2FAF8Primary text. brand.md Main Text.
--color-muted-text#526B67#B5D8D2Secondary text. brand.md Muted Text.
--color-subtle-text#5A736E#9CC5BFTertiary text, placeholders, metadata. Meets 4.5:1 on canvas and surface only — not on inset surfaces.
--color-disabled-text#7F9893#5FA9A0Disabled controls (WCAG-exempt, kept legible).
--color-inverse-text#FFFFFF#073B37Text on surface-inverse and primary action.
--color-inverse-muted-text#CDEBE7#3A524ESecondary text on surface-inverse.
--color-link#087068#5FD6C8
--color-link-hover#06524C#CDEBE7
--color-accent-text#087068#5FD6C8Teal used as text or focus ring. ≥4.5:1.

Borders

Borders colour tokens
TokenSwatchLightDarkRole
--color-border#DCE8E5#1B625BDecorative dividers and card outlines. brand.md Border. Not sufficient to identify a control.
--color-border-strong#BCCFCA#2B7A72Secondary button outline, emphasised dividers.
--color-border-control#688580#5FA9A0Boundary of inputs, checkboxes, radios, switches. ≥3:1 on every surface (WCAG 1.4.11).

Accent

Accent colour tokens
TokenSwatchLightDarkRole
--color-accent-solid#0A8F86#5FD6C8Non-text accent: progress fill, selected indicator, switch-on track. ≥3:1 on canvas, surface and inset.
--color-accent-bg#E4F5F2#0F544ESubtle teal tint (info-ish selected backgrounds).
--color-accent-bg-strong#CDEBE7#1B625BActive navigation item, selected chip.

Chart

Chart colour tokens
TokenSwatchLightDarkRole
--color-chart-1#0A8F86#04A99CSeries 1 — teal. Fixed order; assign by entity, never by rank.
--color-chart-2#B45309#CA7F00Series 2 — amber.
--color-chart-3#2563A8#5C93E3Series 3 — blue. Charts carry at most three series; fold the rest into Other.
--color-chart-other#688580#9CC5BFFolded remainder ("Other"). Neutral, never a fourth hue.
--color-chart-grid#DCE8E5#1B625BGridlines and axes: hairline, solid, one step off the surface.

Actions

Actions colour tokens
TokenSwatchLightDarkRole
--action-primary-bg#073B37#CDEBE7
--action-primary-bg-hover#0B4944#E4F5F2
--action-primary-bg-active#031F1D#B5D8D2
--action-primary-text#FFFFFF#073B37
--action-primary-border#031F1D#B5D8D2
--action-secondary-bg#FFFFFF#0F544E
--action-secondary-bg-hover#F7FAF9#1B625B
--action-secondary-bg-active#EEF4F2#0B4944
--action-secondary-text#073B37#F2FAF8
--action-secondary-border#BCCFCA#2B7A72
--action-tertiary-bg#E4F5F2#0B4944
--action-tertiary-bg-hover#CDEBE7#0F544E
--action-tertiary-bg-active#B3E2DC#05302D
--action-tertiary-text#073B37#5FD6C8
--action-ghost-bg-hover#EEF4F2#0F544E
--action-ghost-bg-active#E4EEEB#1B625B
--action-ghost-text#073B37#F2FAF8
--action-destructive-bg#B42318#FFAFA6
--action-destructive-bg-hover#8F1D13#FFC2BB
--action-destructive-bg-active#7A1810#F2B8B2
--action-destructive-text#FFFFFF#3B0E0A
--action-accent-bg#10B5AA#5FD6C8Lightest teal that holds Deep Afara text at ≥4.5:1. Brand Teal itself reaches only 4.2:1.
--action-accent-bg-hover#18BDB2#7FE0D4
--action-accent-bg-active#0FAEA4#4BC2B4
--action-accent-text#073B37#073B37
--action-disabled-bg#EEF4F2#05302D
--action-disabled-text#7F9893#5FA9A0
--action-disabled-border#DCE8E5#1B625B

Status

Status colour tokens
TokenSwatchLightDarkRole
--status-success-bg#EAF4EB#0A3A2B
--status-success-border#B9D6BD#2F7A58
--status-success-text#2E5634#8FD39B
--status-success-solid#47704C#8FD39B
--status-warning-bg#FEF4DC#3D3010
--status-warning-border#F2D08A#8A6A1F
--status-warning-text#7A4100#F2C265
--status-warning-solid#B45309#F2C265
--status-danger-bg#FDECEA#45201D
--status-danger-border#F2B8B2#9A4A42
--status-danger-text#A3261B#FFAFA6
--status-danger-solid#C42B1E#FFAFA6
--status-info-bg#E8F0FA#12334F
--status-info-border#B8CFEA#3C6FA3
--status-info-text#1B4F8F#A3CBF7
--status-info-solid#2563A8#A3CBF7
--status-neutral-bg#EEF4F2#05302D
--status-neutral-border#BCCFCA#2B7A72
--status-neutral-text#3A524E#B5D8D2
--status-neutral-solid#688580#5FA9A0

Environments

Environments colour tokens
TokenSwatchLightDarkRole
--env-production-bg#073B37#CDEBE7
--env-production-text#FFFFFF#073B37
--env-production-border#073B37#CDEBE7
--env-staging-bg#FEF4DC#3D3010
--env-staging-text#7A4100#F2C265
--env-staging-border#B45309#F2C265
--env-development-bg#FFFFFF#0B4944
--env-development-text#3A524E#B5D8D2
--env-development-border#688580#5FA9A0

Focus

Focus colour tokens
TokenSwatchLightDarkRole
--focus-ring-color#087068#5FD6C8≥4.5:1 on white, canvas and pale-mint selection; mint in dark.

Contrast, measured

80 declared pairs, calculated from the resolved token values with the WCAG relative-luminance formula. Every pair meets its minimum in both themes.

Contrast pairs
Foreground on backgroundUseMinLightDark
text on canvasPrimary text on page4.5:111.83:1 ✓11.72:1 ✓
text on surfacePrimary text on cards4.5:112.43:1 ✓9.66:1 ✓
text on surface-raisedPrimary text in menus/popovers4.5:112.43:1 ✓8.26:1 ✓
text on surface-insetPrimary text in wells4.5:111.16:1 ✓13.50:1 ✓
text on surface-selectedSelected rows4.5:111.03:1 ✓8.26:1 ✓
text on accent-bg-strongActive navigation item4.5:19.84:1 ✓6.73:1 ✓
muted-text on canvasSecondary text on page4.5:15.47:1 ✓8.12:1 ✓
muted-text on surfaceSecondary text on cards4.5:15.74:1 ✓6.69:1 ✓
muted-text on surface-raisedSecondary text in menus4.5:15.74:1 ✓5.72:1 ✓
muted-text on surface-insetSecondary text in wells4.5:15.16:1 ✓9.35:1 ✓
muted-text on surface-selectedSecondary text in selected rows4.5:15.10:1 ✓5.72:1 ✓
subtle-text on canvasTertiary text / placeholder on page4.5:14.86:1 ✓6.60:1 ✓
subtle-text on surfaceTertiary text / placeholder on cards4.5:15.11:1 ✓5.44:1 ✓
subtle-text on surface-raisedTertiary text in menus4.5:15.11:1 ✓4.65:1 ✓
link on canvasLinks on page4.5:15.67:1 ✓7.07:1 ✓
link on surfaceLinks on cards4.5:15.95:1 ✓5.82:1 ✓
link on surface-raisedLinks in popovers4.5:15.95:1 ✓4.98:1 ✓
link-hover on surfaceLink hover4.5:19.06:1 ✓8.11:1 ✓
accent-text on surfaceTeal text on cards4.5:15.95:1 ✓5.82:1 ✓
accent-text on accent-bgTeal text on tint4.5:15.28:1 ✓4.98:1 ✓
inverse-text on surface-inverseTooltip / toast text4.5:112.43:1 ✓9.84:1 ✓
inverse-muted-text on surface-inverseSecondary text in toast4.5:19.84:1 ✓6.66:1 ✓
border-control on canvasInput boundary on page3:13.81:1 ✓4.54:1 ✓
border-control on surfaceInput boundary on cards3:14.00:1 ✓3.74:1 ✓
border-control on surface-insetControl boundary in wells3:13.59:1 ✓5.23:1 ✓
accent-solid on canvasProgress fill / selected indicator on page3:13.78:1 ✓7.07:1 ✓
accent-solid on surfaceProgress fill on cards3:13.97:1 ✓5.82:1 ✓
accent-solid on surface-insetProgress fill on meter track3:13.57:1 ✓8.14:1 ✓
focus.ring.color on canvasFocus ring on page3:15.67:1 ✓7.07:1 ✓
focus.ring.color on surfaceFocus ring on cards3:15.95:1 ✓5.82:1 ✓
focus.ring.color on surface-raisedFocus ring in popovers3:15.95:1 ✓4.98:1 ✓
focus.ring.color on surface-selectedFocus ring on selected rows3:15.28:1 ✓4.98:1 ✓
focus.ring.color on accent-bg-strongFocus ring on active nav item3:14.71:1 ✓4.05:1 ✓
chart-1 on surfaceChart series 1 (teal) on card3:13.97:1 ✓3.49:1 ✓
chart-2 on surfaceChart series 2 (amber) on card3:15.02:1 ✓3.19:1 ✓
chart-3 on surfaceChart series 3 (blue) on card3:16.12:1 ✓3.29:1 ✓
action.primary.text on action.primary.bgPrimary button4.5:112.43:1 ✓9.84:1 ✓
action.primary.text on action.primary.bg-hoverPrimary button hover4.5:110.24:1 ✓11.03:1 ✓
action.primary.text on action.primary.bg-activePrimary button active4.5:117.25:1 ✓8.12:1 ✓
action.secondary.text on action.secondary.bgSecondary button4.5:112.43:1 ✓8.26:1 ✓
action.secondary.text on action.secondary.bg-hoverSecondary button hover4.5:111.83:1 ✓6.73:1 ✓
action.secondary.text on action.secondary.bg-activeSecondary button active4.5:111.16:1 ✓9.66:1 ✓
action.tertiary.text on action.tertiary.bgTertiary button4.5:111.03:1 ✓5.82:1 ✓
action.tertiary.text on action.tertiary.bg-hoverTertiary button hover4.5:19.84:1 ✓4.98:1 ✓
action.tertiary.text on action.tertiary.bg-activeTertiary button active4.5:18.78:1 ✓8.14:1 ✓
action.ghost.text on action.ghost.bg-hoverGhost button hover4.5:111.16:1 ✓8.26:1 ✓
action.ghost.text on action.ghost.bg-activeGhost button active4.5:110.49:1 ✓6.73:1 ✓
action.ghost.text on canvasGhost button on page4.5:111.83:1 ✓11.72:1 ✓
action.ghost.text on surfaceGhost button on cards4.5:112.43:1 ✓9.66:1 ✓
action.destructive.text on action.destructive.bgDestructive button4.5:16.57:1 ✓9.52:1 ✓
action.destructive.text on action.destructive.bg-hoverDestructive button hover4.5:18.92:1 ✓10.92:1 ✓
action.destructive.text on action.destructive.bg-activeDestructive button active4.5:110.68:1 ✓9.79:1 ✓
action.accent.text on action.accent.bgAccent button4.5:14.86:1 ✓7.07:1 ✓
action.accent.text on action.accent.bg-hoverAccent button hover4.5:15.30:1 ✓8.00:1 ✓
action.accent.text on action.accent.bg-activeAccent button active4.5:14.50:1 ✓5.73:1 ✓
action.disabled.text on action.disabled.bgDisabled button (WCAG-exempt; informative floor of 2:1)2:12.77:1 ✓5.23:1 ✓
status.success.text on status.success.bgSuccess label on tint4.5:17.46:1 ✓7.23:1 ✓
status.success.text on surfaceSuccess text on card4.5:18.41:1 ✓5.83:1 ✓
status.success.solid on status.success.bgSuccess icon on tint3:15.05:1 ✓7.23:1 ✓
status.success.solid on surfaceSuccess dot on card3:15.69:1 ✓5.83:1 ✓
status.warning.text on status.warning.bgWarning label on tint4.5:17.43:1 ✓7.79:1 ✓
status.warning.text on surfaceWarning text on card4.5:18.13:1 ✓6.18:1 ✓
status.warning.solid on status.warning.bgWarning icon on tint3:14.59:1 ✓7.79:1 ✓
status.warning.solid on surfaceWarning dot on card3:15.02:1 ✓6.18:1 ✓
status.danger.text on status.danger.bgDanger label on tint4.5:16.44:1 ✓8.08:1 ✓
status.danger.text on surfaceDanger text on card / error message4.5:17.37:1 ✓5.82:1 ✓
status.danger.solid on status.danger.bgDanger icon on tint3:14.95:1 ✓8.08:1 ✓
status.danger.solid on surfaceInvalid input border on card3:15.66:1 ✓5.82:1 ✓
status.info.text on status.info.bgInfo label on tint4.5:17.13:1 ✓7.72:1 ✓
status.info.text on surfaceInfo text on card4.5:18.20:1 ✓6.07:1 ✓
status.info.solid on status.info.bgInfo icon on tint3:15.33:1 ✓7.72:1 ✓
status.info.solid on surfaceInfo dot on card3:16.12:1 ✓6.07:1 ✓
status.neutral.text on status.neutral.bgNeutral label on tint4.5:17.55:1 ✓9.35:1 ✓
status.neutral.solid on status.neutral.bgNeutral icon on tint3:13.59:1 ✓5.23:1 ✓
env.production.text on env.production.bgProduction badge4.5:112.43:1 ✓9.84:1 ✓
env.staging.text on env.staging.bgStaging badge4.5:17.43:1 ✓7.79:1 ✓
env.development.text on env.development.bgDevelopment badge4.5:18.41:1 ✓6.69:1 ✓
env.development.border on env.development.bgDevelopment badge dashed border3:14.00:1 ✓3.74:1 ✓
env.staging.border on env.staging.bgStaging badge border3:14.59:1 ✓7.79:1 ✓
chart-other on surfaceChart 'Other' on card3:14.00:1 ✓5.44:1 ✓

Status

Status is shape + word + colour
Healthy Degraded Failing Syncing Paused
View code
HTML
<span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Healthy</span>
<span class="af-badge af-badge--warning"><span class="af-pip" aria-hidden="true"></span>Degraded</span>
<span class="af-badge af-badge--danger"><span class="af-pip" aria-hidden="true"></span>Failing</span>
<span class="af-badge af-badge--info"><span class="af-pip" aria-hidden="true"></span>Syncing</span>
<span class="af-badge"><span class="af-pip" aria-hidden="true"></span>Paused</span>

Foundations

Typography

Outfit sets headings and display text. Inter carries the interface. JetBrains Mono shows anything a developer might copy. All three are self-hosted variable fonts, so the system makes no third-party requests.

Display and headings

Outfit

Geometric, friendly, precise at large sizes. Weight 500–600.

Interface and reading

Inter

Neutral and legible at small sizes, with tabular numerals.

Technical values

JetBrains Mono

Names, IDs, keys, paths, code. Never for decoration.

Type scale

Display and the larger headings are fluid between 360 px and 1280 px viewports, expressed in rem so users' text-size settings still apply.

Type styles (specimens larger than 40px are shown at 40px)
StyleSpecimenFamilySizeWeightLine
displayObject storagedisplay40–72px (fluid)semibold1.02
h1Object storagedisplay34–56px (fluid)semibold1.08
h2Object storagedisplay28–40px (fluid)semibold1.15
h3Object storagedisplay22–28px (fluid)semibold1.25
h4Object storagedisplay18–20px (fluid)semibold1.35
component-titleObject storagebody15pxsemibold1.4
body-lgObject storagebody18–20px (fluid)regular1.6
bodyObject storagebody15–16px (fluid)regular1.6
body-smObject storagebody14pxregular1.5
labelObject storagebody13pxmedium1.4
captionObject storagebody12pxregular1.4
eyebrowObject storagebody12pxsemibold1.4
metadataObject storagemono12pxregular1.5
codeObject storagemono13–14px (fluid)regular1.6
numericObject storagebody14pxmedium1.4
metricObject storagebody28–32px (fluid)semibold1.1

Rules for numbers and technical text

Tabular numerals keep columns aligned
Sample usage by bucket
BucketObjectsStored
acme-assets-prod1,204,331482.10 GB
acme-logs-prod98,5509.07 GB
acme-backups4121,310.00 GB
View code
HTML
<div class="af-table-wrap"><table class="af-table af-table--compact" style="--table-min: 0">
  <caption class="af-visually-hidden">Sample usage by bucket</caption>
  <thead><tr><th scope="col">Bucket</th><th scope="col" class="af-table__num">Objects</th><th scope="col" class="af-table__num">Stored</th></tr></thead>
  <tbody>
    <tr><th scope="row"><code>acme-assets-prod</code></th><td class="af-table__num">1,204,331</td><td class="af-table__num">482.10 GB</td></tr>
    <tr><th scope="row"><code>acme-logs-prod</code></th><td class="af-table__num">98,550</td><td class="af-table__num">9.07 GB</td></tr>
    <tr><th scope="row"><code>acme-backups</code></th><td class="af-table__num">412</td><td class="af-table__num">1,310.00 GB</td></tr>
  </tbody></table></div>
  • Right-align comparable numbers and give them .af-table__num (tabular figures). State the unit and the byte convention (this system formats with decimal SI units: 1 MB = 1,000,000 B).
  • Set resource names, IDs, regions, keys and paths in the mono family so characters such as l, 1, O and 0 are unambiguous.
  • Line length for reading copy is capped at about 68 characters. Headings use sentence case.

Foundations

Spacing and shape

A 4 px scale. Every gap, padding and size is one of these steps. If a design seems to need a value that is not here, the design is wrong or the scale needs a documented addition.

Spacing scale
TokenValueScale
--space-px1px
--space-0-52px
--space-14px
--space-1-56px
--space-28px
--space-312px
--space-416px
--space-520px
--space-624px
--space-832px
--space-1040px
--space-1248px
--space-1664px
--space-2080px
--space-2496px

Using the scale

  • Space between siblings comes from a layout primitive (.af-stack, .af-cluster, .af-grid) and its --gap, not from margins on children.
  • Space inside a component is set by that component's tokens (for example --card-padding).
  • Half steps (--space-0-5, --space-1-5) exist for optical alignment inside controls. Do not use them for layout.
Gap comes from the primitiveaf-stack with --gap steps
gap 8pxaf-stack--2
gap 24pxaf-stack--6
View code
HTML
<div class="af-stack af-stack--2"><span class="af-badge">gap 8px</span><span class="af-badge">af-stack--2</span></div>
<div class="af-stack af-stack--6"><span class="af-badge">gap 24px</span><span class="af-badge">af-stack--6</span></div>

Radius

Small radii for controls, medium for cards and panels. Pills (--radius-full) are for status badges and compact metadata only.

  • --radius-xs4px
  • --radius-none0px
  • --radius-sm6px
  • --radius-md10px
  • --radius-lg16px
  • --radius-xl24px
  • --radius-full9999px

Foundations

Layout

Composition uses a few primitives: container, stack, cluster, grid and split, plus the application shell. Prefer aligned columns and structured sections over scattered floating cards.

Breakpoints

Breakpoints
TokenMin widthMeaning
--breakpoint-sm640px
--breakpoint-md768px
--breakpoint-lg1024px
--breakpoint-xl1280px
--breakpoint-2xl1536px

CSS custom properties cannot be used inside media queries. Write the literal pixel value in @media and keep it equal to this table — npm run validate checks that every literal in the CSS is one of these.

Primitives

Stack, cluster and grid

af-stack: vertical rhythm

One
Two

af-cluster: wrapping row of related items

AlphaBetaGammaDelta

af-grid--auto: as many columns as fit (min 12rem)

Card
Card
Card
View code
HTML
<div class="af-stack af-stack--3">
  <p class="t-label">af-stack: vertical rhythm</p>
  <div class="af-card"><div class="af-card__body">One</div></div>
  <div class="af-card"><div class="af-card__body">Two</div></div>
</div>
<div class="af-stack af-stack--2"><p class="t-label">af-cluster: wrapping row of related items</p>
  <div class="af-cluster af-cluster--2"><span class="af-badge">Alpha</span><span class="af-badge">Beta</span><span class="af-badge">Gamma</span><span class="af-badge">Delta</span></div></div>
<div class="af-stack af-stack--2"><p class="t-label">af-grid--auto: as many columns as fit (min 12rem)</p>
  <div class="af-grid af-grid--auto af-grid--gap-3" style="--min: 12rem">
    <div class="af-card"><div class="af-card__body">Card</div></div><div class="af-card"><div class="af-card__body">Card</div></div><div class="af-card"><div class="af-card__body">Card</div></div>
  </div></div>

Containers

  • .af-container: marketing and documentation content, 1200 px with a fluid gutter.
  • .af-container--app: application content, wider with tighter gutters.
  • .af-container--narrow and .af-prose: reading copy, about 44rem.

Application shell

Sidebar (248 px, 64 px collapsed), a 56 px header, main content and an optional 360 px inspector. Below 1024 px the sidebar becomes an off-canvas drawer opened from the header. This documentation site is itself built on the shell: resize the window to see it.

Application shell layoutA sidebar on the left spanning the full height, a header across the top of the remaining area, and a main region with an optional inspector on the right. sidebar 248header 56maininspector 360

Foundations

Elevation

Depth explains what a surface is. A static card is flat and bordered. A control is raised. A field is recessed. Only things that float above the page (menus, drawers, dialogs, toasts) cast a shadow.

The four treatments on one canvas

Flat: card

Border only

Recessed: field

Raised: control

Floating: popover

Shadow plus border
View code
HTML
<div class="af-stack af-stack--2"><p class="t-label">Flat: card</p><div class="af-card"><div class="af-card__body">Border only</div></div></div>
<div class="af-stack af-stack--2"><p class="t-label">Recessed: field</p><input class="af-input" aria-label="Recessed field example" value="Inset shadow"></div>
<div class="af-stack af-stack--2"><p class="t-label">Raised: control</p><div><button class="af-button af-button--secondary af-button--md" type="button">Key</button></div></div>
<div class="af-stack af-stack--2"><p class="t-label">Floating: popover</p><div class="af-popover" style="position: static; display: block">Shadow plus border</div></div>

Shadow tokens

  • --shadow-raised-controlSecondary and ghost-hover tactile edge: light top highlight + hard 1px bottom edge.
  • --shadow-raised-primaryPrimary/accent/destructive solid buttons.
  • --shadow-pressedActive/pressed state: the key is pushed in.
  • --shadow-inset-controlText inputs, selects, search fields.
  • --shadow-inset-wellMeter tracks, segmented-control tracks, code wells, inset panels.
  • --shadow-card-hoverInteractive card hover only. Static cards have no shadow.
  • --shadow-popoverPopovers, dropdown menus, tooltips, command-palette results.
  • --shadow-drawerSide drawers, bottom sheets.
  • --shadow-modalModal dialogs and command palette.
  • --shadow-toast

Stacking order

Z-index scale
TokenValueUse
--z-base0
--z-sticky100
--z-dropdown200
--z-popover300
--z-scrim400
--z-modal500
--z-toast600
--z-tooltip700

Do

Draw a card's boundary with its 1px border and a tonal change.

Don't

Use a drop shadow in place of a visible border, or float a static card.

Foundations

Motion

Motion clarifies state and relationships. It is quick, small, and never ambient: opacity and short position changes for menus, tabs, drawers and status changes.

Motion tokens
TokenValueUse
--motion-instant80msPressed feedback, tooltip fade.
--motion-fast120msHover, focus, small state changes.
--motion-standard200msMenus, tabs, accordion, toast.
--motion-emphasis280msModals, drawers, sheets.
--ease-standardcubic-bezier(0.2, 0, 0, 1)Default. State changes and movement within the page.
--ease-entercubic-bezier(0.05, 0.7, 0.1, 1)Elements entering: overlays, toasts.
--ease-exitcubic-bezier(0.3, 0, 0.8, 0.15)Elements leaving.

Rules

  • Use --motion-fast for hover, press and small state changes; --motion-standard for menus and tabs; --motion-emphasis for drawers and dialogs.
  • Enter with --ease-enter, leave with --ease-exit. Entering surfaces fade and rise a few pixels; they do not bounce or scale dramatically.
  • The only continuous animations are the loading spinner and the skeleton shimmer, and both stop under reduced motion. No ambient animation anywhere in the console.
  • Under prefers-reduced-motion: reduce every transition and animation collapses to an instant change, and the system supplies a static alternative where motion carried meaning.

Foundations

Icons

One outline family (Lucide, ISC licence), curated to a small functional set. Icons are an inline SVG sprite, so they render without JavaScript and inherit text colour. Select an icon to copy its markup.

Using an icon
View code
HTML
<svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-hard-drive"/></svg>
<button class="af-button af-button--secondary af-button--md" type="button"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-upload"/></svg>Upload</button>
<button class="af-button af-button--secondary af-button--icon af-button--md" type="button" aria-label="Delete object"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-trash-2"/></svg></button>
  • Decorative icons next to text are aria-hidden="true". An icon that is the only content of a control needs an aria-label on the control.
  • Pair an icon with text unless its meaning is universal. Status icons also have distinct shapes (circle-check, triangle-alert, circle-alert, info).
  • Sizes come from --icon-size-xs/sm/md/lg (12, 16, 20, 24 px) with a consistent 1.75 px stroke.

Resources

Files

Actions

Status

Security

Navigation

Data

Developer

Components

Components

Every component here is real: the markup, the CSS and the behaviour are the shipped implementation. Choose from this catalogue before creating anything new.

Choosing a component

Which component for which job
You need to…UseNot
Trigger the main action of a viewPrimary button (one per region)A second primary, or a link styled as a button
Navigate to another pageLink, nav item, breadcrumb, tab-style <nav>A button with an href-like handler
Show a short, optional action listMenu (popover)A modal
Collect one or two values, then continueModalA drawer
Edit or inspect something while keeping contextDrawer or inspectorA full page
Confirm a destructive actionAlert dialog, with typed confirmation if irreversibleA toast with Undo for something that cannot be undone
Tell the user something happenedToast (transient) or alert (persistent)A modal
Show records to compareTableA grid of cards
Show one figureMetric cardA chart
Show usage against a limitUsage meterA plain progress bar
Show progress of a known task<progress> (.af-progress)A spinner

Catalogue

Actions

Buttons in 7 variants and 4 sizes, icon, toggle, loading, split, group, link.

Navigation

Sidebar, top navigation, breadcrumbs, tabs, pagination, switchers, command palette.

Forms

Text, select, combobox, multi-select, date, file, dropzone, check, radio, switch, slider.

Data display

Tables, badges, meters, charts, code, JSON, timelines, cards and surfaces.

Overlays

Modal, alert dialog, drawer, inspector, sheet, popover, menu, tooltip, toast.

Feedback

Alerts, banners, validation, spinner, skeleton, empty, error and offline states.

Infrastructure

20 storage-specific components and 7 patterns: buckets, objects, keys, uploads, usage.

Full API (classes, attributes, events, states and keyboard behaviour for each component) is in COMPONENT_API.md and components/*.md.

Components

Actions

A button is a physical key. Solid variants have a light top edge and a hard lower edge; pressing pushes the key in. Hierarchy comes from fill, border and weight, never from glow or movement.

Playground

Change the variant, size and state; the preview is the real stylesheet and the code below it is exactly what you would paste.

Variants

Seven variants
View code
HTML
<button class="af-button af-button--primary af-button--md" type="button">Primary</button>
<button class="af-button af-button--secondary af-button--md" type="button">Secondary</button>
<button class="af-button af-button--tertiary af-button--md" type="button">Tertiary</button>
<button class="af-button af-button--ghost af-button--md" type="button">Ghost</button>
<button class="af-button af-button--destructive af-button--md" type="button">Destructive</button>
<button class="af-button af-button--accent af-button--md" type="button">Accent</button>
<button class="af-button af-button--link af-button--md" type="button">Link style</button>
When to use each button variant
VariantUse forPer view
PrimaryThe single main action: Create bucket, Save changes, UploadOne per region
SecondaryAlternative or supporting actionsSeveral
TertiaryLow-emphasis tinted actions inside dense surfacesSeveral
GhostToolbar and row actions, icon buttons, dismissMany
DestructiveDelete, revoke, remove. Always confirmed in an alert dialogRare
AccentMarketing and documentation calls to action onlyOne
Link styleAn action that should read as text inside proseOccasional

Sizes

xs 24 · sm 32 · md 40 · lg 48 px
View code
HTML
<button class="af-button af-button--primary af-button--xs" type="button">Extra small</button>
<button class="af-button af-button--primary af-button--sm" type="button">Small</button>
<button class="af-button af-button--primary af-button--md" type="button">Medium</button>
<button class="af-button af-button--primary af-button--lg" type="button">Large</button>

Medium is the default. Use xs and sm in dense tables and toolbars, lg for marketing and empty-state calls to action. Touch targets stay at least 24 × 24 px (WCAG 2.5.8); on touch, prefer md and above.

Interaction states

Hover, active and focus are shown here with the documentation-only data-force attribute, generated from the real :hover, :active and :focus-visible rules. Press Tab and click the live buttons on the right of each row to see the true states.

default
hover
active (pressed in)
keyboard focus
toggled on
loading
disabled

Icon, toggle, loading

Icon buttons need an accessible name
View code
HTML
<button class="af-button af-button--ghost af-button--icon af-button--md" type="button" aria-label="Copy endpoint" data-tooltip="Copy endpoint"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg></button>
<button class="af-button af-button--secondary af-button--icon af-button--md" type="button" aria-label="Refresh" data-tooltip="Refresh"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-refresh-cw"/></svg></button>
<button class="af-button af-button--primary af-button--md" type="button"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-upload"/></svg>Upload</button>
<button class="af-button af-button--secondary af-button--md" type="button">Next<svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-arrow-right"/></svg></button>
Toggle button (aria-pressed)
View code
HTML
<button class="af-button af-button--secondary af-button--md" type="button" data-toggle aria-pressed="false"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-pin"/></svg>Pin bucket</button>
Loading: click it. The button keeps its size and stays focusable, but ignores clicks while busy
View code
HTML
<button class="af-button af-button--primary af-button--md" type="button" data-demo-busy="Saving"><span data-label>Save changes</span></button>

Split button and group

Split button: a primary action plus related actions
View code
HTML
<div class="af-split-button">
  <button class="af-button af-button--primary af-button--md" type="button"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-upload"/></svg>Upload files</button>
  <button class="af-button af-button--primary af-button--md" type="button" popovertarget="act-split-menu" aria-haspopup="menu" aria-expanded="false" aria-label="More upload options"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg></button>
</div>
<div class="af-menu" id="act-split-menu" popover role="menu" aria-label="Upload options" data-placement="bottom-end">
  <button class="af-menu__item" role="menuitem" type="button" data-action="upload-folder"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-folder-plus"/></svg>Upload folder</button>
  <button class="af-menu__item" role="menuitem" type="button" data-action="from-url"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-link"/></svg>Import from URL</button>
</div>
Button group: related actions sharing a boundary
View code
HTML
<div class="af-button-group" role="group" aria-label="View">
  <button class="af-button af-button--secondary af-button--md" type="button" aria-pressed="true"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-list"/></svg>List</button>
  <button class="af-button af-button--secondary af-button--md" type="button" aria-pressed="false" data-toggle><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-columns-3"/></svg>Columns</button>
</div>

Links

Links are always underlined; colour alone never marks a link
View code
HTML
<p>Read the <a class="af-link" href="#accessibility">accessibility notes</a> or open the <a class="af-link" href="https://www.w3.org/TR/WCAG22/" rel="noopener">WCAG 2.2 specification<svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-external-link"/></svg></a>.</p>

Do

Label buttons with the verb and the object: “Create bucket”, “Revoke key”, “Delete 3 objects”.

Use one primary button per region and put it last (or first on small screens) in the action row.

Don't

Write “OK”, “Submit” or “Click here”. Invent a new button style when a variant fits. Use a destructive button without a confirmation step for anything irreversible.

Components

Forms

Every control is a recessed well: you are pressing into a field, not onto one. Labels are always visible, errors say what to do, and validity is never shown by colour alone.

Field anatomy

A field is label, optional hint, control, then message. The hint and the error are linked to the control with aria-describedby.

Text input in its states

Lowercase letters, digits and hyphens. Cannot be changed later.

Use lowercase letters, digits or hyphens only.

Name is available (sample check).

View code
HTML
<div class="af-field"><label class="af-field__label" for="fm-a">Bucket name <span class="af-field__required">required</span></label><input class="af-input" id="fm-a" placeholder="acme-assets" aria-describedby="fm-a-h"><p class="af-field__hint" id="fm-a-h">Lowercase letters, digits and hyphens. Cannot be changed later.</p></div>
<div class="af-field"><label class="af-field__label" for="fm-b">Bucket name <span class="af-field__required">required</span></label><input class="af-input" id="fm-b" value="Acme_Assets" aria-invalid="true" aria-describedby="fm-b-e"><p class="af-field__error" id="fm-b-e"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-alert"/></svg><span>Use lowercase letters, digits or hyphens only.</span></p></div>
<div class="af-field"><label class="af-field__label" for="fm-c">Bucket name <span class="af-field__required">required</span></label><input class="af-input" id="fm-c" value="acme-assets" data-valid="true" aria-describedby="fm-c-s"><p class="af-field__success" id="fm-c-s"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-check"/></svg><span>Name is available (sample check).</span></p></div>
<div class="af-field"><label class="af-field__label" for="fm-d">Region <span class="af-field__optional">optional</span></label><input class="af-input" id="fm-d" value="Set at creation" disabled></div>
<div class="af-field"><label class="af-field__label" for="fm-e">Bucket ID</label><input class="af-input af-input--mono" id="fm-e" value="bkt_SAMPLE_0001" readonly></div>
Sizes: sm 32 · md 40 · lg 48 px
View code
HTML
<input class="af-input af-input--sm" aria-label="Small input" placeholder="Small">
<input class="af-input" aria-label="Medium input" placeholder="Medium (default)">
<input class="af-input af-input--lg" aria-label="Large input" placeholder="Large">

Text area and select

Textarea with live character counter, and native select
0 / 200
View code
HTML
<div class="af-field"><label class="af-field__label" for="fm-ta">Description <span class="af-field__optional">optional</span></label><textarea class="af-textarea" id="fm-ta" rows="3" maxlength="200" data-counter="#fm-ta-c" aria-describedby="fm-ta-c"></textarea><span class="af-field__counter" id="fm-ta-c">0 / 200</span></div>
<div class="af-field"><label class="af-field__label" for="fm-sel">Storage class</label><div class="af-select"><select class="af-select__control" id="fm-sel"><option>Standard</option><option>Infrequent access</option><option>Archive</option></select><svg class="af-icon af-select__icon" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg></div></div>

Selects are the native element, so the platform supplies keyboard behaviour and mobile pickers. Reach for a combobox only when the list is long enough to need filtering.

Choice controls

Checkbox, radio, switch
Bucket features
Access
View code
HTML
<fieldset class="af-fieldset"><legend class="af-fieldset__legend">Bucket features</legend>
  <label class="af-check"><input class="af-check__input" type="checkbox" checked><span><span class="af-check__label">Versioning</span><span class="af-check__desc">Keep earlier versions of overwritten objects.</span></span></label>
  <label class="af-check"><input class="af-check__input" type="checkbox"><span><span class="af-check__label">Object lock</span><span class="af-check__desc">Block deletes for a retention period.</span></span></label>
  <label class="af-check"><input class="af-check__input" type="checkbox" disabled><span><span class="af-check__label">Replication</span><span class="af-check__desc">Unavailable in this sample.</span></span></label>
</fieldset>
<fieldset class="af-fieldset"><legend class="af-fieldset__legend">Access</legend>
  <label class="af-check"><input class="af-check__input af-radio__input" type="radio" name="fm-acc" checked><span class="af-check__label">Private</span></label>
  <label class="af-check"><input class="af-check__input af-radio__input" type="radio" name="fm-acc"><span class="af-check__label">Signed URLs only</span></label>
  <label class="af-check"><input class="af-check__input af-radio__input" type="radio" name="fm-acc"><span class="af-check__label">Public read</span></label>
</fieldset>
<div class="af-stack">
  <label class="af-switch-field"><input class="af-switch" type="checkbox" role="switch" checked><span class="af-switch-field__text">Request logging</span></label>
  <label class="af-switch-field"><input class="af-switch" type="checkbox" role="switch"><span class="af-switch-field__text">Public access</span></label>
  <label class="af-switch-field"><input class="af-switch" type="checkbox" role="switch" disabled><span class="af-switch-field__text">Disabled switch</span></label>
</div>

A switch applies immediately and takes effect on its own. A checkbox waits for the form to be submitted. Do not use a switch inside a form that has a Save button.

Slider, number and segmented control

Range with visible value, number stepper, segmented control
30 days
Time range
View code
HTML
<div class="af-field"><label class="af-field__label" for="fm-rng">Retention</label><input class="af-slider" type="range" id="fm-rng" min="1" max="90" value="30" data-output="#fm-rng-o" data-unit=" days" aria-describedby="fm-rng-o"><output id="fm-rng-o" class="af-field__hint">30 days</output></div>
<div class="af-field"><label class="af-field__label" for="fm-num">Max keys</label><div class="af-number">
  <button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-number-step="-1" data-number-target="#fm-num" aria-label="Decrease max keys"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-minus"/></svg></button>
  <input class="af-input" id="fm-num" type="number" inputmode="numeric" value="5" min="1" max="10">
  <button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-number-step="1" data-number-target="#fm-num" aria-label="Increase max keys"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-plus"/></svg></button></div></div>
<fieldset class="af-fieldset"><legend class="af-fieldset__legend">Time range</legend><div class="af-segmented">
  <span class="af-segmented__option"><input class="af-segmented__input" type="radio" name="fm-seg" id="fm-s1" checked><label class="af-segmented__label" for="fm-s1">24 h</label></span>
  <span class="af-segmented__option"><input class="af-segmented__input" type="radio" name="fm-seg" id="fm-s2"><label class="af-segmented__label" for="fm-s2">7 d</label></span>
  <span class="af-segmented__option"><input class="af-segmented__input" type="radio" name="fm-seg" id="fm-s3"><label class="af-segmented__label" for="fm-s3">30 d</label></span></div></fieldset>

Search, input group, copyable value and secret

Search with shortcut, prefix and suffix addons, copy, and secret reveal
https://

Re-masks after 30 seconds. Never revealed automatically.

View code
HTML
<div class="af-search"><svg class="af-icon af-search__icon" aria-hidden="true" focusable="false"><use href="#i-search"/></svg><input class="af-input" type="search" placeholder="Search objects" aria-label="Search objects" data-search-shortcut><kbd class="af-search__kbd">/</kbd></div>
<div class="af-field"><label class="af-field__label" for="fm-ig">Custom domain</label><div class="af-input-group"><span class="af-input-group__addon af-input-group__addon--mono">https://</span><input class="af-input af-input--mono" id="fm-ig" value="assets.example.test"></div></div>
<div class="af-field"><label class="af-field__label" for="fm-cp">Endpoint (sample value)</label><div class="af-input-group af-copyable"><input class="af-input" id="fm-cp" value="https://storage.example.test/v1" readonly><button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-copy-target="#fm-cp" data-copy-label="Endpoint" aria-label="Copy endpoint"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg></button></div></div>
<div class="af-field"><label class="af-field__label" for="fm-sc">Secret key (sample value)</label><div class="af-input-group af-secret"><input class="af-input" id="fm-sc" type="password" value="sample-secret-0000-0000" readonly autocomplete="off"><button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-secret-toggle="#fm-sc" aria-pressed="false" aria-label="Show secret"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-eye"/></svg></button></div><p class="af-field__hint">Re-masks after 30 seconds. Never revealed automatically.</p></div>

Combobox and multi-select

A combobox follows the ARIA 1.2 pattern: a text input that owns a listbox, with focus staying in the input and aria-activedescendant pointing at the active option.

Filterable combobox and chip multi-select (sample options)
View code
HTML
<div class="af-field"><label class="af-field__label" for="fm-cb">Region (sample)</label>
  <div class="af-combobox" data-combobox>
    <input class="af-input af-combobox__input" id="fm-cb" role="combobox" aria-expanded="false" aria-controls="fm-cb-l" aria-autocomplete="list" autocomplete="off" placeholder="Search regions">
    <button type="button" class="af-button af-button--ghost af-button--icon af-button--sm af-combobox__toggle" tabindex="-1" aria-label="Show regions"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg></button>
    <ul class="af-listbox" id="fm-cb-l" role="listbox" aria-label="Regions" hidden>
      <li class="af-option" role="option" data-value="region-a">Sample region A</li><li class="af-option" role="option" data-value="region-b">Sample region B</li><li class="af-option" role="option" data-value="region-c">Sample region C</li><li class="af-option" role="option" data-value="region-d">Sample region D</li>
    </ul></div></div>
<div class="af-field"><label class="af-field__label" id="fm-ms-l" for="fm-ms">Labels</label>
  <div class="af-multiselect" data-multiselect data-name="labels">
    <div class="af-multiselect__control"><input class="af-multiselect__input" id="fm-ms" role="combobox" aria-expanded="false" aria-controls="fm-ms-list" aria-autocomplete="list" autocomplete="off" placeholder="Add labels"></div>
    <ul class="af-listbox" id="fm-ms-list" role="listbox" aria-labelledby="fm-ms-l" aria-multiselectable="true" hidden>
      <li class="af-option" role="option" data-value="web">web</li><li class="af-option" role="option" data-value="backup">backup</li><li class="af-option" role="option" data-value="logs">logs</li><li class="af-option" role="option" data-value="media">media</li>
    </ul></div></div>

Date picker

Typed ISO date, or a keyboard-operable calendar grid

Format YYYY-MM-DD. Arrow keys move by day and week, Page Up and Page Down by month.

View code
HTML
<div class="af-field"><label class="af-field__label" for="fm-dp">Expiry date</label>
  <div class="af-datepicker" data-datepicker data-min="2026-01-01" data-max="2027-12-31">
    <div class="af-input-group"><input class="af-input" id="fm-dp" inputmode="numeric" placeholder="YYYY-MM-DD" autocomplete="off" aria-describedby="fm-dp-h"><button type="button" class="af-button af-button--secondary af-button--icon af-button--md" data-datepicker-toggle aria-label="Choose date"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-calendar"/></svg></button></div>
  </div><p class="af-field__hint" id="fm-dp-h">Format YYYY-MM-DD. Arrow keys move by day and week, Page Up and Page Down by month.</p></div>

Upload: dropzone and queue

The dropzone is a labelled file input first, so it works without drag and drop. The queue below shows each file’s state in text and icon. The transport here is simulated: nothing leaves your browser, and a filename containing “fail” fails once so you can try Retry.

Dropzone and upload queue (simulated transfer)

Uploads

    No uploads yet.

    Uploads

      No uploads yet.

      View code
      HTML
      <div class="af-dropzone" data-dropzone data-queue="#fm-q" data-accept=".txt,.png,.zip,.json" data-max-size="5000000">
        <input class="af-dropzone__input" id="fm-dz" type="file" multiple>
        <label class="af-dropzone__label" for="fm-dz"><svg class="af-icon af-dropzone__icon" aria-hidden="true" focusable="false"><use href="#i-cloud-upload"/></svg><span class="af-dropzone__title">Drop files or <span class="af-dropzone__action">browse</span></span><span class="af-dropzone__hint">.txt, .png, .zip or .json up to 5 MB. Simulated upload.</span></label>
        <div data-dropzone-message role="status" hidden></div>
      </div>
      <section class="af-queue" id="fm-q" data-upload-queue aria-label="Upload queue">
        <div class="af-queue__head"><h4 class="af-queue__title">Uploads</h4><span data-queue-summary role="status"></span>
          <span class="af-cluster"><button class="af-button af-button--ghost af-button--sm" type="button" data-queue-cancel-all>Cancel all</button><button class="af-button af-button--ghost af-button--sm" type="button" data-queue-clear>Clear finished</button></span></div>
        <ul class="af-queue__list"></ul><p class="af-queue__empty">No uploads yet.</p>
      </section>

      Validation and the error summary

      Validate on submit, then on blur once a field has been touched. On failure, an error summary receives focus and lists every problem as a link to its field; each field also shows its own message. Native constraints supply the rules, and data-error-* attributes supply the wording.

      Submit empty to see the summary

      3 to 63 characters. Lowercase letters, digits and hyphens.

      3 to 63 characters. Lowercase letters, digits and hyphens.

      View code
      HTML
      <form class="af-form" data-validate novalidate data-demo-submit="Saved (sample, nothing was sent)">
        <div class="af-error-summary" data-error-summary hidden tabindex="-1"><h4 class="af-error-summary__title">There is a problem</h4><ul></ul></div>
        <div class="af-field"><label class="af-field__label" for="fv-name">Bucket name <span class="af-field__required">required</span></label><input class="af-input" id="fv-name" name="name" required pattern="[a-z0-9\-]{3,63}" aria-describedby="fv-name-h" data-error-value-missing="Enter a bucket name." data-error-pattern-mismatch="Use 3 to 63 lowercase letters, digits or hyphens."><p class="af-field__hint" id="fv-name-h">3 to 63 characters. Lowercase letters, digits and hyphens.</p></div>
        <div class="af-field"><label class="af-field__label" for="fv-mail">Alert email <span class="af-field__required">required</span></label><input class="af-input" id="fv-mail" name="mail" type="email" required data-error-value-missing="Enter an email address." data-error-type-mismatch="Enter an email address like name@example.test."></div>
        <label class="af-check"><input class="af-check__input" type="checkbox" required data-error-value-missing="Confirm that you understand."><span class="af-check__label">I understand the bucket name cannot be changed</span></label>
        <div class="af-form__actions"><button class="af-button af-button--secondary af-button--md" type="reset">Reset</button><button class="af-button af-button--primary af-button--md" type="submit">Create bucket</button></div>
      </form>

      Do

      • Keep a visible label on every control. Placeholder text is a hint, never a label.
      • Write errors as instructions: “Use 3 to 63 lowercase letters”, not “Invalid input”.
      • Mark optional rather than required when most fields are required, and be consistent within a form.
      • Put one primary action at the end of the form and keep Cancel or Reset secondary.

      Don’t

      • Signal an error with a red border alone. The icon and the message text are required.
      • Disable the submit button to explain what is missing. Let people submit and tell them.
      • Clear what someone typed when validation fails.
      • Build a custom select when the native one will do.

      Components

      Cards and data display

      Data is the product, so these components are quiet and exact. Numbers are tabular, identifiers are monospaced and never truncated where a person may need to quote them, and status always has a shape and a word.

      Surfaces and cards

      There is one card. It is flat, hairline-bordered and square-cornered to the system radius. Variants change the role, not the decoration. Reach for --elevated only for something that floats, never to make a card look nicer.

      Card, inset, dashed, selected and interactive

      Default card

      Flat with a hairline border.

      Use for a self-contained group of content.

      Inset card

      Recessed, for content that sits inside another surface.

      Dashed card

      An empty slot waiting to be filled, such as “Add a webhook”.

      Selected card

      A filled tint and an accent border mark the choice, together with a visible “Selected” label.

      Selected

      Interactive card

      The whole card is one link. Nested buttons stay independently focusable.

      View code
      HTML
      <div class="af-card"><div class="af-card__header"><div><h4 class="af-card__title">Default card</h4><p class="af-card__subtitle">Flat with a hairline border.</p></div></div><div class="af-card__body"><p>Use for a self-contained group of content.</p></div><div class="af-card__footer"><button class="af-button af-button--secondary af-button--sm" type="button">Action</button></div></div>
      <div class="af-card af-card--inset"><div class="af-card__body"><h4 class="af-card__title">Inset card</h4><p>Recessed, for content that sits inside another surface.</p></div></div>
      <div class="af-card af-card--dashed"><div class="af-card__body"><h4 class="af-card__title">Dashed card</h4><p>An empty slot waiting to be filled, such as “Add a webhook”.</p></div></div>
      <div class="af-card af-card--selected"><div class="af-card__body"><h4 class="af-card__title">Selected card</h4><p>A filled tint and an accent border mark the choice, together with a visible “Selected” label.</p><span class="af-badge af-badge--accent">Selected</span></div></div>
      <div class="af-card af-card--interactive"><div class="af-card__body"><h4 class="af-card__title"><a class="af-card__link" href="#data-display">Interactive card</a></h4><p>The whole card is one link. Nested buttons stay independently focusable.</p></div></div>

      Metric and status cards

      Metric card (sample figures) and status cards
      Objects stored1,204,331Up 4.2% compared with the previous period

      Sample figure. Previous period: 1,155,800.

      Stored62.4GBDown 1.1 GB

      Sample figure. Decimal units: 1 GB = 1,000,000,000 B.

      Replication healthy

      All sample objects are in sync.

      Sample status, not live

      Approaching quota

      Storage is above the 80% warning line.

      Sample status, not live

      View code
      HTML
      <div class="af-card"><div class="af-card__body"><div class="af-metric"><span class="af-metric__label">Objects stored</span><span class="af-metric__value">1,204,331</span><span class="af-metric__delta" data-trend="up"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-chart-line"/></svg>Up 4.2% <span class="af-visually-hidden">compared with the previous period</span></span><p class="af-metric__helper">Sample figure. Previous period: 1,155,800.</p></div></div></div>
      <div class="af-card"><div class="af-card__body"><div class="af-metric"><span class="af-metric__label">Stored</span><span class="af-metric__value">62.4<span class="af-metric__unit">GB</span></span><span class="af-metric__delta" data-trend="down"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-chart-line"/></svg>Down 1.1 GB</span><p class="af-metric__helper">Sample figure. Decimal units: 1 GB = 1,000,000,000 B.</p></div></div></div>
      <div class="af-status-card" data-status="success"><svg class="af-icon af-status-card__icon" aria-hidden="true" focusable="false"><use href="#i-circle-check"/></svg><div><h4 class="af-status-card__title">Replication healthy</h4><p class="af-status-card__text">All sample objects are in sync.</p><p class="af-status-card__meta">Sample status, not live</p></div></div>
      <div class="af-status-card" data-status="warning"><svg class="af-icon af-status-card__icon" aria-hidden="true" focusable="false"><use href="#i-triangle-alert"/></svg><div><h4 class="af-status-card__title">Approaching quota</h4><p class="af-status-card__text">Storage is above the 80% warning line.</p><p class="af-status-card__meta">Sample status, not live</p></div><div class="af-status-card__actions"><button class="af-button af-button--secondary af-button--sm" type="button">Review usage</button></div></div>

      Panels, collapsibles, empty states

      Info panel, collapsible section, empty state

      Keys are shown once

      Copy the secret when you create the key. You cannot view it again.

      Advanced settings3 options

      Details live here. The disclosure is a native <details> element.

      No buckets yet

      A bucket holds your objects. Create one to start uploading.

      Read the guide
      View code
      HTML
      <div class="af-panel af-panel--info"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg><div><p class="af-panel__title">Keys are shown once</p><p>Copy the secret when you create the key. You cannot view it again.</p></div></div>
      <details class="af-collapsible"><summary>Advanced settings<span class="af-collapsible__hint">3 options</span></summary><div class="af-collapsible__body"><p>Details live here. The disclosure is a native <code>&lt;details&gt;</code> element.</p></div></details>
      <div class="af-card af-card--dashed"><div class="af-empty"><span class="af-empty__icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-database"/></svg></span><h4 class="af-empty__title">No buckets yet</h4><p class="af-empty__text">A bucket holds your objects. Create one to start uploading.</p><div class="af-empty__actions"><button class="af-button af-button--primary af-button--md" type="button">Create bucket</button><a class="af-button af-button--ghost af-button--md" href="#data-display">Read the guide</a></div></div></div>

      Table

      Tables are the workhorse. Selection, sorting, sticky headers and a stacked small-screen layout are all built in. Sorting state is exposed as aria-sort, and a bulk bar appears when rows are selected.

      Selectable, sortable table (sample objects)
      Sample objects. Sortable by name, size and modified date.
      Status
      backup.zip48.2 MB2026-10-30Stored
      logo.png18.4 kB2026-11-02Stored
      report.json950 B2026-11-04Processing
      Sample objects. Sortable by name, size and modified date.
      Status
      backup.zip48.2 MB2026-10-30Stored
      logo.png18.4 kB2026-11-02Stored
      report.json950 B2026-11-04Processing
      View code
      HTML
      <div class="af-table-bulkbar" data-bulkbar hidden role="region" aria-label="Bulk actions"><span data-bulk-count role="status"></span><span class="af-cluster af-cluster--2"><button class="af-button af-button--secondary af-button--sm" type="button">Download</button><button class="af-button af-button--destructive af-button--sm" type="button">Delete</button></span></div>
      <div class="af-table-wrap" role="region" aria-label="Sample objects" tabindex="0">
        <table class="af-table" data-table-select>
          <caption class="af-visually-hidden">Sample objects. Sortable by name, size and modified date.</caption>
          <thead><tr>
            <th scope="col" class="af-table__select"><input class="af-check__input" type="checkbox" data-select-all aria-label="Select all rows"></th>
            <th scope="col" aria-sort="ascending"><button class="af-table__sort" type="button">Name</button></th>
            <th scope="col" aria-sort="none" class="af-table__num"><button class="af-table__sort" type="button">Size</button></th>
            <th scope="col" aria-sort="none"><button class="af-table__sort" type="button">Modified</button></th>
            <th scope="col">Status</th></tr></thead>
          <tbody>
            <tr><td class="af-table__select"><input class="af-check__input" type="checkbox" data-select-row aria-label="Select backup.zip"></td><th scope="row" class="af-table__name">backup.zip</th><td class="af-table__num" data-sort="48200000">48.2 MB</td><td data-sort="2026-10-30">2026-10-30</td><td><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Stored</span></td></tr>
            <tr><td class="af-table__select"><input class="af-check__input" type="checkbox" data-select-row aria-label="Select logo.png"></td><th scope="row" class="af-table__name">logo.png</th><td class="af-table__num" data-sort="18400">18.4 kB</td><td data-sort="2026-11-02">2026-11-02</td><td><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Stored</span></td></tr>
            <tr><td class="af-table__select"><input class="af-check__input" type="checkbox" data-select-row aria-label="Select report.json"></td><th scope="row" class="af-table__name">report.json</th><td class="af-table__num" data-sort="950">950 B</td><td data-sort="2026-11-04">2026-11-04</td><td><span class="af-badge af-badge--warning"><span class="af-pip" aria-hidden="true"></span>Processing</span></td></tr>
          </tbody>
        </table>
      </div>

      Badges, status pips, environment, chips, avatars

      Each status has its own shape: ● success, ▲ warning, ◆ danger, ○ info, ▬ neutral, ◐ pending. Colour reinforces; it never carries the meaning alone. Environments differ by fill and border style as well as label.

      Status badges, pips, environment badges
      Healthy Degraded Failed Syncing Inactive Outline v1.0.0 Pending Production Staging Development
      View code
      HTML
      <span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Healthy</span>
      <span class="af-badge af-badge--warning"><span class="af-pip" aria-hidden="true"></span>Degraded</span>
      <span class="af-badge af-badge--danger"><span class="af-pip" aria-hidden="true"></span>Failed</span>
      <span class="af-badge af-badge--info"><span class="af-pip" aria-hidden="true"></span>Syncing</span>
      <span class="af-badge"><span class="af-pip" aria-hidden="true"></span>Inactive</span>
      <span class="af-badge af-badge--outline">Outline</span>
      <span class="af-badge af-badge--mono">v1.0.0</span>
      <span class="af-cluster af-cluster--1"><span class="af-pip" data-status="pending" aria-hidden="true"></span>Pending</span>
      <span class="af-env" data-env="production"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-server"/></svg>Production</span>
      <span class="af-env" data-env="staging"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-layers"/></svg>Staging</span>
      <span class="af-env" data-env="development"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-flask-conical"/></svg>Development</span>
      Chips and avatars
      backup read-only
      View code
      HTML
      <span class="af-chip">backup<button class="af-chip__remove" type="button" aria-label="Remove backup"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></span>
      <span class="af-chip af-chip--static">read-only</span>
      <span class="af-avatar af-avatar--sm" aria-hidden="true">AK</span><span class="af-avatar" aria-hidden="true">AK</span><span class="af-avatar af-avatar--lg" aria-hidden="true">AK</span>

      Lists, key-value and metadata

      List, key-value pairs and a metadata panel
      • acme-assets-prodCreated 2026-09-12 (sample)
        Active
      • acme-logsCreated 2026-10-01 (sample)
        Inactive
      Storage class
      Standard
      Versioning
      Enabled
      Bucket ID
      bkt_SAMPLE_0001
      Content type
      image/png
      Size
      18,400 B (18.4 kB)
      ETag
      sample-etag-0000
      View code
      HTML
      <ul class="af-list">
        <li class="af-list__item"><svg class="af-icon af-list__lead" aria-hidden="true" focusable="false"><use href="#i-database"/></svg><div class="af-list__main"><span class="af-list__title">acme-assets-prod</span><span class="af-list__meta">Created 2026-09-12 (sample)</span></div><div class="af-list__trail"><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Active</span></div></li>
        <li class="af-list__item"><svg class="af-icon af-list__lead" aria-hidden="true" focusable="false"><use href="#i-database"/></svg><div class="af-list__main"><span class="af-list__title">acme-logs</span><span class="af-list__meta">Created 2026-10-01 (sample)</span></div><div class="af-list__trail"><span class="af-badge"><span class="af-pip" aria-hidden="true"></span>Inactive</span></div></li>
      </ul>
      <dl class="af-kv af-kv--ruled">
        <div class="af-kv__item"><dt class="af-kv__term">Storage class</dt><dd class="af-kv__value">Standard</dd></div>
        <div class="af-kv__item"><dt class="af-kv__term">Versioning</dt><dd class="af-kv__value">Enabled</dd></div>
        <div class="af-kv__item"><dt class="af-kv__term">Bucket ID</dt><dd class="af-kv__value af-kv__mono">bkt_SAMPLE_0001</dd></div>
      </dl>
      <dl class="af-meta">
        <div class="af-meta__row"><dt>Content type</dt><dd>image/png</dd></div>
        <div class="af-meta__row"><dt>Size</dt><dd>18,400 B (18.4 kB)</dd></div>
        <div class="af-meta__row"><dt>ETag</dt><dd>sample-etag-0000</dd></div>
      </dl>

      Progress and usage meter

      A progress bar is a task moving to completion. A meter is a quantity against a limit; it has a threshold marker, ruler ticks, and a word for its level. An estimate is hatched so it cannot be mistaken for a measurement.

      Progress (native element)
      View code
      HTML
      <div class="af-stack af-stack--2"><label for="dd-p1">Uploading 3 of 8 files, 42%</label><progress class="af-progress" id="dd-p1" max="100" value="42"></progress></div>
      <div class="af-stack af-stack--2"><label for="dd-p2">Processing</label><progress class="af-progress" id="dd-p2" max="100"></progress></div>
      <div class="af-stack af-stack--2"><label for="dd-p3">Import complete</label><progress class="af-progress" id="dd-p3" max="100" value="100" data-status="success"></progress></div>
      Meter at three levels (sample values)
      Storage used42 GB of 100 GB
      Within limitWarns at 80%
      Storage used86 GB of 100 GB
      Approaching limitWarns at 80%
      Egress this month104 GB of 100 GB
      Over limit (estimate)Warns at 80%
      View code
      HTML
      <div class="af-meter" data-level="ok"><div class="af-meter__head"><span class="af-meter__label" id="dd-m1">Storage used</span><span class="af-meter__figures"><strong>42 GB</strong> of 100 GB</span></div><div class="af-meter__track" role="meter" aria-labelledby="dd-m1" aria-valuemin="0" aria-valuemax="100" aria-valuenow="42" aria-valuetext="42 GB of 100 GB used, 42%"><div class="af-meter__fill" style="--value: 42%"></div><span class="af-meter__threshold" style="--threshold: 80%"></span></div><div class="af-meter__foot"><span class="af-meter__state"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-check"/></svg>Within limit</span><span>Warns at 80%</span></div></div>
      <div class="af-meter" data-level="warning"><div class="af-meter__head"><span class="af-meter__label" id="dd-m2">Storage used</span><span class="af-meter__figures"><strong>86 GB</strong> of 100 GB</span></div><div class="af-meter__track" role="meter" aria-labelledby="dd-m2" aria-valuemin="0" aria-valuemax="100" aria-valuenow="86" aria-valuetext="86 GB of 100 GB used, 86%"><div class="af-meter__fill" style="--value: 86%"></div><span class="af-meter__threshold" style="--threshold: 80%"></span></div><div class="af-meter__foot"><span class="af-meter__state"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-triangle-alert"/></svg>Approaching limit</span><span>Warns at 80%</span></div></div>
      <div class="af-meter" data-level="danger" data-kind="estimate"><div class="af-meter__head"><span class="af-meter__label" id="dd-m3">Egress this month</span><span class="af-meter__figures"><strong>104 GB</strong> of 100 GB</span></div><div class="af-meter__track" role="meter" aria-labelledby="dd-m3" aria-valuemin="0" aria-valuemax="100" aria-valuenow="100" aria-valuetext="104 GB of 100 GB, over the limit, estimated"><div class="af-meter__fill" style="--value: 100%"></div><span class="af-meter__threshold" style="--threshold: 80%"></span></div><div class="af-meter__foot"><span class="af-meter__state"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-octagon-alert"/></svg>Over limit (estimate)</span><span>Warns at 80%</span></div></div>

      Usage breakdown and legend

      Parts of one whole: colour and texture and a legend with values
      • Standard 62.4 GB
      • Infrequent 18.1 GB
      • Archive 9.5 GB
      View code
      HTML
      <div class="af-breakdown" role="img" aria-label="Storage by class: Standard 62.4 GB, Infrequent 18.1 GB, Archive 9.5 GB"><span class="af-breakdown__seg" data-series="1" style="--share: 62.4"></span><span class="af-breakdown__seg" data-series="2" style="--share: 18.1"></span><span class="af-breakdown__seg" data-series="3" style="--share: 9.5"></span></div>
      <ul class="af-legend"><li class="af-legend__item"><span class="af-legend__swatch" data-series="1"></span>Standard <span class="af-legend__value">62.4 GB</span></li><li class="af-legend__item"><span class="af-legend__swatch" data-series="2"></span>Infrequent <span class="af-legend__value">18.1 GB</span></li><li class="af-legend__item"><span class="af-legend__swatch" data-series="3"></span>Archive <span class="af-legend__value">9.5 GB</span></li></ul>

      Charts

      Charts are built from a data spec. Lines are 2 px, bars are at most 24 px wide with a rounded data end, grid lines are hairlines, and series are told apart by colour and by texture or marker. Every chart has a data table twin (“Show data”), keyboard arrows to step through points, and a tooltip on hover. Three series is the ceiling; the rest become “Other”.

      Line chart (sample data)
      View code
      HTML
      <figure data-chart='{"type":"line","title":"Requests per hour","subtitle":"Last 8 hours. Sample data","categories":["00:00","01:00","02:00","03:00","04:00","05:00","06:00","07:00"],"series":[{"name":"Reads","values":[120,180,150,220,260,240,300,280]},{"name":"Writes","values":[40,60,55,90,80,110,95,120]}],"format":"number"}'></figure>
      Bar chart with texture (sample data)
      View code
      HTML
      <figure data-chart='{"type":"bar","title":"Stored by class","subtitle":"Sample data","categories":["Mon","Tue","Wed","Thu"],"series":[{"name":"Standard","values":[1200000,2200000,1800000,2600000]},{"name":"Archive","values":[400000,600000,900000,700000]}],"format":"bytes","texture":true}'></figure>

      Code and JSON

      Code block with copy and a collapsible JSON viewer
      shell
      curl -X PUT https://storage.example.test/v1/buckets/acme-assets \
      -H "Authorization: Bearer $AFARA_SAMPLE_TOKEN"
      View code
      HTML
      <div class="af-code"><div class="af-code__head"><span class="af-code__lang">shell</span><button class="af-button af-button--ghost af-button--sm" type="button" data-copy-target="#dd-code" data-copy-label="Command"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg>Copy</button></div><pre tabindex="0"><code id="dd-code" data-lang="shell">curl -X PUT https://storage.example.test/v1/buckets/acme-assets \
      -H "Authorization: Bearer $AFARA_SAMPLE_TOKEN"</code></pre></div>
      <div class="af-json" data-json='{"bucket":"acme-assets","versioning":true,"lifecycle":{"expire_days":90,"classes":["standard","archive"]},"note":"Sample data"}' data-json-open="2"></div>

      Timeline and event log

      Timeline of a resource, and a dense event log (sample events)
      1. Bucket created

        By a sample user

      2. Versioning enabled

      3. Quota warning

      • OKPutObjectacme-assets/logo.png
      • WARNGetObjectacme-assets/old.zip
      View code
      HTML
      <ol class="af-timeline">
        <li class="af-timeline__item"><span class="af-timeline__marker"><span class="af-pip" data-status="success" aria-hidden="true"></span></span><div><p class="af-timeline__title">Bucket created</p><p class="af-timeline__detail">By a sample user</p><time class="af-timeline__time" datetime="2026-09-12">2026-09-12</time></div></li>
        <li class="af-timeline__item"><span class="af-timeline__marker"><span class="af-pip" data-status="info" aria-hidden="true"></span></span><div><p class="af-timeline__title">Versioning enabled</p><time class="af-timeline__time" datetime="2026-09-14">2026-09-14</time></div></li>
        <li class="af-timeline__item"><span class="af-timeline__marker"><span class="af-pip" data-status="warning" aria-hidden="true"></span></span><div><p class="af-timeline__title">Quota warning</p><time class="af-timeline__time" datetime="2026-10-30">2026-10-30</time></div></li>
      </ol>
      <ul class="af-log">
        <li class="af-log__row"><time class="af-log__time" datetime="2026-11-05T09:14:02Z">09:14:02</time><span class="af-log__level"><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>OK</span></span><span class="af-log__msg"><span class="af-log__op">PutObject</span><span class="af-log__resource">acme-assets/logo.png</span></span></li>
        <li class="af-log__row"><time class="af-log__time" datetime="2026-11-05T09:14:09Z">09:14:09</time><span class="af-log__level"><span class="af-badge af-badge--warning"><span class="af-pip" aria-hidden="true"></span>WARN</span></span><span class="af-log__msg"><span class="af-log__op">GetObject</span><span class="af-log__resource">acme-assets/old.zip</span></span></li>
      </ul>

      Do

      • Right-align numeric columns and use tabular figures so digits line up.
      • State units and the base for bytes (decimal here: 1 MB = 1,000,000 B).
      • Give every chart a title, a unit, and a data table alternative.
      • Label sample or estimated figures as such, in text.

      Don’t

      • Use a coloured dot with no shape and no label as a status.
      • Truncate resource IDs, request IDs or error codes.
      • Invent statistics, uptime or operational status. If you do not have the data, say so.
      • Add a shadow to a card instead of a border.

      Components

      Overlays

      Overlays sit above the page and take responsibility for focus. They are built on native <dialog> and the popover attribute, so focus trapping, Escape and top-layer stacking come from the platform. Choose the lightest one that does the job.

      Choosing an overlay
      NeedUseBlocks the page
      Explain a control or show a shortcutTooltipNo
      A small amount of extra content or controlsPopoverNo
      A list of actionsMenuNo
      A short task or formModalYes
      A destructive or irreversible actionAlert dialogYes
      Editing or details beside a listDrawerYes
      Inspecting one object while browsingInspectorNot at 1280 px and wider
      Mobile menu or short taskBottom sheetYes
      Confirm that something finishedToastNo

      Tooltip

      Supplementary only: never the sole place a label or instruction lives. Appears on hover and keyboard focus, dismisses with Escape.

      Tooltip with an optional shortcut
      View code
      HTML
      <button class="af-button af-button--secondary af-button--icon af-button--md" type="button" aria-label="Refresh" data-tooltip="Refresh"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-refresh-cw"/></svg></button>
      <button class="af-button af-button--secondary af-button--md" type="button" data-tooltip="Open search" data-tooltip-kbd="Ctrl K">Search</button>

      Popover

      Non-modal popover: Escape or outside click closes, focus returns

      acme-assets-prod

      Sample popover. Keep content short; link to a page for more.

      View code
      HTML
      <button class="af-button af-button--secondary af-button--md" type="button" popovertarget="ov-pop" aria-expanded="false">Bucket details</button>
      <div class="af-popover" id="ov-pop" popover data-placement="bottom-start"><p class="af-popover__title">acme-assets-prod</p><p>Sample popover. Keep content short; link to a page for more.</p></div>

      Menu and context menu

      Action menu with shortcut and destructive item; right-click or Shift+F10 on the row for the context menu
      logo.png (sample row). Right-click here.
      View code
      HTML
      <div class="af-cluster">
        <button class="af-button af-button--secondary af-button--md" type="button" popovertarget="ov-menu" aria-haspopup="menu" aria-expanded="false">Actions<svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg></button>
        <div class="af-menu" id="ov-menu" popover role="menu" aria-label="Object actions" data-placement="bottom-start">
          <button class="af-menu__item" role="menuitem" type="button" data-action="rename">Rename<span class="af-menu__shortcut">F2</span></button>
          <button class="af-menu__item" role="menuitem" type="button" data-action="copy">Copy link</button>
          <hr class="af-menu__sep" role="separator">
          <button class="af-menu__item" role="menuitem" type="button" aria-disabled="true">Restore version</button>
          <button class="af-menu__item af-menu__item--danger" role="menuitem" type="button" data-action="delete"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-trash-2"/></svg>Delete</button>
        </div>
      </div>
      <div class="af-card af-card--inset" data-context-menu="#ov-ctx" tabindex="0" aria-label="logo.png. Press Shift+F10 for actions"><div class="af-card__body">logo.png (sample row). Right-click here.</div></div>
      <div class="af-menu" id="ov-ctx" popover role="menu" aria-label="logo.png actions">
        <button class="af-menu__item" role="menuitem" type="button">Download</button>
        <button class="af-menu__item" role="menuitem" type="button">Copy link</button>
        <button class="af-menu__item af-menu__item--danger" role="menuitem" type="button">Delete</button>
      </div>

      Modal

      Modal dialog: focus moves in, Tab is trapped, Escape and the backdrop close it

      Create bucket

      Sample form. Nothing is created.

      View code
      HTML
      <button class="af-button af-button--primary af-button--md" type="button" data-dialog-open="#ov-modal">Create bucket</button>
      <dialog class="af-modal" id="ov-modal" aria-labelledby="ov-modal-t">
        <form method="dialog" class="af-modal__panel">
          <div class="af-modal__header"><div><h4 class="af-modal__title" id="ov-modal-t">Create bucket</h4><p class="af-modal__description">Sample form. Nothing is created.</p></div><button class="af-button af-button--ghost af-button--icon af-button--sm" type="button" data-dialog-close aria-label="Close"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></div>
          <div class="af-modal__body"><div class="af-field"><label class="af-field__label" for="ov-m-name">Bucket name</label><input class="af-input" id="ov-m-name" placeholder="acme-assets" autocomplete="off"></div></div>
          <div class="af-modal__footer"><button class="af-button af-button--secondary af-button--md" type="button" data-dialog-close>Cancel</button><button class="af-button af-button--primary af-button--md" type="button" data-dialog-close="save" data-toast="success" data-toast-title="Bucket created (sample)">Create</button></div>
        </form>
      </dialog>

      Alert dialog with typed confirmation

      For irreversible actions. It states the consequence, lists what will be affected, and asks for the resource name. The destructive button stays disabled until the name matches. Clicking the backdrop does not dismiss it, and Cancel has initial focus. Escape still closes it, as a Cancel.

      Delete confirmation (sample, nothing is deleted)

      Delete acme-assets-prod?

      This permanently removes the bucket. It cannot be undone.

      • 1,204 objects (sample)
      • All versions and metadata
      View code
      HTML
      <button class="af-button af-button--destructive af-button--md" type="button" data-dialog-open="#ov-alert">Delete bucket…</button>
      <dialog class="af-modal af-modal--sm" id="ov-alert" role="alertdialog" aria-labelledby="ov-alert-t" aria-describedby="ov-alert-d">
        <form method="dialog" class="af-modal__panel">
          <div class="af-alertdialog__head"><span class="af-alertdialog__icon" aria-hidden="true"><svg class="af-icon" focusable="false"><use href="#i-triangle-alert"/></svg></span><div><h4 class="af-modal__title" id="ov-alert-t">Delete acme-assets-prod?</h4><p class="af-modal__description" id="ov-alert-d">This permanently removes the bucket. It cannot be undone.</p>
            <ul class="af-confirm-list"><li>1,204 objects (sample)</li><li>All versions and metadata</li></ul></div></div>
          <div class="af-modal__body"><div class="af-field"><label class="af-field__label" for="ov-a-in">Type <span class="af-confirm-input__name">acme-assets-prod</span> to confirm</label><input class="af-input" id="ov-a-in" data-confirm-input="acme-assets-prod" autocomplete="off"></div></div>
          <div class="af-modal__footer"><button class="af-button af-button--secondary af-button--md" type="button" data-dialog-close autofocus>Cancel</button><button class="af-button af-button--destructive af-button--md" type="button" data-confirm-button="delete" disabled data-dialog-close="delete" data-toast="danger" data-toast-title="Deleted (sample)">Delete bucket</button></div>
        </form>
      </dialog>

      Drawer, inspector and bottom sheet

      Side drawer, object inspector, and bottom sheet

      Edit bucket

      logo.png

      Type
      image/png
      Size
      18,400 B
      Modified
      2026-11-02 (sample)

      Object actions

      View code
      HTML
      <button class="af-button af-button--secondary af-button--md" type="button" data-dialog-open="#ov-drawer">Open drawer</button>
      <button class="af-button af-button--secondary af-button--md" type="button" data-dialog-open="#ov-insp">Open inspector</button>
      <button class="af-button af-button--secondary af-button--md" type="button" data-dialog-open="#ov-sheet">Open bottom sheet</button>
      <dialog class="af-drawer" id="ov-drawer" aria-labelledby="ov-drawer-t">
        <form method="dialog" class="af-modal__panel">
          <div class="af-modal__header"><h4 class="af-modal__title" id="ov-drawer-t">Edit bucket</h4><button class="af-button af-button--ghost af-button--icon af-button--sm" type="button" data-dialog-close aria-label="Close"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></div>
          <div class="af-modal__body af-stack"><div class="af-field"><label class="af-field__label" for="ov-d-n">Display name</label><input class="af-input" id="ov-d-n" value="acme-assets-prod"></div><label class="af-switch-field"><input class="af-switch" type="checkbox" role="switch" checked><span class="af-switch-field__text">Versioning</span></label></div>
          <div class="af-modal__footer"><button class="af-button af-button--secondary af-button--md" type="button" data-dialog-close>Cancel</button><button class="af-button af-button--primary af-button--md" type="button" data-dialog-close="save">Save changes</button></div>
        </form>
      </dialog>
      <dialog class="af-drawer af-drawer--inspector" id="ov-insp" aria-labelledby="ov-insp-t">
        <div class="af-modal__panel">
          <div class="af-modal__header"><h4 class="af-modal__title" id="ov-insp-t">logo.png</h4><button class="af-button af-button--ghost af-button--icon af-button--sm" type="button" data-dialog-close aria-label="Close inspector"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></div>
          <div class="af-modal__body"><dl class="af-meta"><div class="af-meta__row"><dt>Type</dt><dd>image/png</dd></div><div class="af-meta__row"><dt>Size</dt><dd>18,400 B</dd></div><div class="af-meta__row"><dt>Modified</dt><dd>2026-11-02 (sample)</dd></div></dl></div>
        </div>
      </dialog>
      <dialog class="af-sheet" id="ov-sheet" aria-labelledby="ov-sheet-t">
        <div class="af-modal__panel"><span class="af-sheet__handle" aria-hidden="true"></span>
          <div class="af-modal__header"><h4 class="af-modal__title" id="ov-sheet-t">Object actions</h4><button class="af-button af-button--ghost af-button--icon af-button--sm" type="button" data-dialog-close aria-label="Close"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></div>
          <div class="af-modal__body"><div class="af-actionsheet"><button class="af-menu__item" type="button" data-dialog-close>Download</button><button class="af-menu__item" type="button" data-dialog-close>Copy link</button><button class="af-menu__item af-menu__item--danger" type="button" data-dialog-close>Delete</button></div></div>
        </div>
      </dialog>

      The inspector is non-modal at 1280 px and wider, so the list behind it stays usable; below that it behaves as a drawer. A menu can become a bottom sheet on small screens with data-menu-sheet.

      Toast

      For confirmation of something that has already happened. Success and info toasts announce politely; danger toasts announce assertively. Toasts never carry the only route to an action, and they wait until an open modal closes.

      Toast triggers
      View code
      HTML
      <button class="af-button af-button--secondary af-button--md" type="button" data-toast="success" data-toast-title="Settings saved" data-toast-text="Sample notification.">Success</button>
      <button class="af-button af-button--secondary af-button--md" type="button" data-toast="warning" data-toast-title="Quota at 86%" data-toast-text="Sample notification.">Warning</button>
      <button class="af-button af-button--secondary af-button--md" type="button" data-toast="danger" data-toast-title="Upload failed" data-toast-text="ERR_CONNECTION_RESET (sample)">Danger</button>

      Command palette

      Global search and commands
      View code
      HTML
      <button class="af-button af-button--secondary af-button--md" type="button" data-dialog-open="#cmd"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-command"/></svg>Open command palette<kbd>Ctrl K</kbd></button>

      Do

      • Give every dialog a title and a visible close or Cancel control.
      • Return focus to the control that opened the overlay.
      • Name the consequence and the resource in destructive confirmations.

      Don’t

      • Stack modals. Replace the content or use a page instead.
      • Put essential information only in a tooltip or toast.
      • Use a modal for something a popover can do.

      Components

      Feedback and system states

      Say what happened, what it means, and what to do next. Every tone pairs an icon and a word with its colour, and no state is ever shown as colour alone.

      Alerts

      Five tones: info, success, warning, danger, neutral

      Keys are shown once

      Copy the secret now. You cannot view it again.

      Bucket created

      You can start uploading objects.

      Approaching your storage limit

      Sample usage is above the 80% warning line.

      Read-only for 5 minutes

      Compact alert for inline use inside a form or card.

      View code
      HTML
      <div class="af-alert" role="status"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg><div class="af-alert__body"><p class="af-alert__title">Keys are shown once</p><p class="af-alert__text">Copy the secret now. You cannot view it again.</p></div></div>
      <div class="af-alert af-alert--success" role="status"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-circle-check"/></svg><div class="af-alert__body"><p class="af-alert__title">Bucket created</p><p class="af-alert__text">You can start uploading objects.</p></div></div>
      <div class="af-alert af-alert--warning" role="status"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-triangle-alert"/></svg><div class="af-alert__body"><p class="af-alert__title">Approaching your storage limit</p><p class="af-alert__text">Sample usage is above the 80% warning line.</p></div><div class="af-alert__actions"><button class="af-button af-button--secondary af-button--sm" type="button">Review usage</button></div></div>
      <div class="af-alert af-alert--danger" role="alert"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-circle-x"/></svg><div class="af-alert__body"><p class="af-alert__title">Upload failed</p><p class="af-alert__text">The connection was reset. Error <code>ERR_CONNECTION_RESET</code>. Try again.</p></div></div>
      <div class="af-alert af-alert--neutral"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg><div class="af-alert__body"><p class="af-alert__title">Read-only for 5 minutes</p></div></div>
      <div class="af-alert af-alert--compact" role="status"><svg class="af-icon af-alert__icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg><div class="af-alert__body"><p class="af-alert__text">Compact alert for inline use inside a form or card.</p></div><button type="button" class="af-button af-button--ghost af-button--icon af-button--sm af-alert__dismiss" aria-label="Dismiss alert"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-x"/></svg></button></div>

      Use role="alert" only for something that appears after the page has loaded and needs immediate attention. Static messages need no role; most dynamic ones use role="status".

      Banner

      Page-level banners (sample messages)
      A new console layout is available. Sample announcement.Read more
      Scheduled maintenance is shown here when there is some. Sample text.
      View code
      HTML
      <div class="af-banner" role="status"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg><span>A new console layout is available. Sample announcement.</span><a href="#feedback">Read more</a></div>
      <div class="af-banner af-banner--warning" role="status"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-triangle-alert"/></svg><span>Scheduled maintenance is shown here when there is some. Sample text.</span></div>

      Error summary

      Used at the top of a form after a failed submit. See Forms for the live version: it takes focus, and each item links to its field.

      Error summary (static example)
      View code
      HTML
      <div class="af-error-summary" role="alert" tabindex="-1"><h4 class="af-error-summary__title">There is a problem</h4><ul><li><a href="#feedback">Enter a bucket name.</a></li><li><a href="#feedback">Enter an email address.</a></li></ul></div>

      Loading: spinner, progress, skeleton

      Know the fraction? Use a progress bar. Know the shape of the content? Use a skeleton. Neither? Use a spinner with a sentence that says what is happening. The skeleton shimmer is the only continuous animation in the system, and it stops under reduced motion.

      Spinner with text, and a skeleton in a busy region
      Verifying bucket name…SmallLarge
      Loading bucket details
      View code
      HTML
      <div class="af-stack"><span class="af-loading"><span class="af-spinner" aria-hidden="true"></span>Verifying bucket name…</span><span class="af-loading"><span class="af-spinner af-spinner--sm" aria-hidden="true"></span>Small</span><span class="af-loading"><span class="af-spinner af-spinner--lg" aria-hidden="true"></span>Large</span></div>
      <div class="af-card" aria-busy="true"><div class="af-card__body af-stack"><span class="af-visually-hidden">Loading bucket details</span><span class="af-skeleton af-skeleton--title"></span><span class="af-skeleton af-skeleton--text"></span><span class="af-skeleton af-skeleton--text"></span><span class="af-skeleton af-skeleton--block"></span></div></div>

      System states

      For whole views that cannot show their content. Each says what happened, whether it is your fault or ours, and what to do. The code is selectable so it can be quoted to support.

      Error, no-permission and not-found states (sample)
      ERR_REQUEST_FAILED

      We could not load your buckets

      This is a problem on our side. Your data is not affected.

      403

      You do not have access

      Ask an owner of this project to grant you the Viewer role.

      404

      Bucket not found

      It may have been deleted, or the address is wrong.

      View code
      HTML
      <div class="af-state" data-status="danger"><span class="af-state__icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-x"/></svg></span><span class="af-state__code">ERR_REQUEST_FAILED</span><h4 class="af-state__title">We could not load your buckets</h4><p class="af-state__text">This is a problem on our side. Your data is not affected.</p><div class="af-state__actions"><button class="af-button af-button--primary af-button--md" type="button">Try again</button><a class="af-button af-button--ghost af-button--md" href="#feedback">Contact support</a></div></div>
      <div class="af-state" data-status="warning"><span class="af-state__icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-lock"/></svg></span><span class="af-state__code">403</span><h4 class="af-state__title">You do not have access</h4><p class="af-state__text">Ask an owner of this project to grant you the Viewer role.</p></div>
      <div class="af-state" data-status="info"><span class="af-state__icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-search"/></svg></span><span class="af-state__code">404</span><h4 class="af-state__title">Bucket not found</h4><p class="af-state__text">It may have been deleted, or the address is wrong.</p><div class="af-state__actions"><a class="af-button af-button--secondary af-button--md" href="#feedback">Back to buckets</a></div></div>

      Connection indicator

      Live stream state: text and icon, not just colour
      Live Reconnecting… Offline
      View code
      HTML
      <span class="af-connection" data-state="online"><span class="af-pip" data-status="success" aria-hidden="true"></span>Live</span>
      <span class="af-connection" data-state="reconnecting"><span class="af-pip" data-status="warning" aria-hidden="true"></span>Reconnecting…</span>
      <span class="af-connection" data-state="offline"><span class="af-pip" data-status="danger" aria-hidden="true"></span>Offline</span>

      Do

      • Lead with the outcome, then the cause, then the next step.
      • Show an error code when one exists and make it copyable.
      • Keep empty states helpful: say what is missing and offer the action.

      Don’t

      • Blame the user, or say “Something went wrong” with nothing else.
      • Show a spinner for more than a moment without saying what is happening.
      • Invent a status. If the real state is unknown, say “Status unavailable”.

      Components and patterns

      Infrastructure patterns

      Twenty components that describe cloud storage as it actually is: a hierarchy of resources, each with a type, a name, a status and an environment. They are composed from the primitives above and add no new colours, radii, shadows or spacing.

      Every value on this page is sample data

      Names, sizes, regions, keys, timestamps and statuses are invented to show the components. Afara has no live service behind this documentation. In a real product, never invent infrastructure statistics, uptime or operational status.

      Sample-data flag

      Required on any card, chart or table that shows invented figures
      Sample data
      View code
      HTML
      <span class="af-sample"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-flask-conical"/></svg>Sample data</span>

      Resource path and resource card

      Where am I? The path is always visible above a resource view: Organization, Project, Environment, Bucket, Object. Each level shows its type as well as its name, and the current level is marked in text weight and a rule, not colour.

      Resource path (current level: bucket)
      View code
      HTML
      <nav aria-label="Resource path"><ol class="af-resource-path">
        <li class="af-resource-path__item" data-level="organization"><span class="af-resource-path__type">Organization</span><a class="af-resource-path__name" href="#infrastructure">Acme (sample)</a></li>
        <li class="af-resource-path__item" data-level="project"><span class="af-resource-path__type">Project</span><a class="af-resource-path__name" href="#infrastructure">web-app</a></li>
        <li class="af-resource-path__item" data-level="environment"><span class="af-resource-path__type">Environment</span><span class="af-env" data-env="production"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-server"/></svg>Production</span></li>
        <li class="af-resource-path__item" data-level="bucket" aria-current="location"><span class="af-resource-path__type">Bucket</span><a class="af-resource-path__name" href="#infrastructure" aria-current="location">acme-assets-prod</a></li>
      </ol></nav>
      Resource card in a list

      acme-assets-prod

      Bucket · Sample region A

      Objects
      1,204
      Stored
      62.4 GB
      Environment
      Production
      Status
      Active
      Sample data

      acme-assets-staging

      Bucket · Sample region B

      Objects
      212
      Stored
      3.1 GB
      Environment
      Staging
      Status
      Provisioning
      Sample data
      View code
      HTML
      <article class="af-card af-card--interactive"><div class="af-card__body">
        <div class="af-resource-card__head"><span class="af-resource-card__tile"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-database"/></svg></span><div><h4 class="af-resource-card__name"><a class="af-card__link" href="#infrastructure">acme-assets-prod</a></h4><p class="af-resource-card__type">Bucket · Sample region A</p></div></div>
        <dl class="af-resource-card__facts"><div><dt>Objects</dt><dd>1,204</dd></div><div><dt>Stored</dt><dd>62.4 GB</dd></div><div><dt>Environment</dt><dd><span class="af-env" data-env="production">Production</span></dd></div><div><dt>Status</dt><dd><span class="af-status"><span class="af-pip" data-status="success" aria-hidden="true"></span>Active</span></dd></div></dl>
        <div><span class="af-sample">Sample data</span></div>
      </div></article>
      <article class="af-card af-card--interactive"><div class="af-card__body">
        <div class="af-resource-card__head"><span class="af-resource-card__tile"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-database"/></svg></span><div><h4 class="af-resource-card__name"><a class="af-card__link" href="#infrastructure">acme-assets-staging</a></h4><p class="af-resource-card__type">Bucket · Sample region B</p></div></div>
        <dl class="af-resource-card__facts"><div><dt>Objects</dt><dd>212</dd></div><div><dt>Stored</dt><dd>3.1 GB</dd></div><div><dt>Environment</dt><dd><span class="af-env" data-env="staging">Staging</span></dd></div><div><dt>Status</dt><dd><span class="af-status"><span class="af-pip" data-status="pending" aria-hidden="true"></span>Provisioning</span></dd></div></dl>
        <div><span class="af-sample">Sample data</span></div>
      </div></article>

      Resource status

      A closed set. Do not invent statuses. Each has its own shape and a word; “since” is shown when known.

      active · provisioning · degraded · failed · suspended · deleting
      Active since 2026-09-12 Provisioning Degraded Failed Suspended Deleting
      View code
      HTML
      <span class="af-status"><span class="af-pip" data-status="success" aria-hidden="true"></span>Active <span class="af-status__since">since 2026-09-12</span></span>
      <span class="af-status"><span class="af-pip" data-status="pending" aria-hidden="true"></span>Provisioning</span>
      <span class="af-status"><span class="af-pip" data-status="warning" aria-hidden="true"></span>Degraded</span>
      <span class="af-status"><span class="af-pip" data-status="danger" aria-hidden="true"></span>Failed</span>
      <span class="af-status"><span class="af-pip" data-status="neutral" aria-hidden="true"></span>Suspended</span>
      <span class="af-status"><span class="af-pip" data-status="info" aria-hidden="true"></span>Deleting</span>
      Status panel with checks (sample, not live)
      DegradedSample data
      • Reads
      • Writes (elevated latency)
      View code
      HTML
      <div class="af-status-panel"><div class="af-status-panel__head"><span class="af-status"><span class="af-pip" data-status="warning" aria-hidden="true"></span>Degraded</span><span class="af-sample">Sample data</span></div>
      <ul class="af-status-panel__checks"><li><span class="af-pip" data-status="success" aria-hidden="true"></span>Reads<time datetime="2026-11-05T09:00:00Z">09:00 UTC</time></li><li><span class="af-pip" data-status="warning" aria-hidden="true"></span>Writes (elevated latency)<time datetime="2026-11-05T09:00:00Z">09:00 UTC</time></li></ul></div>

      Bucket selector and region selector

      Bucket selector: the switcher, specialised with name, region and visibility
      • acme-assets-prodSample region APrivate
      • acme-assets-stagingSample region BPrivate
      • acme-public-siteSample region APublic read
      View code
      HTML
      <div data-switcher="bucket">
        <button type="button" class="af-switcher" popovertarget="in-bsel" aria-haspopup="listbox" aria-expanded="false"><span class="af-switcher__tile">A</span><span class="af-switcher__text"><span class="af-switcher__type">Bucket</span><span class="af-switcher__name">acme-assets-prod</span></span><svg class="af-icon af-switcher__chevron" aria-hidden="true" focusable="false"><use href="#i-chevrons-up-down"/></svg></button>
        <div class="af-popover af-popover--flush af-switcher__panel" id="in-bsel" popover data-placement="bottom-start">
          <div class="af-switcher__search"><input class="af-input af-input--sm" data-switcher-filter aria-label="Filter buckets" placeholder="Filter buckets" autocomplete="off"></div>
          <ul class="af-switcher__list" role="listbox" aria-label="Buckets">
            <li class="af-option" role="option" aria-selected="true" tabindex="0" data-value="acme-assets-prod" data-initial="A" data-label="acme-assets-prod"><span class="af-bucket-option"><span class="af-bucket-option__name">acme-assets-prod</span><span class="af-bucket-option__meta"><span>Sample region A</span><span>Private</span></span></span></li>
            <li class="af-option" role="option" aria-selected="false" tabindex="-1" data-value="acme-assets-staging" data-initial="A" data-label="acme-assets-staging"><span class="af-bucket-option"><span class="af-bucket-option__name">acme-assets-staging</span><span class="af-bucket-option__meta"><span>Sample region B</span><span>Private</span></span></span></li>
            <li class="af-option" role="option" aria-selected="false" tabindex="-1" data-value="acme-public-site" data-initial="A" data-label="acme-public-site"><span class="af-bucket-option"><span class="af-bucket-option__name">acme-public-site</span><span class="af-bucket-option__meta"><span>Sample region A</span><span>Public read</span></span></span></li>
          </ul>
          <p class="af-listbox__empty" data-switcher-empty hidden>No buckets match.</p>
        </div>
      </div>

      A region that is not available yet is shown disabled with the reason, never hidden and never selectable. Selection is shown by border, fill and the native checked state.

      Region selector (sample regions)
      Region
      View code
      HTML
      <fieldset class="af-choice-group"><legend class="af-fieldset__legend">Region</legend>
        <label class="af-choice"><input class="af-choice__input af-check__input af-radio__input" type="radio" name="in-reg" checked><span class="af-choice__title">Sample region A</span><span class="af-choice__code">sample-a-1</span><span class="af-choice__desc">Lowest latency for sample users.</span></label>
        <label class="af-choice"><input class="af-choice__input af-check__input af-radio__input" type="radio" name="in-reg"><span class="af-choice__title">Sample region B</span><span class="af-choice__code">sample-b-1</span><span class="af-choice__desc">A second location for sample redundancy.</span></label>
        <label class="af-choice"><input class="af-choice__input af-check__input af-radio__input" type="radio" name="in-reg" disabled><span class="af-choice__title">Sample region C</span><span class="af-choice__code">sample-c-1</span><span class="af-choice__desc">Not available yet.</span><span class="af-choice__foot"><span class="af-badge af-badge--outline">Planned</span></span></label>
      </fieldset>

      Object explorer, file row and file metadata

      The explorer is a toolbar, a path, a table and a footer. Folders sort before objects. Access (private or public) is visible in every row, and names wrap so an extension is never cut off.

      Object explorer (sample objects)
      Objects in images (sample)
      NameSizeModifiedAccess
      2026/——Private
      logo.png18.4 kB2026-11-02Public
      backup-2026-10.zip48.2 MB2026-10-30Private
      3 items (sample)
      View code
      HTML
      <div class="af-explorer">
        <div class="af-explorer__toolbar"><div class="af-search"><svg class="af-icon af-search__icon" aria-hidden="true" focusable="false"><use href="#i-search"/></svg><input class="af-input" type="search" placeholder="Search in this folder" aria-label="Search in this folder"></div><button class="af-button af-button--primary af-button--md" type="button" data-toast="info" data-toast-title="Sample: the upload dialog would open here"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-upload"/></svg>Upload</button></div>
        <nav class="af-explorer__path" aria-label="Object path"><ol class="af-breadcrumbs af-breadcrumbs--mono"><li><a href="#infrastructure">acme-assets-prod</a></li><li><a href="#infrastructure" aria-current="location">images</a></li></ol></nav>
        <div class="af-table-wrap" role="region" aria-label="Objects in images" tabindex="0"><table class="af-table">
          <caption class="af-visually-hidden">Objects in images (sample)</caption>
          <thead><tr><th scope="col">Name</th><th scope="col" class="af-table__num">Size</th><th scope="col">Modified</th><th scope="col">Access</th></tr></thead>
          <tbody>
            <tr><th scope="row"><span class="af-cluster af-cluster--3"><span class="af-file-icon" data-type="folder"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-folder"/></svg></span><span class="af-file-row__name">2026/</span></span></th><td class="af-table__num">—</td><td>—</td><td><span class="af-badge af-access" data-access="private">Private</span></td></tr>
            <tr><th scope="row"><span class="af-cluster af-cluster--3"><span class="af-file-icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file-image"/></svg></span><span class="af-file-row__name">logo.png</span></span></th><td class="af-table__num">18.4 kB</td><td>2026-11-02</td><td><span class="af-badge af-access" data-access="public"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-globe"/></svg>Public</span></td></tr>
            <tr><th scope="row"><span class="af-cluster af-cluster--3"><span class="af-file-icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file-archive"/></svg></span><span class="af-file-row__name">backup-2026-10.zip</span></span></th><td class="af-table__num">48.2 MB</td><td>2026-10-30</td><td><span class="af-badge af-access" data-access="private">Private</span></td></tr>
          </tbody></table></div>
        <div class="af-table-foot"><span>3 items (sample)</span></div>
      </div>
      File metadata panel (inspector content)

      logo.png

      Public
      Metadata
      Content type
      image/png
      Size
      18,400 B
      Modified
      2026-11-02 (sample)
      Public URL (sample)
      View code
      HTML
      <div class="af-file-meta">
        <div class="af-file-meta__preview"><span class="af-file-icon af-file-icon--lg"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file-image"/></svg></span></div>
        <div><h4 class="af-file-meta__name">logo.png</h4><span class="af-badge af-access" data-access="public"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-globe"/></svg>Public</span></div>
        <section class="af-file-meta__section"><h5 class="af-file-meta__heading">Metadata</h5><dl class="af-meta"><div class="af-meta__row"><dt>Content type</dt><dd>image/png</dd></div><div class="af-meta__row"><dt>Size</dt><dd>18,400 B</dd></div><div class="af-meta__row"><dt>Modified</dt><dd>2026-11-02 (sample)</dd></div></dl></section>
        <section class="af-file-meta__section"><h5 class="af-file-meta__heading">Public URL (sample)</h5><div class="af-input-group af-copyable"><input class="af-input" id="in-url" value="https://assets.example.test/logo.png" readonly aria-label="Public URL"><button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-copy-target="#in-url" data-copy-label="URL" aria-label="Copy URL"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg></button></div></section>
      </div>

      Storage capacity and infrastructure metric

      Capacity card: used and limit share a unit; the estimate says how it is computed

      Storage capacity

      Sample data
      Stored86 GB of 100 GB
      Approaching limitWarns at 80%
      Used
      86 GB
      Remaining
      14 GB
      Plan limit
      100 GB

      Estimate: projected from the last 7 days of sample growth. Decimal units (1 GB = 1,000,000,000 B).

      Requests, last 24 hours48,210

      Total of GET and PUT. Sample data.

      As of 2026-11-05 09:00 UTC (sample)
      View code
      HTML
      <div class="af-card"><div class="af-card__header"><h4 class="af-card__title">Storage capacity</h4><span class="af-sample">Sample data</span></div><div class="af-card__body"><div class="af-capacity">
        <div class="af-meter" data-level="warning"><div class="af-meter__head"><span class="af-meter__label" id="in-cap">Stored</span><span class="af-meter__figures"><strong>86 GB</strong> of 100 GB</span></div><div class="af-meter__track" role="meter" aria-labelledby="in-cap" aria-valuemin="0" aria-valuemax="100" aria-valuenow="86" aria-valuetext="86 GB of 100 GB used, 86%"><div class="af-meter__fill" style="--value: 86%"></div><span class="af-meter__threshold" style="--threshold: 80%"></span></div><div class="af-meter__foot"><span class="af-meter__state"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-triangle-alert"/></svg>Approaching limit</span><span>Warns at 80%</span></div></div>
        <dl class="af-capacity__stats"><div><dt>Used</dt><dd>86 GB</dd></div><div><dt>Remaining</dt><dd>14 GB</dd></div><div><dt>Plan limit</dt><dd>100 GB</dd></div></dl>
        <p class="af-capacity__note"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-info"/></svg>Estimate: projected from the last 7 days of sample growth. Decimal units (1 GB = 1,000,000,000 B).</p>
      </div></div></div>
      <div class="af-card"><div class="af-card__body"><div class="af-metric"><span class="af-metric__label">Requests, last 24 hours</span><span class="af-metric__value">48,210</span><p class="af-metric__helper">Total of GET and PUT. Sample data.</p><span class="af-infra-metric__asof"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-clock"/></svg>As of 2026-11-05 09:00 UTC (sample)</span></div></div></div>

      Upload queue, upload row and transfer status

      The queue is the live component from Forms: drop a file to see rows move through queued, uploading and complete. Each state has its own text, icon and valid actions. The static rows below show every state at once; their buttons raise a sample toast rather than doing nothing.

      Upload rows in each state (static specimens)

      Uploads

      2 of 4 complete~1.2 MB/s~12 s left
      • backup-2026-10.zip
        48.2 MBUploading
      • logo.png
        18.4 kBComplete
      • report-fail.json
        950 BFailed
        The connection was reset. Error ERR_CONNECTION_RESET.
      View code
      HTML
      <section class="af-queue" aria-label="Upload states"><div class="af-queue__head"><h4 class="af-queue__title">Uploads</h4><div class="af-transfer"><span class="af-transfer__item"><strong>2</strong> of 4 complete</span><span class="af-transfer__item"><strong>~1.2 MB/s</strong></span><span class="af-transfer__item">~12 s left</span></div></div>
      <ul class="af-queue__list">
        <li class="af-upload-row" data-status="uploading"><span class="af-file-icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file-archive"/></svg></span><div><div class="af-upload-row__name">backup-2026-10.zip</div><div class="af-upload-row__meta"><span>48.2 MB</span><span class="af-upload-row__status"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-loader-circle"/></svg>Uploading</span></div></div><div class="af-upload-row__actions"><button class="af-button af-button--ghost af-button--sm" type="button" data-toast="info" data-toast-title="Sample: upload would be canceled">Cancel</button></div><div class="af-upload-row__progress"><progress class="af-progress" max="100" value="64" aria-label="Upload progress for backup-2026-10.zip"></progress><span class="af-upload-row__pct" aria-hidden="true">64%</span></div></li>
        <li class="af-upload-row" data-status="complete"><span class="af-file-icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file-image"/></svg></span><div><div class="af-upload-row__name">logo.png</div><div class="af-upload-row__meta"><span>18.4 kB</span><span class="af-upload-row__status"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-check"/></svg>Complete</span></div></div><div class="af-upload-row__actions"><button class="af-button af-button--ghost af-button--sm" type="button" data-toast="info" data-toast-title="Sample: row would be removed">Remove</button></div></li>
        <li class="af-upload-row" data-status="failed"><span class="af-file-icon"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-file"/></svg></span><div><div class="af-upload-row__name">report-fail.json</div><div class="af-upload-row__meta"><span>950 B</span><span class="af-upload-row__status"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-x"/></svg>Failed</span></div></div><div class="af-upload-row__actions"><button class="af-button af-button--secondary af-button--sm" type="button" data-toast="info" data-toast-title="Sample: upload would retry">Retry</button></div><div class="af-upload-row__error"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-circle-alert"/></svg><span>The connection was reset. Error <code>ERR_CONNECTION_RESET</code>.</span></div></li>
      </ul></section>

      Endpoint display and copy control

      Endpoint: the URL is selectable text, wraps, and never an image (sample URL)
      S3-compatible endpoint Production
      HTTPS

      https://storage.example.test/v1/{bucket}

      View code
      HTML
      <div class="af-endpoint"><span class="af-endpoint__label">S3-compatible endpoint <span class="af-env" data-env="production">Production</span></span>
        <div class="af-endpoint__row"><span class="af-endpoint__method">HTTPS</span><p class="af-endpoint__url" id="in-ep">https://storage.example.test/v1/<span class="af-endpoint__param">{bucket}</span></p><button class="af-button af-button--secondary af-button--md" type="button" data-copy-target="#in-ep" data-copy-label="Endpoint"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg>Copy</button></div></div>

      API key row and secret reveal panel

      A key list never shows a secret: only a prefix and the last four characters. The full secret appears once, in the panel after creation or rotation, and must be acknowledged before it can be dismissed.

      API key list (sample keys)
      • CI uploader Active

        afk_s1••••••••••••a9f2
        objects:writeobjects:read
        Created
        2026-09-14
        Last used
        2026-11-04 (sample)
      • Old backup job Revoked

        afk_s1••••••••••••71c0
        objects:read
        Created
        2026-06-01
        Last used
        Never

      Revoke “CI uploader”?

      Anything using this key stops working immediately. This cannot be undone. Sample dialog: nothing is revoked.

      View code
      HTML
      <ul class="af-key-list">
        <li class="af-key-row" data-status="active"><div><h4 class="af-key-row__name">CI uploader <span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>Active</span></h4><span class="af-key-row__secret"><b>afk_s1</b>••••••••••••<span>a9f2</span></span></div><div class="af-cluster af-cluster--2"><button class="af-button af-button--secondary af-button--sm" type="button" data-toast="info" data-toast-title="Sample: a new secret would be created">Rotate</button><button class="af-button af-button--destructive af-button--sm" type="button" data-dialog-open="#in-revoke">Revoke</button></div><div class="af-key-row__perms"><span class="af-badge af-badge--outline">objects:write</span><span class="af-badge af-badge--outline">objects:read</span></div><dl class="af-key-row__meta"><div><dt>Created</dt><dd>2026-09-14</dd></div><div><dt>Last used</dt><dd>2026-11-04 (sample)</dd></div></dl></li>
        <li class="af-key-row" data-status="revoked"><div><h4 class="af-key-row__name">Old backup job <span class="af-badge"><span class="af-pip" aria-hidden="true"></span>Revoked</span></h4><span class="af-key-row__secret"><b>afk_s1</b>••••••••••••<span>71c0</span></span></div><div class="af-key-row__perms"><span class="af-badge af-badge--outline">objects:read</span></div><dl class="af-key-row__meta"><div><dt>Created</dt><dd>2026-06-01</dd></div><div><dt>Last used</dt><dd>Never</dd></div></dl></li>
      </ul>
      <dialog class="af-modal af-modal--sm" id="in-revoke" role="alertdialog" aria-labelledby="in-revoke-t" aria-describedby="in-revoke-d"><form method="dialog" class="af-modal__panel"><div class="af-alertdialog__head"><span class="af-alertdialog__icon" aria-hidden="true"><svg class="af-icon" focusable="false"><use href="#i-triangle-alert"/></svg></span><div><h4 class="af-modal__title" id="in-revoke-t">Revoke “CI uploader”?</h4><p class="af-modal__description" id="in-revoke-d">Anything using this key stops working immediately. This cannot be undone. Sample dialog: nothing is revoked.</p></div></div><div class="af-modal__footer"><button class="af-button af-button--secondary af-button--md" type="button" data-dialog-close autofocus>Cancel</button><button class="af-button af-button--destructive af-button--md" type="button" data-dialog-close="revoke" data-toast="danger" data-toast-title="Sample: key would be revoked">Revoke key</button></div></form></dialog>
      Secret reveal panel (sample value)

      Copy your secret now

      This is the only time it is shown. After you close this panel it cannot be recovered; create a new key instead.

      View code
      HTML
      <div class="af-secret-panel"><div class="af-secret-panel__head"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-key-round"/></svg><div><h4 class="af-secret-panel__title">Copy your secret now</h4><p class="af-secret-panel__text">This is the only time it is shown. After you close this panel it cannot be recovered; create a new key instead.</p></div></div>
        <div class="af-field"><label class="af-field__label" for="in-sec">Secret key (sample value)</label><div class="af-input-group af-secret"><input class="af-input" id="in-sec" type="password" value="sample-secret-0000-0000" readonly autocomplete="off"><button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-secret-toggle="#in-sec" aria-pressed="false" aria-label="Show secret"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-eye"/></svg></button><button class="af-button af-button--secondary af-button--icon af-button--md" type="button" data-copy-target="#in-sec" data-copy-label="Secret" aria-label="Copy secret"><svg class="af-icon" aria-hidden="true" focusable="false"><use href="#i-copy"/></svg></button></div></div>
        <label class="af-check"><input class="af-check__input" type="checkbox" id="in-ack"><span class="af-check__label">I have stored this secret somewhere safe</span></label>
        <div><button class="af-button af-button--primary af-button--md" type="button" aria-disabled="true" id="in-done" data-requires-check="#in-ack" data-toast="success" data-toast-title="Sample: panel would close">Done</button></div></div>

      Webhook delivery

      Delivery attempts: failed ones expand to show request and response (sample)
      200 OKobject.createdhttps://hooks.example.test/afaraAttempt 1 of 3182 ms
      {"event":"object.created","bucket":"acme-assets-prod"}
      {"received":true}
      500 Failedobject.deletedhttps://hooks.example.test/afaraAttempt 2 of 33,004 ms
      {"event":"object.deleted","key":"old.zip"}
      {"error":"upstream_timeout"}
      View code
      HTML
      <div class="af-delivery-list">
        <details class="af-delivery"><summary><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>200 OK</span><span class="af-delivery__event">object.created</span><svg class="af-icon af-delivery__chev" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg><span class="af-delivery__meta"><span>https://hooks.example.test/afara</span><span>Attempt 1 of 3</span><span>182 ms</span><time datetime="2026-11-05T08:58:11Z">08:58:11 UTC</time></span></summary><div class="af-delivery__body"><div class="af-code af-code--bare"><pre tabindex="0"><code data-lang="json">{"event":"object.created","bucket":"acme-assets-prod"}</code></pre></div><div class="af-code af-code--bare"><pre tabindex="0"><code data-lang="json">{"received":true}</code></pre></div></div></details>
        <details class="af-delivery" open><summary><span class="af-badge af-badge--danger"><span class="af-pip" aria-hidden="true"></span>500 Failed</span><span class="af-delivery__event">object.deleted</span><svg class="af-icon af-delivery__chev" aria-hidden="true" focusable="false"><use href="#i-chevron-down"/></svg><span class="af-delivery__meta"><span>https://hooks.example.test/afara</span><span>Attempt 2 of 3</span><span>3,004 ms</span><time datetime="2026-11-05T08:59:40Z">08:59:40 UTC</time></span></summary><div class="af-delivery__body"><div class="af-code af-code--bare"><pre tabindex="0"><code data-lang="json">{"event":"object.deleted","key":"old.zip"}</code></pre></div><div class="af-code af-code--bare"><pre tabindex="0"><code data-lang="json">{"error":"upstream_timeout"}</code></pre></div><div class="af-delivery__actions"><button class="af-button af-button--secondary af-button--sm" type="button" data-toast="info" data-toast-title="Sample: delivery would be retried">Redeliver</button></div></div></details>
      </div>

      Activity entry

      Activity log: actor, operation, resource, request ID, UTC time (sample events)
      • OKci-uploaderPutObjectacme-assets-prod/logo.pngreq_SAMPLE_0001
      • FAILbackup-jobDeleteObjectacme-assets-prod/old.zipreq_SAMPLE_0002
      View code
      HTML
      <ul class="af-log">
        <li class="af-log__row"><time class="af-log__time" datetime="2026-11-05T09:14:02Z" title="Local: 10:14:02 WAT">2026-11-05 09:14:02 UTC</time><span class="af-log__level"><span class="af-badge af-badge--success"><span class="af-pip" aria-hidden="true"></span>OK</span></span><span class="af-log__msg"><span class="af-activity-entry__actor">ci-uploader</span><span class="af-log__op">PutObject</span><span class="af-log__resource">acme-assets-prod/logo.png</span><span class="af-activity-entry__id">req_SAMPLE_0001</span></span></li>
        <li class="af-log__row"><time class="af-log__time" datetime="2026-11-05T09:15:30Z" title="Local: 10:15:30 WAT">2026-11-05 09:15:30 UTC</time><span class="af-log__level"><span class="af-badge af-badge--danger"><span class="af-pip" aria-hidden="true"></span>FAIL</span></span><span class="af-log__msg"><span class="af-activity-entry__actor">backup-job</span><span class="af-log__op">DeleteObject</span><span class="af-log__resource">acme-assets-prod/old.zip</span><span class="af-activity-entry__id">req_SAMPLE_0002</span></span></li>
      </ul>

      Usage breakdown

      Consumption by storage class: the whole, each part with value and share, and the period in words
      Stored this month90.0GB
      Sample data
      • Standard62.4 GB
      • Infrequent18.1 GB
      • Archive9.5 GB

      Period: 1 to 5 November (sample). Unit: decimal gigabytes. This is an estimate, not billing data.

      View code
      HTML
      <div class="af-usage"><div class="af-usage__head"><div class="af-usage__total"><span class="af-metric__label">Stored this month</span><span class="af-metric__value">90.0<span class="af-metric__unit">GB</span></span></div><span class="af-sample">Sample data</span></div>
        <div class="af-breakdown" role="img" aria-label="Standard 62.4 GB, Infrequent 18.1 GB, Archive 9.5 GB"><span class="af-breakdown__seg" data-series="1" style="--share: 62.4"></span><span class="af-breakdown__seg" data-series="2" style="--share: 18.1"></span><span class="af-breakdown__seg" data-series="3" style="--share: 9.5"></span></div>
        <ul class="af-usage__rows"><li class="af-usage__row"><span class="af-legend__swatch" data-series="1"></span><span>Standard</span><span>62.4 GB</span><span class="af-usage__share">69%</span></li><li class="af-usage__row"><span class="af-legend__swatch" data-series="2"></span><span>Infrequent</span><span>18.1 GB</span><span class="af-usage__share">20%</span></li><li class="af-usage__row"><span class="af-legend__swatch" data-series="3"></span><span>Archive</span><span>9.5 GB</span><span class="af-usage__share">11%</span></li></ul>
        <p class="af-usage__explain">Period: 1 to 5 November (sample). Unit: decimal gigabytes. This is an estimate, not billing data.</p></div>

      The seven patterns

      Patterns arrange the components into whole views. Each is built in one of the example pages, using only documented components and flagged sample data.

      Infrastructure patterns and where to see them
      PatternComposesExample
      Resource hierarchyResource path, switchers, environment badge, resource cardBuckets
      Storage overviewMetric cards, capacity card, usage breakdown, chart, activityStorage overview
      File managementObject explorer, file rows, inspector, bulk bar, paginationFiles
      API managementEndpoint display, key rows, secret panel, webhook deliveriesAPI keys and webhooks
      ActivityEvent log, activity entries, timeline, filtersDashboard
      Usage and billingMeters, breakdown, estimate notes, key-value listsStorage overview
      Upload workflowDropzone, upload queue and rows, transfer status, error statesFiles

      Do

      • Show the resource’s type and name together, and keep the environment visible.
      • Show used and limit in one unit, and say when a figure is an estimate.
      • Give every failure a reason and a code, and offer the valid next action.

      Don’t

      • Display a secret in a list, or reveal one automatically.
      • Invent a status, uptime, customer, certification or production metric.
      • Hide an unavailable region or option. Show it disabled with the reason.
      ↑↓ moveEnter goEsc close