ComponentsInput
Custom Input
EasyA highly flexible text input component with 5 visual variants (default, outlined, filled, underlined, search), 3 size presets, built-in password toggle, multiline support with auto-expanding on Android, clearable mode, error/success validation states, and full customization via style overrides and custom color themes. Zero external dependencies - uses only core React Native APIs.
v1.0.0
Updated 2026-02-21
Live Preview
Open in SnackPreview
Installation
1
Copy CustomInput.tsx to your components folder
2
Import and use in your screen
3
No external npm packages required
Source Code
CustomInput.tsx
import React, { useState, forwardRef, useCallback } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Platform,
type ViewStyle,
type TextStyle,
} from 'react-native';
type TextInputProps = React.ComponentProps<typeof TextInput>;
// ─── Types ───────────────────────────────────────────────────────────────────
type InputVariant = 'default' | 'outlined' | 'filled' | 'underlined' | 'search';
type InputSize = 'small' | 'medium' | 'large';
interface CustomInputProps extends Omit<TextInputProps, 'style'> {
/** Label displayed above the input */
label?: string;
/** Placeholder text */
placeholder?: string;
/** Current value */
value?: string;
/** Text change handler */
onChangeText?: (text: string) => void;
// ─── Input types ─────────────────────────────────
/** Secure text entry for passwords */
secureTextEntry?: boolean;
/** Show password visibility toggle (works with secureTextEntry) */
autoPassword?: boolean;
/** Enable multiline input */
multiline?: boolean;
/** Number of visible lines (multiline) */
numberOfLines?: number;
/** Max lines before scrolling (Android) */
maxNumberOfLines?: number;
/** Maximum character length */
maxLength?: number;
// ─── Icons ───────────────────────────────────────
/** Custom left icon element */
leftIcon?: React.ReactNode;
/** Custom right icon element */
rightIcon?: React.ReactNode;
/** Show search icon on the left (defaults to true for 'search' variant) */
showSearchIcon?: boolean;
// ─── Styling and variants ────────────────────────
/** Visual variant */
variant?: InputVariant;
/** Size preset */
size?: InputSize;
/** Disabled state */
disabled?: boolean;
/** Whether the input is editable */
editable?: boolean;
// ─── Validation ──────────────────────────────────
/** Error message (shows red border + text) */
error?: string;
/** Success message (shows green border + text) */
success?: string;
/** Helper text below input */
helperText?: string;
/** Show required asterisk on label */
required?: boolean;
// ─── Style overrides ─────────────────────────────
/** Container style override */
containerStyle?: ViewStyle;
/** Input text style override */
inputStyle?: TextStyle;
/** Label style override */
labelStyle?: TextStyle;
// ─── Clearable ───────────────────────────────────
/** Show clear button when input has value */
clearable?: boolean;
/** Callback when clear button is pressed */
onClear?: () => void;
// ─── Custom colors (optional theme override) ─────
colors?: {
primary?: string;
background?: string;
border?: string;
text?: string;
placeholder?: string;
error?: string;
success?: string;
disabled?: string;
};
}
// ─── Default Colors ──────────────────────────────────────────────────────────
const DEFAULT_COLORS = {
primary: '#1a1a2e',
background: '#f5f5f5',
border: '#c0c0c0',
text: '#1a1a2e',
placeholder: '#a0a0a0',
error: '#e74c3c',
success: '#2ecc71',
disabled: '#e0e0e0',
white: '#ffffff',
};
// ─── Size Config ─────────────────────────────────────────────────────────────
const SIZE_CONFIG = {
small: { height: 40, fontSize: 13, px: 12, iconSize: 18 },
medium: { height: 48, fontSize: 15, px: 16, iconSize: 20 },
large: { height: 56, fontSize: 17, px: 20, iconSize: 22 },
};
// ─── Component ───────────────────────────────────────────────────────────────
const CustomInput = forwardRef<any, CustomInputProps>(
(
{
label,
placeholder,
value,
onChangeText,
secureTextEntry = false,
autoPassword = false,
multiline = false,
numberOfLines = 1,
maxNumberOfLines = 4,
maxLength,
leftIcon,
rightIcon,
showSearchIcon,
variant = 'default',
size = 'medium',
disabled = false,
editable = true,
error,
success,
helperText,
required = false,
containerStyle,
inputStyle,
labelStyle,
placeholderTextColor,
onFocus,
onBlur,
clearable = false,
onClear,
colors: customColors,
...rest
},
ref
) => {
const COLORS = { ...DEFAULT_COLORS, ...customColors };
const sizeConfig = SIZE_CONFIG[size as InputSize] || SIZE_CONFIG.medium;
const shouldShowSearchIcon =
showSearchIcon !== undefined ? showSearchIcon : variant === 'search';
const [isSecureVisible, setIsSecureVisible] = useState(secureTextEntry);
const [isFocused, setIsFocused] = useState(false);
const [androidHeight, setAndroidHeight] = useState<number | null>(null);
// ─── Handlers ────────────────────────────────
const handleFocus = useCallback(
(e: any) => {
setIsFocused(true);
onFocus?.(e);
},
[onFocus]
);
const handleBlur = useCallback(
(e: any) => {
setIsFocused(false);
onBlur?.(e);
},
[onBlur]
);
const toggleSecure = useCallback(() => {
setIsSecureVisible((prev: boolean) => !prev);
}, []);
const handleContentSizeChange = useCallback(
(e: any) => {
if (!multiline || Platform.OS !== 'android') return;
const { height } = e.nativeEvent.contentSize;
const lineH = sizeConfig.fontSize * 1.4;
const minH = lineH + 8;
const maxH = lineH * maxNumberOfLines + 8;
const newH = Math.min(Math.max(height + 8, minH), maxH);
if (newH !== androidHeight) setAndroidHeight(newH);
},
[multiline, sizeConfig.fontSize, maxNumberOfLines, androidHeight]
);
// ─── Styles ──────────────────────────────────
const showPasswordToggle = autoPassword && secureTextEntry;
const getVariantStyle = (): ViewStyle => {
switch (variant) {
case 'outlined':
return {
borderWidth: 1,
borderColor: COLORS.border,
backgroundColor: 'transparent',
};
case 'filled':
return {
backgroundColor: COLORS.background,
borderWidth: 0,
};
case 'underlined':
return {
borderRadius: 0,
borderBottomWidth: 1.5,
borderBottomColor: COLORS.border,
backgroundColor: 'transparent',
borderWidth: 0,
};
case 'search':
return {
borderRadius: 24,
backgroundColor: COLORS.background,
borderWidth: 0,
};
default:
return {
borderWidth: 0.5,
borderColor: COLORS.border,
};
}
};
const getStateStyle = (): ViewStyle => {
if (error) {
return {
borderColor: COLORS.error,
borderWidth: variant === 'underlined' ? 0 : 1,
borderBottomWidth: variant === 'underlined' ? 2 : 1,
};
}
if (success) {
return {
borderColor: COLORS.success,
borderWidth: variant === 'underlined' ? 0 : 1,
borderBottomWidth: variant === 'underlined' ? 2 : 1,
};
}
if (isFocused) {
return {
borderColor: COLORS.primary,
borderWidth: variant === 'underlined' ? 0 : 1,
borderBottomWidth: variant === 'underlined' ? 2 : 1,
};
}
return {};
};
const containerBaseStyle: ViewStyle = {
flexDirection: 'row',
alignItems: multiline ? 'flex-start' : 'center',
borderRadius: 8,
backgroundColor: COLORS.background,
height: multiline ? undefined : sizeConfig.height,
minHeight: multiline ? sizeConfig.height : undefined,
paddingHorizontal: sizeConfig.px,
...getVariantStyle(),
...getStateStyle(),
...(disabled ? { backgroundColor: COLORS.disabled, opacity: 0.6 } : {}),
...(multiline && Platform.OS === 'android' && androidHeight
? { height: androidHeight }
: {}),
};
// ─── Render ──────────────────────────────────
return (
<View style={styles.wrapper}>
{/* Label */}
{label && (
<Text style={[styles.label, { color: COLORS.text }, labelStyle]}>
{label}
{required && <Text style={{ color: COLORS.error }}> *</Text>}
</Text>
)}
{/* Input Container */}
<View style={[containerBaseStyle, containerStyle]}>
{/* Left Icon / Search Icon */}
{(leftIcon || shouldShowSearchIcon) && (
<View style={styles.iconLeft}>
{leftIcon || (
<Text style={{ fontSize: sizeConfig.iconSize, color: COLORS.placeholder }}>
🔍
</Text>
)}
</View>
)}
{/* TextInput */}
<TextInput
ref={ref}
style={[
styles.input,
{
fontSize: sizeConfig.fontSize,
color: COLORS.text,
textAlignVertical: multiline ? 'top' : 'center',
paddingVertical: multiline ? 12 : 4,
},
inputStyle,
] as any}
placeholder={placeholder}
placeholderTextColor={placeholderTextColor || COLORS.placeholder}
value={value}
onChangeText={onChangeText}
secureTextEntry={isSecureVisible}
multiline={multiline}
numberOfLines={numberOfLines}
maxLength={maxLength}
editable={editable && !disabled}
onFocus={handleFocus}
onBlur={handleBlur}
onContentSizeChange={
Platform.OS === 'android' ? handleContentSizeChange : undefined
}
{...rest}
/>
{/* Password Toggle */}
{showPasswordToggle && (
<TouchableOpacity
onPress={toggleSecure}
style={styles.iconRight}
disabled={disabled}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Text style={{ fontSize: sizeConfig.iconSize }}>
{isSecureVisible ? '👁️🗨️' : '👁️'}
</Text>
</TouchableOpacity>
)}
{/* Right Icon */}
{!showPasswordToggle && rightIcon && (
<View style={styles.iconRight}>{rightIcon}</View>
)}
{/* Clear Button */}
{clearable && (value?.length ?? 0) > 0 && !showPasswordToggle && (
<TouchableOpacity
onPress={onClear}
style={styles.iconRight}
disabled={disabled}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Text style={{ fontSize: sizeConfig.iconSize, color: COLORS.placeholder }}>
✕
</Text>
</TouchableOpacity>
)}
</View>
{/* Feedback Text */}
{error && <Text style={[styles.feedbackText, { color: COLORS.error }]}>{error}</Text>}
{success && !error && (
<Text style={[styles.feedbackText, { color: COLORS.success }]}>{success}</Text>
)}
{helperText && !error && !success && (
<Text style={[styles.feedbackText, { color: COLORS.placeholder }]}>
{helperText}
</Text>
)}
</View>
);
}
);
CustomInput.displayName = 'CustomInput';
// ─── Styles ──────────────────────────────────────────────────────────────────
const styles = StyleSheet.create({
wrapper: {
marginVertical: 6,
},
label: {
fontSize: 14,
fontWeight: '600',
marginBottom: 6,
},
input: {
flex: 1,
padding: 0,
margin: 0,
outlineStyle: 'none' as any, // removes default blue web focus outline
borderWidth: 0, // removes any default TextInput border on web
},
iconLeft: {
marginRight: 10,
justifyContent: 'center',
alignItems: 'center',
},
iconRight: {
marginLeft: 10,
justifyContent: 'center',
alignItems: 'center',
},
feedbackText: {
fontSize: 12,
marginTop: 4,
marginLeft: 4,
},
});
export default CustomInput;
Usage Examples
Default Input
Basic text input with label
Example Code
const [name, setName] = useState('');
<CustomInput
label="Full Name"
placeholder="Enter your name"
value={name}
onChangeText={setName}
/>Outlined Variant
Input with outlined border style
Example Code
<CustomInput
variant="outlined"
label="Email"
placeholder="you@example.com"
keyboardType="email-address"
value={email}
onChangeText={setEmail}
/>Password Input
Secure input with visibility toggle
Example Code
<CustomInput
label="Password"
placeholder="Enter password"
secureTextEntry
autoPassword
value={password}
onChangeText={setPassword}
/>Search Input
Rounded search bar variant
Example Code
<CustomInput
variant="search"
placeholder="Search..."
value={query}
onChangeText={setQuery}
clearable
onClear={() => setQuery('')}
/>Underlined Variant
Minimal underlined style input
Example Code
<CustomInput
variant="underlined"
label="Username"
placeholder="@username"
value={username}
onChangeText={setUsername}
/>With Validation
Input showing error and success states
Example Code
<CustomInput
label="Email"
placeholder="you@example.com"
value={email}
onChangeText={setEmail}
error={!isValid ? 'Please enter a valid email' : ''}
success={isValid ? 'Looks good!' : ''}
required
/>Multiline Input
Auto-expanding text area
Example Code
<CustomInput
label="Bio"
placeholder="Tell us about yourself..."
multiline
numberOfLines={3}
size="large"
value={bio}
onChangeText={setBio}
/>Filled Variant with Icons
Filled background with custom left icon
Example Code
import Icon from 'react-native-vector-icons/MaterialIcons';
<CustomInput
variant="filled"
label="Location"
placeholder="Enter city"
leftIcon={<Icon name="location-on" size={20} color="#666" />}
value={city}
onChangeText={setCity}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | Label text displayed above the input |
placeholder | string | - | Placeholder text shown when empty |
value | string | - | Current value of the input |
onChangeText | (text: string) => void | - | Callback fired when text changes |
variant | 'default' | 'outlined' | 'filled' | 'underlined' | 'search' | default | Visual variant of the input |
size | 'small' | 'medium' | 'large' | medium | Size preset affecting height, font size, and padding |
secureTextEntry | boolean | false | Hide text for password fields |
autoPassword | boolean | false | Show password visibility toggle icon (use with secureTextEntry) |
multiline | boolean | false | Enable multiline text input |
numberOfLines | number | 1 | Initial number of visible lines (multiline) |
maxNumberOfLines | number | 4 | Maximum lines before scrolling (Android) |
leftIcon | ReactNode | - | Custom icon element on the left side |
rightIcon | ReactNode | - | Custom icon element on the right side |
error | string | - | Error message (shows red border and text) |
success | string | - | Success message (shows green border and text) |
helperText | string | - | Helper text displayed below the input |
required | boolean | false | Show required asterisk on label |
disabled | boolean | false | Disable the input |
clearable | boolean | false | Show clear × button when input has value |
onClear | () => void | - | Callback when clear button is pressed |
colors | object | - | Custom color theme override (primary, background, border, text, placeholder, error, success, disabled) |
Features
- 5 visual variants: default, outlined, filled, underlined, search
- 3 size presets: small, medium, large
- Password field with visibility toggle
- Multiline with auto-expand on Android
- Clearable input with × button
- Error, success, and helper text states
- Left and right icon slots
- Built-in search icon for search variant
- Label with required asterisk indicator
- forwardRef support for programmatic focus
- Fully customizable colors via theme override
- Zero external dependencies
- TypeScript support with full type definitions
Dependencies
Required
Zero Dependencies