Skip to main content

Counter

The Counter component provides an intuitive way for users to adjust numeric values using increment and decrement buttons. It’s ideal for quantity selectors, rating adjustments, and any scenario requiring precise numeric input.

Installation

npm install @naturacosmeticos/natds-web

Usage

import { Counter } from '@naturacosmeticos/natds-web';
import { useState } from 'react';

function BasicCounter() {
  const [value, setValue] = useState(0);

  const handleIncrement = () => setValue(value + 1);
  const handleDecrement = () => setValue(value - 1);
  const handleChange = (event) => setValue(Number(event.target.value));

  return (
    <Counter
      value={value}
      onIncrement={handleIncrement}
      onDecrement={handleDecrement}
      onChange={handleChange}
    />
  );
}

Counter Variants

Small Size

Compact counter for tight spaces:
function SmallCounter() {
  const [value, setValue] = useState(0);

  return (
    <Counter
      size="small"
      value={value}
      onIncrement={() => setValue(value + 1)}
      onDecrement={() => setValue(value - 1)}
      onChange={(e) => setValue(Number(e.target.value))}
    />
  );
}

Medium Size

Larger counter for better visibility:
<Counter
  size="medium"
  value={value}
  onIncrement={handleIncrement}
  onDecrement={handleDecrement}
  onChange={handleChange}
/>

With Label

Add a descriptive label above the counter:
<Counter
  label="Quantity"
  value={quantity}
  onIncrement={handleIncrement}
  onDecrement={handleDecrement}
  onChange={handleChange}
/>

With Min and Max Values

Constraint the range of valid values:
<Counter
  value={value}
  minValue={0}
  maxValue={10}
  onIncrement={handleIncrement}
  onDecrement={handleDecrement}
  onChange={handleChange}
/>

Read-Only Counter

Display-only counter without interaction:
<Counter
  value={value}
  readOnly={true}
  onIncrement={() => {}}
  onDecrement={() => {}}
  onChange={() => {}}
/>

Shopping Cart Example

function ShoppingCartItem({ item }) {
  const [quantity, setQuantity] = useState(item.quantity);

  const handleIncrement = () => {
    const newQuantity = quantity + 1;
    setQuantity(newQuantity);
    updateCart(item.id, newQuantity);
  };

  const handleDecrement = () => {
    const newQuantity = Math.max(1, quantity - 1);
    setQuantity(newQuantity);
    updateCart(item.id, newQuantity);
  };

  const handleChange = (event) => {
    const newQuantity = Number(event.target.value);
    if (newQuantity >= 1 && newQuantity <= 99) {
      setQuantity(newQuantity);
      updateCart(item.id, newQuantity);
    }
  };

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
      <img src={item.image} alt={item.name} style={{ width: 60 }} />
      <div style={{ flex: 1 }}>
        <h3>{item.name}</h3>
        <p>${item.price}</p>
      </div>
      <Counter
        label="Quantity"
        value={quantity}
        minValue={1}
        maxValue={99}
        onIncrement={handleIncrement}
        onDecrement={handleDecrement}
        onChange={handleChange}
      />
      <p>${(item.price * quantity).toFixed(2)}</p>
    </div>
  );
}

Form Integration

function ProductForm() {
  const [formData, setFormData] = useState({
    productName: '',
    quantity: 1,
    priority: 5
  });

  const updateQuantity = (newValue) => {
    setFormData(prev => ({ ...prev, quantity: newValue }));
  };

  const updatePriority = (newValue) => {
    setFormData(prev => ({ ...prev, priority: newValue }));
  };

  return (
    <form>
      <TextField
        label="Product Name"
        value={formData.productName}
        onChange={(e) => setFormData(prev => ({ ...prev, productName: e.target.value }))}
      />
      
      <Counter
        label="Quantity"
        value={formData.quantity}
        minValue={1}
        maxValue={100}
        onIncrement={() => updateQuantity(formData.quantity + 1)}
        onDecrement={() => updateQuantity(formData.quantity - 1)}
        onChange={(e) => updateQuantity(Number(e.target.value))}
      />

      <Counter
        label="Priority (1-10)"
        value={formData.priority}
        minValue={1}
        maxValue={10}
        onIncrement={() => updatePriority(formData.priority + 1)}
        onDecrement={() => updatePriority(formData.priority - 1)}
        onChange={(e) => updatePriority(Number(e.target.value))}
      />
    </form>
  );
}

Custom Step Size

function CustomStepCounter() {
  const [value, setValue] = useState(0);
  const step = 5;

  const handleIncrement = () => {
    const newValue = Math.min(value + step, 100);
    setValue(newValue);
  };

  const handleDecrement = () => {
    const newValue = Math.max(value - step, 0);
    setValue(newValue);
  };

  const handleChange = (event) => {
    const newValue = Number(event.target.value);
    // Round to nearest step
    const rounded = Math.round(newValue / step) * step;
    setValue(Math.max(0, Math.min(100, rounded)));
  };

  return (
    <Counter
      label="Volume (step: 5)"
      value={value}
      minValue={0}
      maxValue={100}
      onIncrement={handleIncrement}
      onDecrement={handleDecrement}
      onChange={handleChange}
    />
  );
}

Multiple Counters

function MultipleCounters() {
  const [counters, setCounters] = useState({
    adults: 1,
    children: 0,
    infants: 0
  });

  const updateCounter = (type, value) => {
    setCounters(prev => ({ ...prev, [type]: value }));
  };

  return (
    <div>
      <Counter
        label="Adults"
        value={counters.adults}
        minValue={1}
        maxValue={10}
        onIncrement={() => updateCounter('adults', counters.adults + 1)}
        onDecrement={() => updateCounter('adults', counters.adults - 1)}
        onChange={(e) => updateCounter('adults', Number(e.target.value))}
      />
      
      <Counter
        label="Children (2-12 years)"
        value={counters.children}
        minValue={0}
        maxValue={10}
        onIncrement={() => updateCounter('children', counters.children + 1)}
        onDecrement={() => updateCounter('children', counters.children - 1)}
        onChange={(e) => updateCounter('children', Number(e.target.value))}
      />
      
      <Counter
        label="Infants (under 2)"
        value={counters.infants}
        minValue={0}
        maxValue={5}
        onIncrement={() => updateCounter('infants', counters.infants + 1)}
        onDecrement={() => updateCounter('infants', counters.infants - 1)}
        onChange={(e) => updateCounter('infants', Number(e.target.value))}
      />
      
      <p>Total: {counters.adults + counters.children + counters.infants} guests</p>
    </div>
  );
}

Props

value
number
default:"0"
The current value of the counter.
onChange
(event: ChangeEvent) => void
required
Callback executed when the user manually changes the input value.
onIncrement
() => void
required
Callback executed when the increment button is pressed.
onDecrement
() => void
required
Callback executed when the decrement button is pressed.
label
string
Label text displayed above the counter input group.
size
'small' | 'medium'
default:"'small'"
Defines the height of the counter component.
minValue
number
default:"0"
The minimum valid value for the component. The decrement button is disabled when this value is reached.
maxValue
number
default:"99"
The maximum valid value for the component. The increment button is disabled when this value is reached.
readOnly
boolean
default:"false"
If true, disables all component actions (buttons and input).

Best Practices

  • Always provide all three handlers: onChange, onIncrement, and onDecrement
  • Set appropriate minValue and maxValue constraints
  • Use meaningful labels to indicate what the counter represents
  • Handle edge cases when manually typing values
  • Consider using small size for inline or space-constrained layouts
  • Validate and sanitize input values in the onChange handler
  • Disable buttons at min/max values for clear feedback

Validation

function ValidatedCounter() {
  const [value, setValue] = useState(1);
  const [error, setError] = useState('');

  const validate = (newValue) => {
    if (newValue < 1) {
      setError('Minimum value is 1');
      return false;
    }
    if (newValue > 99) {
      setError('Maximum value is 99');
      return false;
    }
    setError('');
    return true;
  };

  const handleChange = (event) => {
    const newValue = Number(event.target.value);
    if (validate(newValue)) {
      setValue(newValue);
    }
  };

  return (
    <div>
      <Counter
        value={value}
        minValue={1}
        maxValue={99}
        onIncrement={() => setValue(Math.min(value + 1, 99))}
        onDecrement={() => setValue(Math.max(value - 1, 1))}
        onChange={handleChange}
      />
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  );
}

Accessibility

  • The component uses semantic button elements
  • Buttons are automatically disabled at min/max values
  • Input field supports keyboard entry
  • Labels are properly associated with inputs
  • Consider adding aria-label to increment/decrement buttons for screen readers
  • TextField - For general numeric input
  • Slider - For selecting from a range visually
  • Button - Used internally for increment/decrement

Build docs developers (and LLMs) love