COMPONENTS/INPUTS
Number field.
Precision, one step at a time.
In practice
Set min, max and step on NumberField. Label the input and increment/decrement buttons.
Installation
Install Coal once. Components and TypeScript declarations are included. React 19 and React DOM 19 are peer dependencies.
Terminal · local package
npm install http://localhost:3100/downloads/chlohal-coal-ui-0.5.0.tgzThis installs the local package. It is not yet published on npm. See installation for stylesheet setup and integration.
Usage
example.tsx
"use client";
import * as React from "react";
import "@chlohal/coal-ui/styles.css";
import { Label, NumberField, NumberFieldGroup, NumberFieldInput, NumberFieldIncrement, NumberFieldDecrement } from "@chlohal/coal-ui";
export default function Example() {
const id = React.useId();
return (<NumberField defaultValue={3} min={1} max={12}>
<Label htmlFor={id}>Team members</Label>
<NumberFieldGroup>
<NumberFieldDecrement aria-label="Remove member">
−
</NumberFieldDecrement>
<NumberFieldInput id={id}/>
<NumberFieldIncrement aria-label="Add member">
+
</NumberFieldIncrement>
</NumberFieldGroup>
</NumberField>);
}
Source
The original Coal implementation. Use the package export to integrate it.
packages/react/src/number-field.tsx
"use client";
import * as React from "react";
import {
cn,
useValue,
useRequired,
Action,
type ActionProps,
} from "./internal.js";
const Context = React.createContext<
| {
value: number;
set: (v: number) => void;
min?: number;
max?: number;
step: number;
disabled?: boolean;
name?: string;
}
| undefined
>(undefined);
export type NumberFieldProps = Omit<
React.ComponentPropsWithRef<"div">,
"defaultValue"
> & {
value?: number;
defaultValue?: number;
onValueChange?: (value: number) => void;
min?: number;
max?: number;
step?: number;
disabled?: boolean;
name?: string;
};
export function NumberField({
value,
defaultValue = 0,
onValueChange,
min,
max,
step = 1,
disabled,
name,
children,
className,
...props
}: NumberFieldProps) {
const [v, set] = useValue(value, defaultValue, onValueChange);
return (
<Context.Provider
value={{
value: v,
set: (n) =>
set(Math.min(max ?? Infinity, Math.max(min ?? -Infinity, n))),
min,
max,
step,
disabled,
name,
}}
>
<div className={cn("coal-field", className)} {...props}>
{children}
</div>
</Context.Provider>
);
}
export function NumberFieldGroup({
className,
...props
}: React.ComponentPropsWithRef<"div">) {
return <div className={cn("coal-number-group", className)} {...props} />;
}
export function NumberFieldInput({
onChange,
onBlur,
className,
...props
}: React.ComponentPropsWithRef<"input">) {
const c = useRequired(Context, "NumberFieldInput");
const [draft, setDraft] = React.useState(String(c.value));
React.useEffect(() => setDraft(String(c.value)), [c.value]);
return (
<input
type="number"
value={draft}
min={c.min}
max={c.max}
step={c.step}
name={c.name}
disabled={c.disabled}
className={cn("coal-number-input", className)}
{...props}
onBlur={(e) => {
onBlur?.(e);
setDraft(String(c.value));
}}
onChange={(e) => {
onChange?.(e);
if (!e.defaultPrevented) setDraft(e.target.value);
if (!e.defaultPrevented && Number.isFinite(e.target.valueAsNumber))
c.set(e.target.valueAsNumber);
}}
/>
);
}
function StepButton({
direction,
onClick,
className,
...props
}: ActionProps & { direction: number }) {
const c = useRequired(Context, "NumberField step");
return (
<Action
className={cn("coal-number-step", className)}
disabled={
c.disabled ||
(direction > 0
? c.value >= (c.max ?? Infinity)
: c.value <= (c.min ?? -Infinity))
}
{...props}
onClick={(e) => {
onClick?.(e);
if (!e.defaultPrevented)
c.set(Number((c.value + direction * c.step).toFixed(12)));
}}
/>
);
}
export function NumberFieldIncrement(props: ActionProps) {
return <StepButton direction={1} {...props} />;
}
export function NumberFieldDecrement(props: ActionProps) {
return <StepButton direction={-1} {...props} />;
}
Accessibility checklist
- Provide an accessible name for controls, including icon-only buttons.
- Check keyboard navigation, visible focus and disabled states in your application.
- Do not rely on color alone to communicate status.