Number Input

Installation

Add the following Soul components

The number-input component uses the field-error and label components. Make sure you have added them to your project.

Install the following dependencies

npm install clsx lucide-react

Copy and paste the following code into your project

form/number-input/index.tsx

'use client';import { clsx } from 'clsx';import { Minus, Plus } from 'lucide-react';import * as React from 'react';import { FieldError } from '@/vibes/soul/form/field-error';import { Label } from '@/vibes/soul/form/label';/** * This component supports various CSS variables for theming. Here's a comprehensive list, along * with their default values: * * ```css *  :root { *   --number-input-focus: hsl(var(--primary)); *   --number-input-light-background: hsl(var(--background)); *   --number-input-light-text: hsl(var(--foreground)); *   --number-input-light-icon: hsl(var(--contrast-300)); *   --number-input-light-icon-hover: hsl(var(--foreground)); *   --number-input-light-button-background: hsl(var(--background)); *   --number-input-light-button-background-hover: hsl(var(--contrast-100) / 50%); *   --number-input-dark-background: hsl(var(--background)); *   --number-input-dark-text: hsl(var(--background)); *   --number-input-dark-icon: hsl(var(--contrast-300)); *   --number-input-dark-icon-hover: hsl(var(--background)); *   --number-input-dark-button-background: hsl(var(--foreground)); *   --number-input-dark-button-background-hover: hsl(var(--contrast-500) / 50%); *  } * ``` */export const NumberInput = React.forwardRef<  React.ComponentRef<'input'>,  Omit<React.ComponentPropsWithoutRef<'input'>, 'id'> & {    label?: string;    errors?: string[];    decrementLabel?: string;    incrementLabel?: string;    colorScheme?: 'light' | 'dark';  }>(  (    {      label,      className,      required,      errors,      decrementLabel,      incrementLabel,      disabled = false,      colorScheme = 'light',      ...rest    },    ref,  ) => {    const id = React.useId();    return (      <div className={clsx('space-y-2', className)}>        {label != null && label !== '' && (          <Label colorScheme={colorScheme} htmlFor={id}>            {label}          </Label>        )}        <div          className={clsx(            'inline-flex items-center rounded-lg border',            {              light: 'bg-[var(--number-input-light-background,hsl(var(--background)))]',              dark: 'bg-[var(--number-input-dark-background,hsl(var(--foreground)))]',            }[colorScheme],          )}        >          <button            aria-label={decrementLabel}            className={clsx(              'group rounded-l-lg p-3.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--number-input-focus,hsl(var(--primary)))] disabled:cursor-not-allowed disabled:opacity-30',              {                light:                  'bg-[var(--number-input-light-button-background,hsl(var(--background)))] hover:bg-[var(--number-input-light-button-background-hover,hsl(var(--contrast-100)/50%))]',                dark: 'bg-[var(--number-input-dark-button-background,hsl(var(--foreground)))] hover:bg-[var(--number-input-dark-button-background-hover,hsl(var(--contrast-500)/50%))]',              }[colorScheme],            )}            disabled={disabled}            onClick={(e) => {              e.preventDefault();              const input = e.currentTarget.parentElement?.querySelector('input');              input?.stepDown();              input?.dispatchEvent(new InputEvent('change', { bubbles: true, cancelable: true }));            }}          >            <Minus              className={clsx(                'transition-colors duration-300',                {                  light:                    'text-[var(--number-input-light-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--number-input-light-icon-hover,hsl(var(--foreground)))]',                  dark: 'text-[var(--number-input-dark-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--number-input-dark-icon-hover,hsl(var(--background)))]',                }[colorScheme],              )}              size={18}              strokeWidth={1.5}            />          </button>          <input            {...rest}            className={clsx(              'w-8 flex-1 select-none justify-center bg-transparent text-center [appearance:textfield] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-30 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',              {                light: 'text-[var(--number-input-light-text,hsl(var(--foreground)))]',                dark: 'text-[var(--number-input-dark-text,hsl(var(--background)))]',              }[colorScheme],            )}            disabled={disabled}            id={id}            ref={ref}            type="number"          />          <button            aria-label={incrementLabel}            className={clsx(              'group rounded-r-lg p-3.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--number-input-focus,hsl(var(--primary)))] disabled:cursor-not-allowed disabled:opacity-30',              {                light:                  'bg-[var(--number-input-light-button-background,hsl(var(--background)))] hover:bg-[var(--number-input-light-button-background-hover,hsl(var(--contrast-100)/50%))]',                dark: 'bg-[var(--number-input-dark-button-background,hsl(var(--foreground)))] hover:bg-[var(--number-input-dark-button-background-hover,hsl(var(--contrast-500)/50%))]',              }[colorScheme],            )}            disabled={disabled}            onClick={(e) => {              e.preventDefault();              const input = e.currentTarget.parentElement?.querySelector('input');              input?.stepUp();              input?.dispatchEvent(new InputEvent('change', { bubbles: true, cancelable: true }));            }}          >            <Plus              className={clsx(                'transition-colors duration-300',                {                  light:                    'text-[var(--number-input-light-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--number-input-light-icon-hover,hsl(var(--foreground)))]',                  dark: 'text-[var(--number-input-dark-icon,hsl(var(--contrast-300)))] group-hover:text-[var(--number-input-dark-icon-hover,hsl(var(--background)))]',                }[colorScheme],              )}              size={18}              strokeWidth={1.5}            />          </button>        </div>        {errors?.map((error) => <FieldError key={error}>{error}</FieldError>)}      </div>    );  },);NumberInput.displayName = 'NumberInput';

Usage

import { NumberInput } from '@/vibes/soul/form/number-input';function Usage() {  return (    <NumberInput errors={['Please select a quantity.']} min={0} max={5} label="Quantity" />  );}

API Reference

NumberInputProps

PropTypeDefault
className
string
label
string
errors
string[]
decrementLabel
string
incrementLabel
string
colorScheme
'light' | 'dark'
'light'

CSS Variables

This component supports various CSS variables for theming. Here's a comprehensive list.

:root {  --number-input-focus: hsl(var(--primary));  --number-input-light-background: hsl(var(--background));  --number-input-light-text: hsl(var(--foreground));  --number-input-light-icon: hsl(var(--contrast-300));  --number-input-light-icon-hover: hsl(var(--foreground));  --number-input-light-button-background: hsl(var(--background));  --number-input-light-button-background-hover: hsl(var(--contrast-100) / 50%);  --number-input-dark-background: hsl(var(--background));  --number-input-dark-text: hsl(var(--background));  --number-input-dark-icon: hsl(var(--contrast-300));  --number-input-dark-icon-hover: hsl(var(--background));  --number-input-dark-button-background: hsl(var(--foreground));  --number-input-dark-button-background-hover: hsl(var(--contrast-500) / 50%);}