MUIUnitInput
MUIUnitInput pairs a numeric value (MUINumberInput)
with a unit picker (MUISelect) - currency, weight, temperature,
or any other unit set - rendered borderless inside one bordered pill, similar in spirit to
MUIPhoneInput's country + number pairing.
fieldName and customIds are both { unit, value } objects — one entry per
control, so each is registered independently against a flat form schema.
fieldName requires both keys; customIds keys are optional. value /
onValueChange still report the pair together as one { unit, value } object.
containerProps styles the outer pill container; dividerProps styles the
vertical divider between the two controls.
unitOptions accepts either a plain string array, or an object array read via
labelKey/valueKey — the same convention as MUISelect.
Always seed value.unit with a real option — an empty/missing unit renders
the unit Select with nothing selected rather than silently defaulting to
unitOptions[0], so what's shown always matches what you passed in.
Usage
import MUIUnitInput, { MUIUnitInputValue } from '@nish1896/mui-components/mui/unit-input';Plain string unitOptions:
const [price, setPrice] = useState<MUIUnitInputValue>({
unit: 'USD',
value: null
});
<MUIUnitInput
fieldName={{
unit: 'priceUnit',
value: 'priceAmount'
}}
value={price}
onValueChange={({ newValue }) => setPrice(newValue)}
unitOptions={['USD', 'EUR', 'GBP']}
nonNegative
maxDecimalPlaces={2}
/>Object unitOptions via labelKey/valueKey — newValue.unit is typed
'USD' | 'EUR' | 'GBP' automatically, even though price already comes
from a useState<MUIUnitInputValue<Currency>>:
type Currency = 'USD' | 'EUR' | 'GBP';
type CurrencyOption = { code: Currency; label: string };
const currencyOptions: CurrencyOption[] = [
{ code: 'USD', label: 'US Dollar' },
{ code: 'EUR', label: 'Euro' },
{ code: 'GBP', label: 'British Pound' }
];
<MUIUnitInput
fieldName={{
unit: 'priceUnit',
value: 'priceAmount'
}}
value={price}
onValueChange={({ newValue }) => setPrice(newValue)}
unitOptions={currencyOptions}
labelKey="label"
valueKey="code"
unitPosition="start"
/>API
Props marked with * are required.
| Name | Type | Description |
|---|---|---|
fieldName* | { unit: string; value: string; } | Name/path of the field's two underlying controls, kept separate (rather than one combined fieldName) so each can be registered independently against a flat form schema — e.g. { unit: 'weightUnit', value: 'weight' }. |
unitOptions* | Option[] | Units selectable from the dropdown — a plain string array (e.g. ['USD', 'EUR', 'GBP'] or ['kg', 'lb'], a string-literal union/enum's values for literal-union safety), or an object array read via labelKey/valueKey, same convention as MUISelect. |
labelKey | string | Object key used to read the display label from each option, when unitOptions is an array of objects. |
valueKey | string | Object key used to derive the unit value from each option, when unitOptions is an array of objects. |
value | { unit: string; value: number | null; } | Current value of the field. unit and value are always reported together through onValueChange, even though they are two controls. |
onValueChange* | ({ newValue, event }) => void | Called whenever either the unit or the value changes. Always receives the full { unit, value } value. |
min | number | Lower bound for the value. nonNegative sets the default value to 0 unless overridden. |
max | number | Upper bound for the value. |
onlyIntegers | boolean | When true, decimal input is not allowed. Cannot be combined with maxDecimalPlaces. |
nonNegative | boolean | When true, negative values cannot be entered. |
maxDecimalPlaces | number | Maximum number of decimal places accepted while typing. |
stepAmount | number | Amount the value changes on Arrow Up/Down key presses. |
renderValue | (value: number | null) => string | Formats the numeric value for display, e.g. value => value?.toLocaleString() ?? '' for thousands separators or a currency prefix. Forwarded to the internal MUINumberInput: the raw number is shown while the value input is focused, the formatted string when it is not, and value.value stays a real number | null. |
unitPosition | 'start' | 'end' | Which side the unit Select renders on relative to the value input.Default: 'end' |
unitWidth | ResponsiveStyleValue<string> | Width of the unit Select as a CSS flex-basis value, the value input fills the rest. Accepts a single value (e.g. 100px or '30%') or a responsive breakpoint object, e.g. { xs: '40%', md: '30%' }.Both segments have a default minWidth: 50px so neither collapses. When omitted, the unit Select sizes to its content. |
unitSelectProps | MUISelectProps | Props forwarded to the internal unit MUISelect. |
valueInputProps | MUINumberInputProps | Props forwarded to the internal MUINumberInput determining the value of the field. |
containerProps | BoxProps | Props forwarded to the outer pill container wrapping the value input and unit Select. containerProps.sx is merged with the component's own base pill styles rather than replacing them, and accepts any sx form — object, array, or function. |
dividerProps | BoxProps | Props forwarded to the vertical divider between the value input and the unit Select (a plain Box with borderLeft/borderColor). |
label | ReactNode | Custom field label. Defaults to a humanized version of fieldName.value. |
showLabelAboveFormField | boolean | When true, renders the field label above the form field in the FormLabel component, instead of inside or beside it. |
formLabelProps | FormLabelProps | FormLabelProps forwarded to the internal FormLabel. The id is managed by the component. Multiple fields can be configured using the ConfigProvider component. |
hideLabel | boolean | When true, hides the rendered field label while preserving accessible labeling where possible. |
required | boolean | Indicates that the field is mandatory by adding an asterisk symbol (*) to the form label and setting the relevant accessibility attributes. |
placeholder | string | Placeholder shown in the empty value input. |
disabled | boolean | When true, disables the field and associated controls. |
errorMessage | string | string[] | Validation error for the field — pass a single message string, or a string[] when the field can fail multiple rules at once (every message is shown together). A non-empty string or array puts the field in an error state; undefined/''/[] clear it.Normalize your form library's error shape to this at the call site (e.g. an RHF FieldError via its .message). Use renderError to customize how the message(s) are rendered. |
renderError | (errors: string[]) => ReactNode | Custom renderer for the resolved error message(s), called only when the field is in an error state. Always receives a string[] — use errors[0] for the common single-message case, or map over errors when a field fails several rules. By default a single message renders as text and multiple messages render on separate lines. |
hideErrorMessage | boolean | If true, hides the error message text while keeping the field in an error state. |
helperText | ReactNode | Content displayed in the FormHelperText component below the field when there is no visible validation error. |
formHelperTextProps | FormHelperTextProps | FormHelperTextProps forwarded to the internal FormHelperText. The id is managed by the component. Multiple fields can be configured using the ConfigProvider component. |
customIds | { unit?: CustomComponentIds; value?: CustomComponentIds } | Custom ids for the unit and value controls respectively. |
Playground
Driven by plain React state — three fields, all required and validated on submit:
- Currency — object
unitOptionsvialabelKey/valueKey, unit on the left (unitPosition="start"), withrenderOptionLabelandgetOptionDisabledforwarded throughunitSelectProps;nonNegativewithmaxDecimalPlaces={2}, andrenderValue. - Weight — plain string
unitOptions, responsiveunitWidth({ xs: '40%', md: '30%' }),onlyIntegersandmax={150}. - Temperature —
containerProps/dividerPropsandsxoverrides on the internal quantity input and unitSelect.
Unit-Input integration with react-hook-form and zod validation.