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
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>
);
}
The current value of the counter.
onChange
(event: ChangeEvent) => void
required
Callback executed when the user manually changes the input value.
Callback executed when the increment button is pressed.
Callback executed when the decrement button is pressed.
Label text displayed above the counter input group.
size
'small' | 'medium'
default:"'small'"
Defines the height of the counter component.
The minimum valid value for the component. The decrement button is disabled when this value is reached.
The maximum valid value for the component. The increment button is disabled when this value is reached.
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
Related Components
- TextField - For general numeric input
- Slider - For selecting from a range visually
- Button - Used internally for increment/decrement