Number manipulation utilities including precision control, percentage calculations, value clamping, and step-based rounding. These utilities are specifically designed for UI controls like sliders, numeric inputs, progress bars, and other interactive components that require precise mathematical operations.
Utilities for controlling number precision and decimal places.
/**
* Converts a number to a string with specified precision
* @param value - The number to format
* @param precision - Number of decimal places (optional)
* @returns String representation with specified precision
*/
function toPrecision(value: number, precision?: number): string;
/**
* Counts the number of decimal places in a number
* @param value - The number to analyze
* @returns Number of decimal places
*/
function countDecimalPlaces(value: number): number;Usage Examples:
import { toPrecision, countDecimalPlaces } from "@chakra-ui/utils";
// Number formatting for display
const userInput = 3.14159265359;
const formatted = toPrecision(userInput, 2); // "3.14"
const scientific = toPrecision(0.000123, 3); // "1.23e-4"
// Decimal place counting for validation
const decimals1 = countDecimalPlaces(3.14159); // 5
const decimals2 = countDecimalPlaces(100); // 0
const decimals3 = countDecimalPlaces(0.5); // 1
// Slider component with precision control
interface SliderProps {
min: number;
max: number;
step?: number;
precision?: number;
value: number;
onChange: (value: number) => void;
}
function PrecisionSlider({ min, max, step = 1, precision, value, onChange }: SliderProps) {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const rawValue = parseFloat(event.target.value);
const precisionToUse = precision ?? countDecimalPlaces(step);
const formattedValue = parseFloat(toPrecision(rawValue, precisionToUse));
onChange(formattedValue);
};
return (
<div className="slider">
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={handleChange}
/>
<span className="slider-value">
{toPrecision(value, precision)}
</span>
</div>
);
}
// Currency formatter with precision
function formatCurrency(amount: number, currency = "USD"): string {
const precision = currency === "JPY" ? 0 : 2; // Japanese Yen has no decimals
const formatted = toPrecision(amount, precision);
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
minimumFractionDigits: precision,
maximumFractionDigits: precision
}).format(parseFloat(formatted));
}Utilities for constraining values within specified ranges.
/**
* Clamps a value between minimum and maximum bounds
* @param value - The value to clamp
* @param min - Minimum allowed value
* @param max - Maximum allowed value
* @returns Clamped value within the specified range
*/
function clampValue(value: number, min: number, max: number): number;Usage Examples:
import { clampValue } from "@chakra-ui/utils";
// Basic clamping
const clamped1 = clampValue(150, 0, 100); // 100 (clamped to max)
const clamped2 = clampValue(-10, 0, 100); // 0 (clamped to min)
const clamped3 = clampValue(50, 0, 100); // 50 (within range)
// Progress bar component
interface ProgressBarProps {
value: number;
min?: number;
max?: number;
className?: string;
}
function ProgressBar({ value, min = 0, max = 100, className }: ProgressBarProps) {
const clampedValue = clampValue(value, min, max);
const percentage = ((clampedValue - min) / (max - min)) * 100;
return (
<div className={cx("progress-bar", className)}>
<div
className="progress-bar__fill"
style={{ width: `${percentage}%` }}
role="progressbar"
aria-valuenow={clampedValue}
aria-valuemin={min}
aria-valuemax={max}
/>
</div>
);
}
// Volume control with clamping
function useVolumeControl(initialVolume = 50) {
const [volume, setVolume] = React.useState(clampValue(initialVolume, 0, 100));
const increaseVolume = (amount = 10) => {
setVolume(prev => clampValue(prev + amount, 0, 100));
};
const decreaseVolume = (amount = 10) => {
setVolume(prev => clampValue(prev - amount, 0, 100));
};
const setVolumeDirectly = (newVolume: number) => {
setVolume(clampValue(newVolume, 0, 100));
};
return { volume, increaseVolume, decreaseVolume, setVolumeDirectly };
}
// Color component validation
interface RGBColor {
r: number;
g: number;
b: number;
}
function createRGBColor(r: number, g: number, b: number): RGBColor {
return {
r: clampValue(Math.round(r), 0, 255),
g: clampValue(Math.round(g), 0, 255),
b: clampValue(Math.round(b), 0, 255)
};
}
function rgbToHex({ r, g, b }: RGBColor): string {
const toHex = (value: number) => clampValue(value, 0, 255).toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}Utilities for converting between values and percentages within ranges.
/**
* Converts a value to a percentage within a given range
* @param value - The value to convert
* @param min - Minimum value of the range
* @param max - Maximum value of the range
* @returns Percentage (0-100) representing the value's position in range
*/
function valueToPercent(value: number, min: number, max: number): number;
/**
* Converts a percentage to a value within a given range
* @param percent - Percentage as decimal (0-1) to convert
* @param min - Minimum value of the range
* @param max - Maximum value of the range
* @returns Value corresponding to the percentage within the range
*/
function percentToValue(percent: number, min: number, max: number): number;Usage Examples:
import { valueToPercent, percentToValue, clampValue } from "@chakra-ui/utils";
// Basic percentage conversions
const percent1 = valueToPercent(25, 0, 100); // 25%
const percent2 = valueToPercent(5, 0, 10); // 50%
const percent3 = valueToPercent(75, 50, 100); // 50%
const value1 = percentToValue(0.5, 0, 100); // 50
const value2 = percentToValue(0.25, 0, 200); // 50
const value3 = percentToValue(0.75, 100, 200); // 175
// Range input component
interface RangeInputProps {
min: number;
max: number;
value: number;
onChange: (value: number) => void;
showPercentage?: boolean;
}
function RangeInput({ min, max, value, onChange, showPercentage }: RangeInputProps) {
const percentage = valueToPercent(value, min, max);
const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = parseFloat(event.target.value);
onChange(newValue);
};
const handlePercentageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newPercentage = parseFloat(event.target.value);
const newValue = percentToValue(newPercentage / 100, min, max); // Convert 0-100 to 0-1
onChange(clampValue(newValue, min, max));
};
return (
<div className="range-input">
<input
type="range"
min={min}
max={max}
value={value}
onChange={handleSliderChange}
className="range-input__slider"
/>
<div className="range-input__displays">
<input
type="number"
value={value}
onChange={(e) => onChange(clampValue(parseFloat(e.target.value) || min, min, max))}
className="range-input__value"
/>
{showPercentage && (
<input
type="number"
value={Math.round(percentage)}
onChange={handlePercentageChange}
min={0}
max={100}
className="range-input__percentage"
/>
)}
</div>
</div>
);
}
// Loading progress indicator
interface LoadingProgressProps {
current: number;
total: number;
showText?: boolean;
}
function LoadingProgress({ current, total, showText = true }: LoadingProgressProps) {
const percentage = valueToPercent(current, 0, total);
const clampedPercentage = clampValue(percentage, 0, 100);
return (
<div className="loading-progress">
<div className="loading-progress__bar">
<div
className="loading-progress__fill"
style={{ width: `${clampedPercentage}%` }}
/>
</div>
{showText && (
<span className="loading-progress__text">
{Math.round(clampedPercentage)}% ({current}/{total})
</span>
)}
</div>
);
}
// Battery level indicator
function BatteryIndicator({ level }: { level: number }) {
const percentage = clampValue(level * 100, 0, 100); // level is 0-1
const displayValue = Math.round(percentage);
return (
<div
className={cx(
"battery",
percentage <= 20 && "battery--low",
percentage <= 5 && "battery--critical"
)}
>
<div
className="battery__fill"
style={{ height: `${percentage}%` }}
/>
<span className="battery__text">{displayValue}%</span>
</div>
);
}Utilities for rounding values to specific increments or steps.
/**
* Rounds a value to the nearest step increment from a starting point
* @param value - The value to round
* @param from - The starting point for step calculation
* @param step - The step increment to round to
* @returns String representation of the rounded value
*/
function roundValueToStep(value: number, from: number, step: number): string;Usage Examples:
import { roundValueToStep, toPrecision } from "@chakra-ui/utils";
// Basic step rounding
const rounded1 = roundValueToStep(23, 0, 5); // "25" (rounds to nearest 5)
const rounded2 = roundValueToStep(17.3, 10, 2); // "18" (rounds to nearest 2 from 10)
const rounded3 = roundValueToStep(3.14, 0, 0.5); // "3" (rounds to nearest 0.5)
// Stepper input component
interface StepperInputProps {
value: number;
min: number;
max: number;
step: number;
onChange: (value: number) => void;
}
function StepperInput({ value, min, max, step, onChange }: StepperInputProps) {
const roundedValue = parseFloat(roundValueToStep(value, min, step));
const increment = () => {
const newValue = clampValue(roundedValue + step, min, max);
const steppedValue = parseFloat(roundValueToStep(newValue, min, step));
onChange(steppedValue);
};
const decrement = () => {
const newValue = clampValue(roundedValue - step, min, max);
const steppedValue = parseFloat(roundValueToStep(newValue, min, step));
onChange(steppedValue);
};
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = parseFloat(event.target.value) || min;
const clampedValue = clampValue(inputValue, min, max);
const steppedValue = parseFloat(roundValueToStep(clampedValue, min, step));
onChange(steppedValue);
};
return (
<div className="stepper-input">
<button onClick={decrement} disabled={roundedValue <= min}>
-
</button>
<input
type="number"
value={roundedValue}
onChange={handleInputChange}
min={min}
max={max}
step={step}
/>
<button onClick={increment} disabled={roundedValue >= max}>
+
</button>
</div>
);
}
// Price rounding for e-commerce
function roundPrice(price: number, currency = "USD"): number {
const roundingMap = {
USD: 0.01, // Round to cents
EUR: 0.01, // Round to cents
JPY: 1, // Round to whole yen
GBP: 0.01 // Round to pence
};
const step = roundingMap[currency as keyof typeof roundingMap] || 0.01;
return parseFloat(roundValueToStep(price, 0, step));
}
// Grid snap functionality
interface GridSnapOptions {
gridSize: number;
origin?: { x: number; y: number };
}
function snapToGrid(
point: { x: number; y: number },
{ gridSize, origin = { x: 0, y: 0 } }: GridSnapOptions
) {
return {
x: parseFloat(roundValueToStep(point.x, origin.x, gridSize)),
y: parseFloat(roundValueToStep(point.y, origin.y, gridSize))
};
}
// Usage in draggable component
function useDraggableWithSnap(gridSize = 10) {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
const updatePosition = (newPosition: { x: number; y: number }) => {
const snappedPosition = snapToGrid(newPosition, { gridSize });
setPosition(snappedPosition);
};
return { position, updatePosition };
}
// Time rounding for scheduling
function roundToTimeIncrement(minutes: number, increment = 15): number {
return parseFloat(roundValueToStep(minutes, 0, increment));
}
function formatTimeSlot(minutes: number): string {
const roundedMinutes = roundToTimeIncrement(minutes, 15);
const hours = Math.floor(roundedMinutes / 60);
const mins = roundedMinutes % 60;
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
}
// Usage
const timeSlots = [480, 495, 510, 525]; // 8:00, 8:15, 8:30, 8:45
const formattedSlots = timeSlots.map(formatTimeSlot);
// ["08:00", "08:15", "08:30", "08:45"]