Patterns · Settings
Number with unit
A fixed-width digit input with the unit written IN FULL inline (rendered exactly as passed — "seconds", "points" — muted grey), a hover ▲/▼ stepper, a hard digit cap, clamp-on-change, and an error line — the settings field for a number that carries a unit.
Examples
Unit
Size
Hover a field to reveal the ▲/▼ stepper · the unit is inline, written IN FULL ("seconds")
Error line (probe) · sizes · disabled
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-number-with-unit> is the SAME shared custom element React and Vue consume — here in its
native form. Theming comes only from the --aha-* tokens in tokens.css. Hover the field to reveal
the ▲/▼ stepper; the unit is written inline IN FULL — rendered exactly as passed ("seconds", "points"). -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-number-with-unit.js'; // registers <aha-number-with-unit>
</script>
<!-- a per-question timer: unit written in full, clamps to 5–120, hard cap of 4 digits -->
<aha-number-with-unit id="timer" value="20" unit="seconds" min="5" max="120" maxdigits="4"></aha-number-with-unit>
<!-- points, and a field carrying an error message -->
<aha-number-with-unit value="1000" unit="points" min="0" max="9999" maxdigits="4"></aha-number-with-unit>
<aha-number-with-unit value="200" unit="seconds" min="5" max="120" errormessage="Must be 120 or less."></aha-number-with-unit>
<script>
// Plain DOM — no framework. e.detail.value is the clamped number.
document.getElementById('timer').addEventListener('change', (e) => {
console.log('seconds:', e.detail.value);
});
</script>
import '@ahaslides-product/design/aha-number-with-unit'; // registers <aha-number-with-unit>
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper; React 19 can use <aha-number-with-unit> directly.
// e.detail.value is the clamped number; the unit is written inline IN FULL — rendered exactly as passed ("seconds").
function AhaNumberWithUnit({ value, unit, min, max, step, maxDigits, size, errorMessage, disabled, onChange }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
el.value = value == null ? '' : String(value);
el.disabled = !!disabled;
const h = (e) => onChange?.(e.detail.value); // composed CustomEvent
el.addEventListener('change', h);
return () => el.removeEventListener('change', h);
});
return (
<aha-number-with-unit
ref={ref}
unit={unit}
{...(min != null ? { min: String(min) } : {})}
{...(max != null ? { max: String(max) } : {})}
{...(step != null ? { step: String(step) } : {})}
{...(maxDigits ? { maxdigits: String(maxDigits) } : {})}
{...(size ? { size } : {})}
{...(errorMessage ? { errormessage: errorMessage } : {})}
/>
);
}
// usage
<AhaNumberWithUnit value={20} unit="seconds" min={5} max={120} maxDigits={4} onChange={setSeconds} />
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-number-with-unit'; // registers <aha-number-with-unit>
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<script setup>
import { ref } from 'vue';
const seconds = ref(20);
</script>
<template>
<!-- the unit is written inline IN FULL — rendered exactly as passed ("seconds"); e.detail.value is the clamped number -->
<aha-number-with-unit
unit="seconds"
min="5"
max="120"
maxdigits="4"
:value.prop="String(seconds)"
@change="seconds = $event.detail.value"
/>
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
value | number | — | Controlled numeric value |
unit | string | — | Inline suffix — written IN FULL, rendered exactly as passed (seconds, points, %) |
min | number | — | Lower clamp bound |
max | number | — | Upper clamp bound |
step | number | 1 | Stepper / arrow-key increment |
maxdigits | number | 4 | Hard digit cap — a keystroke past this many digits is ignored |
size | small | default | large | default | Control height — small 24 · default 32 · large 40; radius stays 8 |
errormessage | string | — | Presence draws the red border/ring and the message line below |
disabled | boolean | false | Non-interactive; grey fill |
change | CustomEvent<{value}> | — | Composed event on commit / stepper / arrow; read e.detail.value (clamped number) |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-number-with-unit'; // registers <aha-number-with-unit>
Agent feed for this component (absolute, fetchable anywhere): number-with-unit.agent.json · number-with-unit.md · number-with-unit.llms.txt
When to use
When to use
- Number with unit — a number the host does NOT provide that carries a unit — a per-item timer, points-per-answer, a spin duration
- Host time limit — a standard whole-slide countdown — use the host-native enableTimeLimit, not this
- Select — the unit itself is changeable — a unit dropdown, not an inline suffix
A fixed unit is an inline suffix inside the field (SETTINGS-41) — a bordered box beside the input implies the unit is selectable. Write the unit IN FULL, rendered exactly as passed (`seconds`, `points`) — never truncated to a short label. Ship pre-filled with the recommended default and clamp immediately.
Surfaces
editor settings
Spec
Box height 32 (default) · radius 8 · digit input + inline unit + stepper · Sizes small 24 · default 32 · large 40 — all radius 8 · Unit 14/21 #8A8A8A inline suffix; written IN FULL, rendered exactly as passed (seconds, points) · Stepper ▲/▼ tertiary (system-caret-up/down), revealed on hover/focus; disabled at min/max · Digit cap maxDigits (default 4) — a keystroke past the cap is dropped; numeric only · Clamp value clamps to min/max on change AND blur (immediate fallback) · Error #F5222D border + ring + a 12/18 #F5222D message line below; sets aria-invalid · Focus #6A1EBB border + 2px #D3B4FF@30% ring (persistent wrapper) · Disabled #F1F1F1 fill, #EBEBEB border, not-allowed