Skip to main content

Components

All 23, in the framework you pick above. The props, types and code samples change with it; the preview does not — see the note under each title.

Button

HTML / CSS · .hg-btn

Preview

Props

PropTypeNotes
variant"primary" | "ghost" | "icon"Defaults to primary.
size"sm"Omit for the default height.
blockboolean attributeFills the width of its container.
loadingboolean attributePrepends the pulsing dot and sets aria-busy.
disabledboolean attribute
hrefstringRenders an anchor instead of a button.
aria-labelstringRequired on an icon button; copied onto the inner control.

UsageHTML / CSS

<button class="hg-btn hg-btn--primary">Start a task</button>
<button class="hg-btn hg-btn--ghost hg-btn--sm">Cancel</button>

<!-- An icon button is nothing but an icon, so it must carry the label -->
<button
class="hg-btn hg-btn--icon"
aria-label="More options"
>
<span aria-hidden="true"></span>
</button>

HTML / CSS · .hg-link

Preview

View all

Props

PropTypeNotes
direction"back"Omit for forward.
hrefstringRenders an anchor. Without it you get a button.

UsageHTML / CSS

<a
class="hg-link"
href="/clients"
>
View all <span class="hg-link__arrow" aria-hidden="true"></span>
</a>

<button class="hg-link hg-link--back">
<span
class="hg-link__arrow"
aria-hidden="true"
>
</span> Back
</button>

TextField

HTML / CSS · .hg-field

Preview

Client Pick a client before saving.

Props

PropTypeNotes
valuestringWhat is shown. Typing fires hg-change and changes nothing.
label
hint
error
stringAs above.
monoboolean attribute
multilineboolean attributeWith rows.
disabledboolean attribute
autoboolean attributeLets the field keep its own value. Prototypes only.
hg-changeeventdetail.value is the new text.

UsageHTML / CSS

<label class="hg-field">
<span class="hg-field__label">Description</span>
<input
class="hg-field__input"
placeholder="What are you working on?"
/>
<span class="hg-field__hint">One line. Keep it short.</span>
</label>

<!-- Invalid: weight and an icon, never a red border -->
<div class="hg-field hg-field--invalid">
<span class="hg-field__label">Client</span>
<input
class="hg-field__input"
aria-invalid="true"
/>
<span class="hg-field__error"><span aria-hidden="true">!</span> Pick a client.</span>
</div>

Select

HTML / CSS · .hg-select

Preview

Props

PropTypeNotes
valuestring
optionsJSON stringAttributes are strings, so the list comes in as JSON.
label
hint
error
string
hg-changeeventdetail.value is the chosen value.

Types

SelectOption

// Attributes are strings, so the list arrives as JSON.
options='[
{ "value": "redesign", "label": "Redesign" },
{ "value": "maintenance", "label": "Maintenance", "disabled": true }
]'
FieldTypeNotes
valuestringWhat is stored, and what comes back from onChange.
labelstringWhat is shown. The component never translates or formats it.
disabledboolean?Rendered but not selectable.

UsageHTML / CSS

<label class="hg-field">
<span class="hg-field__label">Project</span>
<span class="hg-select">
<select class="hg-select__control">
<option value="redesign">Redesign</option>
<option value="maintenance">Maintenance</option>
</select>
<span
class="hg-select__chevron"
aria-hidden="true"
>
</span>
</span>
</label>

DateTimeField

HTML / CSS · .hg-dtf

Preview

August 2026

Props

PropTypeNotes
valueISO stringhg-change reports a real Date in detail.value.
min
max
ISO string
week-starts-on0 | 1
localestring
show-exact"false"Hides the typed box.
autoboolean attributeLets the field keep its own value.

UsageHTML / CSS

<!-- The markup is the styling; the dates are always your data.
Build the cells yourself, or use one of the framework libraries. -->
<div class="hg-dtf">
<div class="hg-dtf__head">
<button
class="hg-btn hg-btn--icon"
aria-label="Previous month"
>
</button>
<span class="hg-dtf__month">August 2026</span>
<button
class="hg-btn hg-btn--icon"
aria-label="Next month"
>
</button>
</div>
<div
class="hg-dtf__grid"
role="grid"
>
<!-- .hg-dtf__dow ×7, then .hg-dtf__day ×42 --></div>
<div class="hg-dtf__time">
<div
class="hg-dtf__col"
role="listbox"
aria-label="Hour"
>
<!-- .hg-dtf__opt ×24 --></div>
<div
class="hg-dtf__col"
role="listbox"
aria-label="Minute"
>
<!-- .hg-dtf__opt ×60 --></div>
</div>
</div>

Toggle

HTML / CSS · .hg-toggle

Preview

Props

PropTypeNotes
checkedboolean attribute
labelstring
disabledboolean attribute
autoboolean attributeLets the toggle flip itself. Prototypes only.
hg-changeeventdetail.checked.

UsageHTML / CSS

<button
class="hg-toggle"
role="switch"
aria-checked="true"
id="billable"
>
<span class="hg-toggle__track"><span class="hg-toggle__knob"></span></span>
<span class="hg-toggle__label">Billable</span>
</button>

<script>
const el = document.getElementById("billable");
el.addEventListener("hg:change", (e) => {
save(e.detail.checked);
Hourglass.setToggle(el, e.detail.checked);
});
</script>

Segmented

HTML / CSS · .hg-segmented

Preview

Props

PropTypeNotes
optionsJSON string[{"value":"day","label":"Day"}].
valuestring
blockboolean attribute
autoboolean attribute
hg-changeeventdetail.value.
layout()methodRe-measures the thumb. Called for you on resize.

Types

SegmentedOption

React types label as a node, so an icon or a mono figure can sit in a segment. Vue, Svelte, Angular and React Native take a plain string — a segment is a one-word label, and anything richer belongs in a different control.

// Attributes are strings, so the list arrives as JSON.
options='[
{ "value": "day", "label": "Day" },
{ "value": "week", "label": "Week" },
{ "value": "month", "label": "Month" }
]'
FieldTypeNotes
valuestringGeneric, so a union type ("day" | "week") narrows onChange for you.
labelReactNode | stringWhat is shown. Never formatted by the component.

UsageHTML / CSS

<div class="hg-segmented">
<span class="hg-segmented__thumb"></span>
<button
class="hg-segmented__item"
data-value="day"
aria-selected="true"
>
Day</button>
<button
class="hg-segmented__item"
data-value="week"
aria-selected="false"
>
Week</button>
</div>

<script>
Hourglass.enhance();
el.addEventListener("hg:change", (e) => Hourglass.setSegmented(el, e.detail.value));
</script>

ChoiceRow

HTML / CSS · .hg-choice

Preview

Props

PropTypeNotes
kind"radio" | "check"Defaults to check.
checkedboolean attribute
title
description
string
disabledboolean attribute
autoboolean attribute
hg-changeeventdetail.checked and detail.value.

UsageHTML / CSS

<button
class="hg-choice hg-choice--radio"
role="radio"
aria-checked="true"
>
<span class="hg-choice__mark"><span aria-hidden="true"></span></span>
<span class="hg-choice__body">
<span class="hg-choice__title">Round to the nearest minute</span>
<span class="hg-choice__desc">Exports keep the raw seconds.</span>
</span>
</button>

FilterPill

HTML / CSS · .hg-pill

Preview

Props

PropTypeNotes
pressedboolean attribute
countstring
valuestringEchoed back in the event, so one listener can serve a row of pills.
autoboolean attribute
hg-changeeventdetail.pressed, detail.value.

UsageHTML / CSS

<button
class="hg-pill"
aria-pressed="true"
>
All <span class="hg-pill__count">42</span></button>
<button
class="hg-pill"
aria-pressed="false"
>
Billable <span class="hg-pill__count">31</span></button>

Card

HTML / CSS · .hg-card

Preview

Acme Ltd

3 projects · last worked yesterday

Floating

For something genuinely above the page.

Props

PropTypeNotes
bordered
flat
floating
boolean attribute
hrefstringRenders an anchor.

UsageHTML / CSS

<div class="hg-card hg-card--bordered">
<p class="hg-h3">Acme Ltd</p>
<p class="hg-small hg-muted">3 projects · last worked yesterday</p>
</div>

Row

HTML / CSS · .hg-row

Preview

Props

PropTypeNotes
title
meta
value
string
hrefstring
interactiveboolean attributeHover tint and pointer without a link.
Children are projected after meta, inside the body.

UsageHTML / CSS

<button class="hg-row hg-row--interactive">
<span
class="hg-row__lead"
aria-hidden="true"
>
</span>
<span class="hg-row__body">
<span class="hg-row__title">Rebuild the timesheet flow</span>
<span class="hg-row__meta">Acme · Redesign</span>
</span>
<span class="hg-row__value">2:15</span>
</button>

SectionLabel

HTML / CSS · .hg-section-label

Preview

Props

PropTypeNotes
asidestringQuiet text on the right.

UsageHTML / CSS

<p class="hg-section-label">
<span>Tracked today</span>
<span class="hg-section-label__aside">6:12</span>
</p>

StatTile

HTML / CSS · .hg-stat

Preview

Tracked 32:10 This week
Billable £2,080 At £65/hr
Entries 42

Props

PropTypeNotes
label
value
aside
stringChildren render inside the value, after value.
compactboolean attribute

UsageHTML / CSS

<div class="hg-stat">
<span class="hg-stat__label">Tracked</span>
<span class="hg-stat__value">32:10</span>
<span class="hg-stat__aside">This week</span>
</div>

Chip

HTML / CSS · .hg-chip

Preview

Acme Ltd Redesign Not removable

Props

PropTypeNotes
removableboolean attributeShows the remove control.
remove-labelstring
hg-removeevent

UsageHTML / CSS

<span class="hg-chip">
Acme Ltd
<button
class="hg-chip__remove"
aria-label="Remove Acme Ltd"
>
</button>
</span>

Badge

HTML / CSS · .hg-badge

Preview

Draft Running Synced Pro

Props

PropTypeNotes
variant"solid" | "outline" | "lock"Omit for the default.

UsageHTML / CSS

<span class="hg-badge">Draft</span>
<span class="hg-badge hg-badge--solid">Running</span>
<span class="hg-badge hg-badge--lock"><span aria-hidden="true">🔒</span> Pro</span>

EmptyState

HTML / CSS · .hg-empty

Preview

Nothing tracked yet

Entries you record will show up here, newest first.

Props

PropTypeNotes
iconstringA glyph.
titlestring
action-labelstringRenders the dashed CTA.
hg-actionevent

Types

EmptyStateAction

One action, or none. An empty state offering two ways out is a menu, and the dashed CTA is the only affordance the component draws.

<!-- No object: the label is an attribute and the press is an event. -->
<hg-empty-state title="Nothing tracked yet" action-label="Start a task"></hg-empty-state>

<script type="module">
el.addEventListener("hg-action", start);
</script>
FieldTypeNotes
action-labelattributeRenders the dashed CTA. Omit it and no button appears.
hg-actioneventFired on press. Bubbling and composed.

UsageHTML / CSS

<div class="hg-empty">
<span
class="hg-empty__icon"
aria-hidden="true"
>
</span>
<p class="hg-empty__title">Nothing tracked yet</p>
<p class="hg-empty__body">Entries you record will show up here, newest first.</p>
<button class="hg-empty__cta">Start a task</button>
</div>

RollingNumber

HTML / CSS · .hg-reel

Preview

static:

Props

PropTypeNotes
valuestringAlready formatted.
staticboolean attributeNo travel.
duration
delay
stagger
numberMilliseconds.
easingstringA CSS timing function.
labelstringFalls back to the value if omitted.

UsageHTML / CSS

<span
class="hg-h1"
data-hg-reel="4:40"
aria-label="4 hours 40 minutes"
></span>

<script>
Hourglass.enhance();
// Later — only the digits that changed will move
Hourglass.reel(el, "5:02", { label: "5 hours 2 minutes" });
</script>

ProgressBar

HTML / CSS · .hg-progress

Preview

Today6:12 / 8:00
Yesterday9:30 / 8:00 · +1:30

Props

PropTypeNotes
value
max
number
label
value-label
string
indeterminateboolean attribute
Hourglass.setProgress(el, f)methodHTML library only — sets the fill from a 0–1 fraction.

UsageHTML / CSS

<div
class="hg-progress"
id="today"
>
<div class="hg-progress__head"><span>Today</span><span>6:12 / 8:00</span></div>
<div class="hg-progress__track"><div class="hg-progress__fill"></div></div>
</div>

<script>Hourglass.setProgress(document.getElementById("today"), 6.2 / 8);</script>

Bars

HTML / CSS · .hg-bars

Preview

116:20
128:05
133:40
140:00

Props

PropTypeNotes
itemsJSON stringSame shape as above.
max, label-width, value-width, staggernumber
interactiveboolean attributeEnables hg-select.
hg-selecteventdetail.key.

Types

BarItem

value is the number the bar is drawn from; valueLabel is the text printed beside it. They are separate because the bar needs a magnitude and the reader needs 7:24 — and only you know that 7.4 hours is written that way.

// Attributes are strings, so the list arrives as JSON.
items='[
{ "key": "2026-08-16", "label": "16", "value": 7.4, "valueLabel": "7:24", "emphasis": true },
{ "key": "2026-08-17", "label": "17", "value": 3.7, "valueLabel": "3:40" }
]'
FieldTypeNotes
keystringIdentity. Used for the render key and handed back by onSelect. Never rendered.
labelReactNode | stringThe left column. The component never formats a date or a name.
valuenumberWhat the bar length is derived from, against max.
valueLabelReactNode?The right column. Falls back to value if omitted, which is rarely what you want.
emphasisboolean?Accent fill and full-weight text — the "today" bar. Tone and weight, never a second hue.

UsageHTML / CSS

<div
class="hg-bars"
id="week"
>
<div class="hg-bars__row">
<span class="hg-bars__label">16</span>
<span class="hg-bars__track"><span class="hg-bars__fill"></span></span>
<span class="hg-bars__value">7:24</span>
</div>
</div>

<script>
document.querySelectorAll("#week .hg-bars__row")
.forEach((row, i) => Hourglass.setBar(row, fractions[i]));
</script>

RadialGauge

HTML / CSS · .hg-gauge

Preview

Today of 8:00

Props

PropTypeNotes
value
max
size
thickness
number
labelstring
Hourglass.gauge(el, opts)methodHTML library only — builds and updates the SVG.

UsageHTML / CSS

<div
class="hg-gauge"
id="dial"
>
<div class="hg-gauge__center">
<span class="hg-caption hg-muted">Today</span>
<span
class="hg-h1 hg-mono"
data-hg-reel="6:12"
aria-label="6 hours 12 minutes"
></span>
<span class="hg-micro hg-faint">of 8:00</span>
</div>
</div>

<script>
Hourglass.gauge(document.getElementById("dial"), {
value: 6.2, max: 8, size: 220, thickness: 14,
label: "6 hours 12 minutes of an 8 hour target",
});
</script>

BottomSheet

HTML / CSS · .hg-sheet

Preview

Props

PropTypeNotes
openboolean attribute
title
labelled-by
string
autoboolean attributeLets the sheet close itself.
hg-dismisseventdetail.reason is scrim, close or escape.
[data-hg-open="id"]attributeOn any button — opens that sheet.

UsageHTML / CSS

<button
class="hg-btn hg-btn--ghost"
data-hg-open="sheet"
>
Open sheet</button>

<div
class="hg-sheet"
id="sheet"
hidden
data-hg-auto
>
<button
class="hg-sheet__scrim"
aria-label="Dismiss"
></button>
<div
class="hg-sheet__panel"
tabindex="-1"
aria-labelledby="sheet-title"
>
<div class="hg-sheet__head">
<h2
class="hg-sheet__title"
id="sheet-title"
>
Start</h2>
<button
class="hg-btn hg-btn--icon"
data-hg-close
aria-label="Close"
>
</button>
</div>
<div class="hg-sheet__body"></div>
</div>
</div>

Toast

HTML / CSS · .hg-toast

Preview

Entry saved

Props

PropTypeNotes
message
action-label
string
leavingboolean attribute
hg-actionevent
Hourglass.toast(opts)methodHTML library only. Owns its own timer; returns { dismiss }.

Types

ToastAction

At most one, and it is the caller's job to dismiss the toast afterwards — the component has no timer to cancel. A toast with two decisions in it is a dialog.

<!-- No object: the label is an attribute and the press is an event. -->
<hg-toast message="Entry saved" action-label="Undo"></hg-toast>

<script type="module">
el.addEventListener("hg-action", undo);
</script>

<!-- The HTML library's imperative helper is the one place a timer is owned -->
<script>
Hourglass.toast({
message: "Entry saved",
action: { label: "Undo", onPress: undo },
durationMs: 4000,
});
</script>
FieldTypeNotes
action-labelattributeRenders the action. Omit it for a toast that only reports.
hg-actioneventFired on press.
durationMsnumberHourglass.toast() only. Defaults to 4000, and returns { dismiss }.

UsageHTML / CSS

<script>
Hourglass.toast({
message: "Entry saved",
action: { label: "Undo", onPress: undo },
durationMs: 4000,
});
</script>

ToastRegion

HTML / CSS · .hg-toast-region

Preview

A fixed region at the bottom of the viewport — see the Toast section's button for it in use.

Props

PropTypeNotes
No attributes. Put <hg-toast> elements inside it.

UsageHTML / CSS

<!-- Created for you by Hourglass.toast() if it does not exist -->
<div
class="hg-toast-region"
role="status"
aria-live="polite"
></div>