Motion Number Input
Motion Number Input is a React and Tailwind CSS form component from UI Layouts, licensed MIT. Copy it and paste it into your project.
UI Layouts
MIT
forms
What it is
Motion Number Input is a React and Tailwind CSS form component from UI Layouts, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component comes from UI Layouts and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Motion Number Input
'use client';
import { cn } from '@/lib/utils';
import NumberFlow from '@number-flow/react';
import { Minus, Plus } from 'lucide-react';
import * as React from 'react';
type Props = {
value?: number;
min?: number;
max?: number;
onChange?: (value: number) => void;
};
export function Input({
value = 0,
min = Number.NEGATIVE_INFINITY,
max = Number.POSITIVE_INFINITY,
onChange,
}: Props) {
const defaultValue = React.useRef(value);
const inputRef = React.useRef<HTMLInputElement>(null);
const [animated, setAnimated] = React.useState(true);
// Hide the caret during transitions so you can't see it shifting around:
const [showCaret, setShowCaret] = React.useState(true);
const handleInput: React.ChangeEventHandler<HTMLInputElement> = ({ currentTarget: el }) => {
setAnimated(false);
if (el.value === '') {
onChange?.(defaultValue.current);
return;
}
const num = Number.parseInt(el.value);
if (isNaN(num) || (min != null && num < min) || (max != null && num > max)) {
// Revert input's value:
el.value = String(value);
} else {
// Manually update value in case they e.g. start with a "0" or end with a "."
// which won't trigger a DOM update (because the number is the same):
el.value = String(num);
onChange?.(num);
}
};
const handlePointerDown = (diff: number) => (event: React.PointerEvent<HTMLButtonElement>) => {
setAnimated(true);
if (event.pointerType === 'mouse') {
event?.preventDefault();
inputRef.current?.focus();
}
const newVal = Math.min(Math.max(value + diff, min), max);
onChange?.(newVal);
};
return (
<div className='group flex items-stretch rounded-md text-3xl font-semibold border w-fit mx-auto dark:bg-neutral-950 bg-neutral-50'>
<button
aria-hidden
tabIndex={-1}
className='flex items-center pl-[.5em] pr-[.325em]'
disabled={min != null && value <= min}
onPointerDown={handlePointerDown(-1)}
>
<Minus className='size-4' absoluteStrokeWidth strokeWidth={3.5} />
</button>
<div className="relative grid items-center justify-items-center text-center [grid-template-areas:'overlap'] *:[grid-area:overlap]">
<input
ref={inputRef}
className={cn(
showCaret ? 'caret-primary' : 'caret-transparent',
'spin-hide w-[1.5em] bg-transparent py-2 text-center font-[inherit] text-transparent outline-hidden appearance-none'
)}
// Make sure to disable kerning, to match NumberFlow:
style={{ fontKerning: 'none' }}
type='number'
min={min}
step={1}
autoComplete='off'
inputMode='numeric'
max={max}
value={value}
onInput={handleInput}
/>
<NumberFlow
value={value}
format={{ useGrouping: false }}
aria-hidden
animated={animated}
onAnimationsStart={() => setShowCaret(false)}
onAnimationsFinish={() => setShowCaret(true)}
className='pointer-events-none'
willChange
/>
</div>
<button
aria-hidden
tabIndex={-1}
className='flex items-center pl-[.325em] pr-[.5em]'
disabled={max != null && value >= max}
onPointerDown={handlePointerDown(1)}
>
<Plus className='size-4' absoluteStrokeWidth strokeWidth={3.5} />
</button>
</div>
);
}
export default function MotionNumberInput() {
const [value, setValue] = React.useState(0);
return (
<>
<Input value={value} min={0} max={99} onChange={setValue} />
</>
);
}