Back to Components
✏️

Custom Input

Easy

A 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.

Inputv1.0.0Updated 2026-02-21

Live Preview

Open in Snack

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 1
const [name, setName] = useState('');

<CustomInput
  label="Full Name"
  placeholder="Enter your name"
  value={name}
  onChangeText={setName}
/>

Outlined Variant

Input with outlined border style

Example 2
<CustomInput
  variant="outlined"
  label="Email"
  placeholder="you@example.com"
  keyboardType="email-address"
  value={email}
  onChangeText={setEmail}
/>

Password Input

Secure input with visibility toggle

Example 3
<CustomInput
  label="Password"
  placeholder="Enter password"
  secureTextEntry
  autoPassword
  value={password}
  onChangeText={setPassword}
/>

Search Input

Rounded search bar variant

Example 4
<CustomInput
  variant="search"
  placeholder="Search..."
  value={query}
  onChangeText={setQuery}
  clearable
  onClear={() => setQuery('')}
/>

Underlined Variant

Minimal underlined style input

Example 5
<CustomInput
  variant="underlined"
  label="Username"
  placeholder="@username"
  value={username}
  onChangeText={setUsername}
/>

With Validation

Input showing error and success states

Example 6
<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 7
<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 8
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

PropTypeDefaultDescription
labelstring-Label text displayed above the input
placeholderstring-Placeholder text shown when empty
valuestring-Current value of the input
onChangeText(text: string) => void-Callback fired when text changes
variant'default' | 'outlined' | 'filled' | 'underlined' | 'search'defaultVisual variant of the input
size'small' | 'medium' | 'large'mediumSize preset affecting height, font size, and padding
secureTextEntrybooleanfalseHide text for password fields
autoPasswordbooleanfalseShow password visibility toggle icon (use with secureTextEntry)
multilinebooleanfalseEnable multiline text input
numberOfLinesnumber1Initial number of visible lines (multiline)
maxNumberOfLinesnumber4Maximum lines before scrolling (Android)
leftIconReactNode-Custom icon element on the left side
rightIconReactNode-Custom icon element on the right side
errorstring-Error message (shows red border and text)
successstring-Success message (shows green border and text)
helperTextstring-Helper text displayed below the input
requiredbooleanfalseShow required asterisk on label
disabledbooleanfalseDisable the input
clearablebooleanfalseShow clear × button when input has value
onClear() => void-Callback when clear button is pressed
colorsobject-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:

None