Number
<gui-number> is a number field. It renders a native <input type="number"> with its label, hint
and errors. The arrow keys step the number up and down, within the bounds you set.
import { GuiNumber } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function NumberBasic() { return <GuiNumber label="Guests" value={2} />;}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/number';
@Component({ selector: 'app-number-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: `<gui-number label="Guests" value="2"></gui-number>`,})export class NumberBasicComponent {}<script setup lang="ts">import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';</script>
<template> <gui-number label="Guests" :value="2"></gui-number></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';
@customElement('number-basic')export class NumberBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-number label="Guests" value="2"></gui-number> `; }}<gui-number label="Guests" value="2"></gui-number>The element’s entry point is @golemui/gui-components/number, which registers <gui-number> and
nothing else. To install the package and load the stylesheet, see
Installation.
The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.
import { useState } from 'react';import { GuiNumber } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function GuestsField() { const [guests, setGuests] = useState(1);
return ( <GuiNumber label="Guests" name="guests" value={guests} onGuiChange={(event) => setGuests(event.detail.value ?? 1)} /> );}Add CUSTOM_ELEMENTS_SCHEMA to the component that uses the tags, and the stylesheet to styles in angular.json.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { GuiEventMap, GuiNumberEvents } from '@golemui/gui-components';import '@golemui/gui-components/number';
type ChangeEvent = GuiEventMap<typeof GuiNumberEvents>['gui-change'];
@Component({ selector: 'app-guests-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-number label="Guests" name="guests" [value]="guests" (gui-change)="onChange($event)" ></gui-number> `,})export class GuestsFieldComponent { guests = 1;
onChange(event: Event) { this.guests = (event as ChangeEvent).detail.value ?? 1; }}"styles": ["node_modules/@golemui/gui-components/lib/styles/index.css"]Tell Vue's compiler that gui-* tags are custom elements, and add the typings once for typed props and @gui-* handlers.
<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';
const guests = ref(1);</script>
<template> <gui-number label="Guests" name="guests" :value="guests" @gui-change="guests = $event.detail.value ?? 1" /></template>vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('gui-') } },});import type {} from '@golemui/gui-components/vue';The page's stylesheet styles the gui-* elements, so it only reaches them outside a shadow root. Render the host component into the light DOM too, as below, or load the stylesheet into its shadow root.
import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';
@customElement('guests-field')export class GuestsField extends LitElement { @state() private guests = 1;
protected override createRenderRoot() { return this; }
override render() { return html` <gui-number label="Guests" name="guests" .value=${this.guests} @gui-change=${(event: CustomEvent) => (this.guests = event.detail.value)} ></gui-number> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-number label="Guests" name="guests"></gui-number>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-number');
element.addEventListener('gui-change', (event) => { console.log(event.detail.value);});The value is a number, or undefined while the field is empty. The snippets fall back to 1 when
the user clears the field.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
localeId
attribute locale-id | string | 'en' | BCP 47 locale for formatting and parsing, such as en-US or es. |
value | number | – | The number, or undefined when empty. |
step | number | – | The step of the ArrowUp and ArrowDown keys. Defaults to 1. An empty field steps from 0. |
placeholder | string | – | Text shown while the control is empty. |
autocomplete | string | – | The autocomplete hint passed to the inner native control. |
minimum | number | – | Smallest allowed number. |
maximum | number | – | Largest allowed number. |
autoGrow
attribute auto-grow | boolean | false | Grows the field with its content instead of scrolling. |
uid | string | – | The id of the element's control. The ids of its label, hint and errors derive from it. Optional: when it is not set, the element generates one from its tag name and a page-wide counter, e.g. gui-textinput-3. Generated ids depend on creation order, so a server-rendered element must set uid or its ids will not match after hydration. |
Form control properties
Every field has these, from the base class of the form controls.
| Property | Type | Default | Description |
|---|---|---|---|
name | string | – | The name the value is submitted under. Without it the element stays out of its form. Reflected, like a native control's: the form reads a single value's name from the attribute. |
label | string | – | The visible label, which also names the control for assistive technology. Without one the control has no accessible name; the date, time, range, tags and file fields warn about it in development builds. |
hint | string | – | Help text shown under the label and announced as the control's description. |
errors | string[] | [] | Error messages to show, typically from your own validation. They show as soon as they are set, unless touched is false. |
touched | boolean | – | Whether the user has interacted with the control. Leave it unset unless you validate on interaction: false holds errors back until it becomes true. |
required | boolean | false | The control needs a value: it is marked as required and, with a name, blocks submission. |
readOnly
attribute readonly | boolean | – | The value can be read and focused but not changed. |
disabled | boolean | – | Disabled by its own attribute or by a disabled <fieldset> around it. Setting the property sets the attribute, like a native control's. |
form read-only, no attribute | HTMLFormElement | null | – | The <form> the element belongs to, if any. |
validity read-only, no attribute | ValidityState | – | The element's validity, like a native control's. |
validationMessage read-only, no attribute | string | – | The message the browser shows for an invalid value. |
Methods
| Method | Description |
|---|---|
checkValidity(): boolean | – |
reportValidity(): boolean | – |
Events
Every event bubbles and is composed, so you can listen on any ancestor.
| Event | Description |
|---|---|
gui-input | The user changed the value. detail.value is the new value. |
gui-change | The user committed the number: on blur or Enter, or at once with a step. detail.value is the number. |
gui-blur | Focus left the control. |
Examples
Section titled “Examples”Step and bounds
Section titled “Step and bounds”↑ and ↓ add or take away step, which is 1 by default. An
empty field steps from 0. minimum and maximum stop the arrow keys at the bounds.
import { GuiNumber } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function NumberBounds() { return ( <div className="gui-grid gui-grid--row"> <div className="gui-grid__cell"> <GuiNumber label="Tickets" value={1} minimum={1} maximum={6} /> </div> <div className="gui-grid__cell"> <GuiNumber label="Hours" value={1.5} step={0.5} minimum={0.5} maximum={8} /> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/number';
@Component({ selector: 'app-number-bounds', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Tickets" value="1" minimum="1" maximum="6" ></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Hours" value="1.5" step="0.5" minimum="0.5" maximum="8" ></gui-number> </div> </div> `,})export class NumberBoundsComponent {}<script setup lang="ts">import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Tickets" :value="1" :minimum="1" :maximum="6" ></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Hours" :value="1.5" :step="0.5" :minimum="0.5" :maximum="8" ></gui-number> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';
@customElement('number-bounds')export class NumberBounds extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Tickets" value="1" minimum="1" maximum="6" ></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Hours" value="1.5" step="0.5" minimum="0.5" maximum="8" ></gui-number> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Tickets" value="1" minimum="1" maximum="6"></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Hours" value="1.5" step="0.5" minimum="0.5" maximum="8"></gui-number> </div></div>The user can still type a number outside the bounds. The field keeps it: in a form, it blocks the
submit (see In a form). To show a message as the user works, set errors from your
own validation.
Auto-grow
Section titled “Auto-grow”The field is narrow, made for short numbers, and a longer number scrolls inside it. With
auto-grow, the field starts narrower still and widens to fit the number as the user types.
import { GuiNumber } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function NumberAutoGrow() { return ( <div className="gui-grid gui-grid--row"> <div className="gui-grid__cell"> <GuiNumber label="Fixed width" hint="Type 8100000000." /> </div> <div className="gui-grid__cell"> <GuiNumber label="Auto-grow" hint="Type 8100000000." autoGrow /> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/number';
@Component({ selector: 'app-number-auto-grow', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Fixed width" hint="Type 8100000000."></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Auto-grow" hint="Type 8100000000." auto-grow ></gui-number> </div> </div> `,})export class NumberAutoGrowComponent {}<script setup lang="ts">import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Fixed width" hint="Type 8100000000."></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Auto-grow" hint="Type 8100000000." :autoGrow="true" ></gui-number> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';
@customElement('number-auto-grow')export class NumberAutoGrow extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Fixed width" hint="Type 8100000000."></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Auto-grow" hint="Type 8100000000." auto-grow ></gui-number> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Fixed width" hint="Type 8100000000."></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Auto-grow" hint="Type 8100000000." auto-grow></gui-number> </div></div>Errors
Section titled “Errors”errors takes a list of messages from your own validation, and the field is marked invalid. To
check the number as the user works, set the errors property from a gui-change listener. Its
detail.value is a number, or undefined for an empty field.
import { useState } from 'react';import { GuiNumber } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function NumberValidation() { const [errors, setErrors] = useState<string[]>([]);
return ( <GuiNumber id="team-size" label="Team size" hint="Even numbers only, for pair work." errors={errors} onGuiChange={(event) => { const value = event.detail.value; setErrors( value === undefined || value % 2 === 0 ? [] : ['Pick an even number.'], ); }} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiNumberEvents } from '@golemui/gui-components';import '@golemui/gui-components/number';
type NumberEvent<K extends keyof typeof GuiNumberEvents> = GuiEventMap< typeof GuiNumberEvents>[K];
@Component({ selector: 'app-number-validation', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-number id="team-size" label="Team size" hint="Even numbers only, for pair work." [errors]="errors()" (gui-change)="onChange($event)" ></gui-number> `,})export class NumberValidationComponent { errors = signal<string[]>([]);
onChange(event: Event) { const { value } = (event as NumberEvent<'gui-change'>).detail; this.errors.set( value === undefined || value % 2 === 0 ? [] : ['Pick an even number.'], ); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';
const errors = ref<string[]>([]);
function onChange(value: number | undefined) { errors.value = value === undefined || value % 2 === 0 ? [] : ['Pick an even number.'];}</script>
<template> <gui-number id="team-size" label="Team size" hint="Even numbers only, for pair work." :errors.prop="errors" @gui-change="onChange($event.detail.value)" ></gui-number></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiEventMap, GuiNumberEvents } from '@golemui/gui-components';import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';
type NumberEvent<K extends keyof typeof GuiNumberEvents> = GuiEventMap< typeof GuiNumberEvents>[K];
@customElement('number-validation')export class NumberValidation extends LitElement { @state() private errors: string[] = [];
protected override createRenderRoot() { return this; }
private onChange(event: NumberEvent<'gui-change'>) { const value = event.detail.value; this.errors = value === undefined || value % 2 === 0 ? [] : ['Pick an even number.']; }
override render() { return html` <gui-number id="team-size" label="Team size" hint="Even numbers only, for pair work." .errors=${this.errors} @gui-change=${this.onChange} ></gui-number> `; }}<gui-number id="team-size" label="Team size" hint="Even numbers only, for pair work."></gui-number>
<script type="module"> const size = document.querySelector('#team-size');
size.addEventListener('gui-change', (event) => { const value = event.detail.value; size.errors = value === undefined || value % 2 === 0 ? [] : ['Pick an even number.']; });</script>placeholder, hint, disabled and readonly work as on the
text input. A read-only field doesn’t step.
The field takes digits, a decimal point, a sign and an exponent, and drops any other character
the user types or pastes. It shows the number as the browser’s own number input does: locale-id
doesn’t change it.
In a form
Section titled “In a form”With a name, <gui-number> submits its number as text, such as 3 or 1.5, under that name.
0 is submitted; an empty field submits nothing. These block a submit:
required, while the field is empty;- a number below
minimumor abovemaximum, with the message “Value must be greater than or equal to …” or “less than or equal to …”.
step is only the step of the arrow keys: any number, decimals included, is valid.
Submit the form below empty, then with too many adults.
import { useState, type SubmitEvent } from 'react';import { GuiButton, GuiNumber } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function NumberNativeForm() { const [result, setResult] = useState('');
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => { event.preventDefault(); const data = Object.fromEntries(new FormData(event.currentTarget)); setResult(`Submitted ${JSON.stringify(data)}`); };
return ( <form id="booking" onSubmit={onSubmit}> <div className="gui-grid gui-grid--row"> <div className="gui-grid__cell"> <GuiNumber label="Adults" name="adults" minimum={1} maximum={8} required /> </div> <div className="gui-grid__cell"> <GuiNumber label="Children" name="children" minimum={0} maximum={6} /> </div> </div> <GuiButton type="submit" label="Check availability" /> <p id="booking-result" role="status"> {result} </p> </form> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import '@golemui/gui-components/button';import '@golemui/gui-components/number';
@Component({ selector: 'app-number-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="booking" #form (submit)="onSubmit($event, form)"> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Adults" name="adults" minimum="1" maximum="8" required ></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Children" name="children" minimum="0" maximum="6" ></gui-number> </div> </div> <gui-button type="submit" label="Check availability"></gui-button> <p id="booking-result" role="status">{{ result() }}</p> </form> `,})export class NumberNativeFormComponent { result = signal('');
onSubmit(event: SubmitEvent, form: HTMLFormElement) { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); this.result.set(`Submitted ${JSON.stringify(data)}`); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/button';import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';
const result = ref('');
function onSubmit(event: SubmitEvent) { if (!(event.currentTarget instanceof HTMLFormElement)) return; const data = Object.fromEntries(new FormData(event.currentTarget)); result.value = `Submitted ${JSON.stringify(data)}`;}</script>
<template> <form id="booking" @submit.prevent="onSubmit"> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Adults" name="adults" :minimum="1" :maximum="8" :required="true" ></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Children" name="children" :minimum="0" :maximum="6" ></gui-number> </div> </div> <gui-button type="submit" label="Check availability"></gui-button> <p id="booking-result" role="status">{{ result }}</p> </form></template>import { LitElement, html } from 'lit';import { customElement, query, state } from 'lit/decorators.js';import '@golemui/gui-components/button';import '@golemui/gui-components/number';import '@golemui/gui-components/index.css';
@customElement('number-native-form')export class NumberNativeForm extends LitElement { @query('#booking') private form!: HTMLFormElement; @state() private result = '';
protected override createRenderRoot() { return this; }
private onSubmit(event: SubmitEvent) { event.preventDefault(); const data = Object.fromEntries(new FormData(this.form)); this.result = `Submitted ${JSON.stringify(data)}`; }
override render() { return html` <form id="booking" @submit=${this.onSubmit}> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Adults" name="adults" minimum="1" maximum="8" required ></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Children" name="children" minimum="0" maximum="6" ></gui-number> </div> </div> <gui-button type="submit" label="Check availability"></gui-button> <p id="booking-result" role="status">${this.result}</p> </form> `; }}<form id="booking"> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-number label="Adults" name="adults" minimum="1" maximum="8" required></gui-number> </div> <div class="gui-grid__cell"> <gui-number label="Children" name="children" minimum="0" maximum="6"></gui-number> </div> </div> <gui-button type="submit" label="Check availability"></gui-button> <p id="booking-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#booking');
form.addEventListener('submit', (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); document.querySelector('#booking-result').textContent = `Submitted ${JSON.stringify(data)}`; });</script>How the elements submit, validate and reset is in Native forms.
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<gui-number> renders into the light DOM, so you can style every part with plain CSS. These are
the parts and their public classes:
<gui-number class="gui-field"> <label class="gui-label" for="guests" id="guests_label"> <span class="gui-label__text">Guests</span> </label> <div class="gui-widget"> <input type="number" inputmode="decimal" id="guests" class="gui-widget-input" step="any" aria-valuemin="1" aria-valuemax="12" style="min-width: 23px;" /> <span class="gui-number__decoration"> <span class="gui-caret gui-caret--up" aria-hidden="true"></span> <span class="gui-caret" aria-hidden="true"></span> </span> </div> <div class="gui-validator gui-validator--empty" id="guests_errors" role="alert" ></div></gui-number>| Class | Part |
|---|---|
gui-field | The element itself. Lines its parts up with the fields next to it in a gui-grid row. |
gui-label | The label, with the text and the hint. |
gui-label__text | The label text and the required marker. |
gui-widget-hint | The hint. |
gui-widget | The box around the number and the carets: it draws the border. |
gui-widget-input | The <input>. |
gui-number__decoration | The two carets at the end of the field. They are decorative: the arrow keys step. |
gui-caret | One caret. gui-caret--up turns it up. See Carets. |
gui-validator | The errors container. It is always there, so the alert role announces new errors. |
gui-validator--empty | On the errors container while there are no errors. |
gui-validator__error | One error message. |
The element sets the input’s width in an inline style. The ids derive from uid, or from a
generated id such as gui-number-3 when you don’t set one. Select by class or by tag, not by id.
Tokens
Section titled “Tokens”The field reads these design tokens. Set them on :root to change every field, or on any element
to change the fields inside it:
| Token | What it styles |
|---|---|
--gui-widget-size | The height of the field. |
--gui-radius-md | The corner radius. |
--gui-bg-surface | The field’s background. |
--gui-border-default | The border. |
--gui-text-default | The number, the label and the carets. |
--gui-font-family | The font. |
--gui-font-sm | The text size of the label. |
--gui-font-xs | The text size of the hint and the errors. |
--gui-text-secondary | The hint. |
--gui-border-focus | The border while focused. |
--gui-shadow-focus | The focus ring. |
--gui-intent-error | The border and the messages when there are errors. |
--gui-shadow-focus-error | The focus ring when there are errors. |
--gui-bg-disabled | The background when disabled. |
--gui-text-disabled | The number when disabled. |
--gui-space-1 | The space between the label, hint and errors, and the carets’ inset. |
--gui-caret-shape, --gui-caret-image | The carets, see Carets. |
The number is 20 pixels, in tabular figures, whatever the tokens. To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.
Accessibility
Section titled “Accessibility”- Name. The
labelnames the field. Without one the field has no accessible name. - Role. The input is a native number input, which screen readers announce as a spin button
with its value.
minimumandmaximumbecome itsaria-valueminandaria-valuemax. - Description. The
hintis the field’s description, througharia-describedby. - Errors. While errors show, the input has
aria-invalid="true"and points at them witharia-errormessage, and screen readers announce new errors as they appear. A number out of bounds doesn’t set it: onlyerrorsdoes. - Required.
requiredsetsaria-required; the visual marker is hidden from screen readers. - Carets. The carets are hidden from assistive technology and don’t respond to clicks; the arrow keys step.
- Keyboard on phones.
inputmode="decimal"opens a keypad with a decimal point.
| Key | Action |
|---|---|
| Tab | Moves focus to the field, and away from it. |
| ↑ | Adds step, up to maximum, and fires gui-input and gui-change. |
| ↓ | Takes away step, down to minimum, and fires gui-input and gui-change. |
| Enter | Commits a typed number, as leaving the field does, and fires gui-change. |
| Number keys | Type digits, a decimal separator, a sign or an exponent. Other characters are dropped. |
What every element shares, such as translating the browser messages, is in Accessibility.