Shared TypeScript type definitions for React Spectrum components and hooks, providing common interfaces for DOM interactions, styling, accessibility, internationalization, and component behavior across the React Spectrum ecosystem
—
Comprehensive input management system including validation, value handling, form integration, and help text support for building robust form components.
Core input properties that apply to all input components.
/**
* Base input properties for all input components
*/
interface InputBase {
/** Whether the input is disabled */
isDisabled?: boolean;
/** Whether the input can be selected but not changed by the user */
isReadOnly?: boolean;
}
/**
* Base properties for inputs with text placeholders
*/
interface TextInputBase {
/** Temporary text that occupies the text input when it is empty */
placeholder?: string;
}
/**
* Spectrum-specific text input base properties
* @deprecated placeholder - Please use help text instead
*/
interface SpectrumTextInputBase {
/** Temporary text that occupies the text input when it is empty */
placeholder?: string;
}Generic value handling for controlled and uncontrolled input components.
/**
* Base value management for input components
* @template T The type of the input value
* @template C The type passed to the change handler (defaults to T)
*/
interface ValueBase<T, C = T> {
/** The current value (controlled) */
value?: T;
/** The default value (uncontrolled) */
defaultValue?: T;
/** Handler that is called when the value changes */
onChange?: (value: C) => void;
}
/**
* Range value type for inputs like sliders
* @template T The type of the range values
*/
interface RangeValue<T> {
/** The start value of the range */
start: T;
/** The end value of the range */
end: T;
}
/**
* Base properties for range inputs
* @template T The type of the range values
*/
interface RangeInputBase<T> {
/** The smallest value allowed for the input */
minValue?: T;
/** The largest value allowed for the input */
maxValue?: T;
/** The amount that the input value changes with each increment or decrement */
step?: T;
}Comprehensive validation system with multiple validation behaviors and error reporting.
/** Validation state indicator */
type ValidationState = "valid" | "invalid";
/** Validation error can be a single string or array of strings */
type ValidationError = string | string[];
/** Record of validation errors keyed by field name */
type ValidationErrors = Record<string, ValidationError>;
/**
* Function that validates a value
* @template T The type of value being validated
*/
type ValidationFunction<T> = (value: T) => ValidationError | true | null | undefined;
/**
* Core validation properties
* @template T The type of value being validated
*/
interface Validation<T = unknown> {
/** Whether user input is required on the input before form submission */
isRequired?: boolean;
/** Whether the input value is invalid */
isInvalid?: boolean;
/** @deprecated Use isInvalid instead */
validationState?: ValidationState;
/**
* Whether to use native HTML form validation to prevent form submission
* when the value is missing or invalid, or mark the field as required
* or invalid via ARIA
* @default "aria"
*/
validationBehavior?: "aria" | "native";
/**
* A function that returns an error message if a given value is invalid.
* Validation errors are displayed to the user when the form is submitted
* if validationBehavior="native". For realtime validation, use the isInvalid
* prop instead.
*/
validate?: (value: T) => ValidationError | true | null | undefined;
}
/**
* Results of input validation
*/
interface ValidationResult {
/** Whether the input value is invalid */
isInvalid: boolean;
/** The current error messages for the input if it is invalid, otherwise an empty array */
validationErrors: string[];
/** The native validation details for the input */
validationDetails: ValidityState;
}
/**
* Spectrum-specific field validation properties
* @template T The type of value being validated
*/
interface SpectrumFieldValidation<T> extends Omit<Validation<T>, "isInvalid" | "validationState"> {
/** Whether the input should display its "valid" or "invalid" visual styling */
validationState?: ValidationState;
}Support for help text, descriptions, and error messages.
/**
* Properties for help text and error messages
*/
interface HelpTextProps {
/** A description for the field. Provides a hint such as specific requirements for what to choose */
description?: ReactNode;
/** An error message for the field */
errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode);
}
/**
* Spectrum-specific help text properties
*/
interface SpectrumHelpTextProps extends HelpTextProps {
/** Whether the description is displayed with lighter text */
isDisabled?: boolean;
/** Whether an error icon is rendered */
showErrorIcon?: boolean;
}Properties for components that can be labeled.
/** Label positioning options */
type LabelPosition = "top" | "side";
/** Label alignment options */
type Alignment = "start" | "end";
/** How to indicate required fields */
type NecessityIndicator = "icon" | "label";
/**
* Basic labelable properties
*/
interface LabelableProps {
/** The content to display as the label */
label?: ReactNode;
}
/**
* Spectrum-specific labelable properties
*/
interface SpectrumLabelableProps extends LabelableProps {
/**
* The label's overall position relative to the element it is labeling
* @default "top"
*/
labelPosition?: LabelPosition;
/**
* The label's horizontal alignment relative to the element it is labeling
* @default "start"
*/
labelAlign?: Alignment;
/**
* Whether the required state should be shown as an icon or text
* @default "icon"
*/
necessityIndicator?: NecessityIndicator;
/** Whether the label is labeling a required field or group */
isRequired?: boolean;
/** A ContextualHelp element to place next to the label */
contextualHelp?: ReactNode;
}Support for items that can be removed.
/**
* Properties for removable items
* @template T The type of the item value
* @template R The return type of the remove handler
*/
interface Removable<T, R> {
/** Whether the item can be removed */
isRemovable?: boolean;
/** Handler called when the item is removed */
onRemove?: (value: T, event?: SyntheticEvent) => R;
}Usage Examples:
import {
InputBase,
ValueBase,
Validation,
HelpTextProps,
LabelableProps,
ValidationResult
} from "@react-types/shared";
// Basic text input with validation
interface TextFieldProps
extends InputBase,
ValueBase<string>,
Validation<string>,
HelpTextProps,
LabelableProps {
type?: "text" | "email" | "password";
}
function TextField({
value,
defaultValue,
onChange,
isDisabled,
isReadOnly,
isRequired,
isInvalid,
validate,
description,
errorMessage,
label,
type = "text"
}: TextFieldProps) {
const [inputValue, setInputValue] = useState(defaultValue || "");
const [validationResult, setValidationResult] = useState<ValidationResult>({
isInvalid: false,
validationErrors: [],
validationDetails: {} as ValidityState
});
const handleChange = (newValue: string) => {
setInputValue(newValue);
onChange?.(newValue);
// Run validation if provided
if (validate) {
const result = validate(newValue);
if (result !== true && result != null) {
const errors = Array.isArray(result) ? result : [result];
setValidationResult({
isInvalid: true,
validationErrors: errors,
validationDetails: {} as ValidityState
});
} else {
setValidationResult({
isInvalid: false,
validationErrors: [],
validationDetails: {} as ValidityState
});
}
}
};
return (
<div>
{label && <label>{label}{isRequired && " *"}</label>}
<input
type={type}
value={value ?? inputValue}
onChange={(e) => handleChange(e.target.value)}
disabled={isDisabled}
readOnly={isReadOnly}
required={isRequired}
aria-invalid={isInvalid || validationResult.isInvalid}
/>
{description && <div>{description}</div>}
{(isInvalid || validationResult.isInvalid) && errorMessage && (
<div role="alert">
{typeof errorMessage === "function"
? errorMessage(validationResult)
: errorMessage}
</div>
)}
</div>
);
}
// Range input example
interface SliderProps
extends InputBase,
ValueBase<number>,
RangeInputBase<number>,
LabelableProps {
formatValue?: (value: number) => string;
}
function Slider({
value,
defaultValue,
onChange,
minValue = 0,
maxValue = 100,
step = 1,
isDisabled,
label,
formatValue
}: SliderProps) {
const [sliderValue, setSliderValue] = useState(defaultValue || minValue);
const handleChange = (newValue: number) => {
setSliderValue(newValue);
onChange?.(newValue);
};
const displayValue = value ?? sliderValue;
return (
<div>
{label && <label>{label}: {formatValue?.(displayValue) || displayValue}</label>}
<input
type="range"
min={minValue}
max={maxValue}
step={step}
value={displayValue}
onChange={(e) => handleChange(Number(e.target.value))}
disabled={isDisabled}
/>
</div>
);
}
// Validation helper functions
const validators = {
required: (message = "This field is required"): ValidationFunction<string> =>
(value) => !value?.trim() ? message : true,
email: (message = "Please enter a valid email"): ValidationFunction<string> =>
(value) => {
const emailRegex = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
return !value || emailRegex.test(value) ? true : message;
},
minLength: (min: number, message?: string): ValidationFunction<string> =>
(value) => {
if (!value || value.length >= min) return true;
return message || `Must be at least ${min} characters`;
}
};
// Usage with validation
function ContactForm() {
return (
<form>
<TextField
label="Name"
isRequired
validate={validators.required()}
description="Enter your full name"
errorMessage="Name is required"
/>
<TextField
label="Email"
type="email"
isRequired
validate={(value) => {
const required = validators.required()(value);
if (required !== true) return required;
return validators.email()(value);
}}
description="We'll use this to contact you"
errorMessage={(result) => result.validationErrors.join(", ")}
/>
</form>
);
}Install with Tessl CLI
npx tessl i tessl/npm-react-types--shared