npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@acusti/input-text

v2.2.2

Published

React component that renders a semi-controlled input with multiLine and selectTextOnFocus support

Readme

@acusti/input-text

latest version maintenance status downloads per month bundle size

A React text input component that combines the simplicity of uncontrolled inputs with the power of automatic resizing, smart text selection, and advanced interaction patterns. Built for modern web applications that need reliable text input handling without the complexity of fully controlled components.

Key Features

  • Semi-Controlled Architecture - Uncontrolled for performance, automatically syncs when initialValue changes
  • Automatic Multi-line Resizing - Textarea elements grow and shrink vertically to fit content
  • Smart Text Selection - Optional select-all-on-focus behavior that works across all browsers
  • Double-Click to Edit - Read-only display mode with inline editing activation
  • Submit on Enter - Intelligent form submission with multi-line support
  • Modern CSS Integration - Uses field-sizing: content when available with ResizeObserver fallback
  • Universal Input Types - Supports text, email, password, search, tel, url, and number inputs
  • Accessibility First - Proper focus management, keyboard navigation, and screen reader support
  • Zero Dependencies - Lightweight implementation with no external dependencies

Installation

npm install @acusti/input-text
# or
yarn add @acusti/input-text

Quick Start

import InputText from '@acusti/input-text';
import { useState } from 'react';

// Simple text input
function SimpleInput() {
    return <InputText placeholder="Enter your name" name="userName" />;
}

// Multi-line textarea with auto-resize
function AutoResizingTextarea() {
    return (
        <InputText
            multiLine
            placeholder="Write your message here…"
            maxHeight={300}
            name="message"
        />
    );
}

// Input with select-all-on-focus
function SelectOnFocusInput() {
    return (
        <InputText
            initialValue="Edit this text"
            selectTextOnFocus
            name="editableText"
        />
    );
}

API Reference

InputText Component

type Props = {
    /** Controls text capitalization behavior */
    autoCapitalize?: 'none' | 'off' | 'sentences' | 'words' | 'characters';

    /** Browser autocomplete hint */
    autoComplete?: HTMLInputElement['autocomplete'];

    /** Whether input should be focused on mount */
    autoFocus?: boolean;

    /** Additional CSS class name for styling */
    className?: string;

    /** Whether the input is disabled */
    disabled?: boolean;

    /**
     * If true, input renders as readonly initially and only becomes interactive
     * when double-clicked or when user focuses the readonly input and then
     * presses the enter key. Input becomes readonly again when blurred.
     */
    doubleClickToEdit?: boolean;

    /** Hint for virtual keyboard enter key */
    enterKeyHint?:
        | 'enter'
        | 'done'
        | 'go'
        | 'next'
        | 'previous'
        | 'search'
        | 'send';

    /** Associates input with a form element */
    form?: string;

    /** Unique identifier for the input */
    id?: string;

    /**
     * The initial value of the text input. If this prop changes,
     * the input value will be updated to match the new value.
     */
    initialValue?: string;

    /** ID of a datalist element for autocomplete suggestions */
    list?: string;

    /** Maximum numeric value (for number inputs) */
    max?: number;

    /** Maximum height for multi-line inputs (px or CSS value) */
    maxHeight?: number | string;

    /** Maximum number of characters allowed */
    maxLength?: number;

    /** Minimum numeric value (for number inputs) */
    min?: number;

    /** Minimum number of characters required */
    minLength?: number;

    /**
     * If true, input renders as a <textarea> that automatically grows and
     * shrinks vertically to adjust to the length of its contents.
     */
    multiLine?: boolean;

    /** Whether multiple values are allowed (for email inputs) */
    multiple?: boolean;

    /** Input name for form submission */
    name?: string;

    /** Standard blur event handler */
    onBlur?: (event: FocusEvent<InputElement>) => unknown;

    /** Standard change event handler */
    onChange?: (event: ChangeEvent<InputElement>) => unknown;

    /**
     * Simplified change handler that receives just the new value.
     * Perfect for passing setState functions directly.
     */
    onChangeValue?: (value: string) => unknown;

    /** Standard focus event handler */
    onFocus?: (event: FocusEvent<InputElement>) => unknown;

    /** Standard key down event handler */
    onKeyDown?: (event: KeyboardEvent<InputElement>) => unknown;

    /** Standard key up event handler */
    onKeyUp?: (event: KeyboardEvent<InputElement>) => unknown;

    /** Regular expression pattern for validation */
    pattern?: string;

    /** Placeholder text when input is empty */
    placeholder?: string;

    /** Whether the input is read-only */
    readOnly?: boolean;

    /** Whether the input is required for form submission */
    required?: boolean;

    /** Initial number of rows for multi-line inputs */
    rows?: number;

    /** If true, all text is selected when input receives focus */
    selectTextOnFocus?: boolean;

    /** Visual size of the input (character width) */
    size?: number;

    /** Step value for numeric inputs */
    step?: number;

    /** Inline styles */
    style?: CSSProperties;

    /**
     * If true, pressing Enter submits the parent form or blurs the input.
     * For multi-line inputs, Cmd/Ctrl+Enter always submits.
     */
    submitOnEnter?: boolean;

    /** Tab order index */
    tabIndex?: number;

    /** Tooltip text */
    title?: string;

    /** Input type - determines validation and virtual keyboard */
    type?:
        | 'text'
        | 'email'
        | 'number'
        | 'password'
        | 'search'
        | 'tel'
        | 'url';
};

Types

import type { InputElement } from '@acusti/input-text';

// InputElement is a union type for event handlers
type InputElement = HTMLInputElement | HTMLTextAreaElement;

Usage Examples

Basic Text Inputs (equivalent API to native )

import InputText from '@acusti/input-text';
import { useState } from 'react';

function ContactForm() {
    return (
        <form>
            {/* Simple text input */}
            <InputText
                name="firstName"
                placeholder="First name"
                required
                autoCapitalize="words"
            />

            {/* Email input with autocomplete */}
            <InputText
                type="email"
                name="email"
                placeholder="Email address"
                autoComplete="email"
                required
            />

            {/* Phone input */}
            <InputText
                type="tel"
                name="phone"
                placeholder="Phone number"
                autoComplete="tel"
            />

            {/* URL input */}
            <InputText
                type="url"
                name="website"
                placeholder="Website URL"
                autoComplete="url"
            />
        </form>
    );
}

Auto-Resizing Multi-line Inputs

import InputText from '@acusti/input-text';
import { useState } from 'react';

function CommentSystem() {
    const [comment, setComment] = useState('');
    const [isSubmitting, setIsSubmitting] = useState(false);

    const handleSubmit = async (event) => {
        event.preventDefault();
        setIsSubmitting(true);

        try {
            await submitComment(comment);
            setComment('');
        } finally {
            setIsSubmitting(false);
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <label htmlFor="comment">Add a comment</label>
            <InputText
                id="comment"
                name="comment"
                multiLine
                placeholder="Share your thoughts…"
                initialValue={comment}
                onChangeValue={setComment}
                maxHeight={200}
                rows={3}
                disabled={isSubmitting}
                submitOnEnter
            />

            <div className="comment-actions">
                <small>
                    Press Enter to submit, Shift+Enter for new line
                </small>
                <button
                    type="submit"
                    disabled={!comment.trim() || isSubmitting}
                >
                    {isSubmitting ? 'Submitting…' : 'Submit Comment'}
                </button>
            </div>
        </form>
    );
}

Inline Editing with Double-Click

import InputText from '@acusti/input-text';
import { useState, useCallback } from 'react';

function EditableTitle() {
    const [title, setTitle] = useState('Click to edit this title');
    const [savedTitle, setSavedTitle] = useState(title);

    const handleTitleChange = useCallback((value: string) => {
        setTitle(value);
    }, []);

    const handleBlur = useCallback(() => {
        // Auto-save when editing finishes
        if (title !== savedTitle) {
            setSavedTitle(title);
            // Save to backend here
            console.log('Saving title:', title);
        }
    }, [title, savedTitle]);

    return (
        <div className="editable-content">
            <h1>
                <InputText
                    doubleClickToEdit
                    initialValue={title}
                    onChangeValue={handleTitleChange}
                    onBlur={handleBlur}
                    selectTextOnFocus
                    placeholder="Enter title…"
                    style={{
                        background: 'transparent',
                        border: 'none',
                        fontSize: 'inherit',
                        fontWeight: 'inherit',
                        color: 'inherit',
                        padding: 0,
                        width: '100%',
                    }}
                />
            </h1>
            <p className="hint">Double-click the title above to edit it</p>
        </div>
    );
}

Search Input with Instant Results

import InputText from '@acusti/input-text';
import { useState, useDeferredValue, useMemo } from 'react';

function SearchableList({ items }) {
    const [searchTerm, setSearchTerm] = useState('');
    const deferredSearchTerm = useDeferredValue(searchTerm);

    const filteredItems = useMemo(() => {
        if (!deferredSearchTerm) return items;
        return items.filter((item) =>
            item.name
                .toLowerCase()
                .includes(deferredSearchTerm.toLowerCase()),
        );
    }, [deferredSearchTerm, items]);

    return (
        <div className="search-container">
            <InputText
                type="search"
                placeholder="Search items…"
                initialValue={searchTerm}
                onChangeValue={setSearchTerm}
                autoFocus
                enterKeyHint="search"
                style={{
                    width: '100%',
                    padding: '12px 16px',
                    fontSize: '16px',
                    borderRadius: '8px',
                    border: '2px solid #e1e5e9',
                    outline: 'none',
                }}
            />

            <div className="search-results">
                {filteredItems.length === 0 ? (
                    <div className="no-results">
                        No items found for “{deferredSearchTerm}”
                    </div>
                ) : (
                    filteredItems.map((item) => (
                        <div key={item.id} className="search-result-item">
                            {item.name}
                        </div>
                    ))
                )}
            </div>
        </div>
    );
}

Form with Smart Validation

import InputText from '@acusti/input-text';
import { useState } from 'react';

function UserProfileForm() {
    const [formData, setFormData] = useState({
        username: '',
        email: '',
        bio: '',
        website: '',
    });
    const [errors, setErrors] = useState({});

    const updateField = (field: string) => (value: string) => {
        setFormData((prev) => ({ ...prev, [field]: value }));
        // Clear error when user starts typing
        if (errors[field]) {
            setErrors((prev) => ({ ...prev, [field]: null }));
        }
    };

    const validateField = (field: string, value: string) => {
        switch (field) {
            case 'username':
                if (value.length < 3)
                    return 'Username must be at least 3 characters';
                if (!/^[a-zA-Z0-9_]+$/.test(value))
                    return 'Username can only contain letters, numbers, and underscores';
                break;
            case 'email':
                if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
                    return 'Please enter a valid email address';
                break;
            case 'website':
                if (value && !/^https?:\/\//.test(value))
                    return 'Website must start with http:// or https://';
                break;
        }
        return null;
    };

    const handleBlur = (field: string) => (event: any) => {
        const error = validateField(field, event.target.value);
        if (error) {
            setErrors((prev) => ({ ...prev, [field]: error }));
        }
    };

    return (
        <form className="user-profile-form">
            <div className="field-group">
                <label htmlFor="username">Username*</label>
                <InputText
                    id="username"
                    name="username"
                    initialValue={formData.username}
                    onChangeValue={updateField('username')}
                    onBlur={handleBlur('username')}
                    selectTextOnFocus
                    pattern="[a-zA-Z0-9_]+"
                    minLength={3}
                    maxLength={20}
                    required
                    style={{
                        borderColor: errors.username
                            ? '#dc3545'
                            : '#ced4da',
                    }}
                />
                {errors.username && (
                    <div className="error-message">{errors.username}</div>
                )}
            </div>

            <div className="field-group">
                <label htmlFor="email">Email*</label>
                <InputText
                    id="email"
                    name="email"
                    type="email"
                    initialValue={formData.email}
                    onChangeValue={updateField('email')}
                    onBlur={handleBlur('email')}
                    autoComplete="email"
                    required
                    style={{
                        borderColor: errors.email ? '#dc3545' : '#ced4da',
                    }}
                />
                {errors.email && (
                    <div className="error-message">{errors.email}</div>
                )}
            </div>

            <div className="field-group">
                <label htmlFor="bio">Bio</label>
                <InputText
                    id="bio"
                    name="bio"
                    multiLine
                    initialValue={formData.bio}
                    onChangeValue={updateField('bio')}
                    placeholder="Tell us about yourself…"
                    maxLength={500}
                    maxHeight={150}
                    rows={4}
                />
                <div className="char-count">
                    {formData.bio.length}/500 characters
                </div>
            </div>

            <div className="field-group">
                <label htmlFor="website">Website</label>
                <InputText
                    id="website"
                    name="website"
                    type="url"
                    initialValue={formData.website}
                    onChangeValue={updateField('website')}
                    onBlur={handleBlur('website')}
                    placeholder="https://yourwebsite.com"
                    autoComplete="url"
                    style={{
                        borderColor: errors.website
                            ? '#dc3545'
                            : '#ced4da',
                    }}
                />
                {errors.website && (
                    <div className="error-message">{errors.website}</div>
                )}
            </div>

            <button type="submit" className="submit-button">
                Save Profile
            </button>
        </form>
    );
}

Chat Input with Enhanced UX

import InputText from '@acusti/input-text';
import { useState, useRef, useCallback } from 'react';

function ChatInput({ onSendMessage, disabled }) {
    const [message, setMessage] = useState('');
    const inputRef = useRef(null);

    const handleSend = useCallback(() => {
        const trimmedMessage = message.trim();
        if (trimmedMessage && !disabled) {
            onSendMessage(trimmedMessage);
            setMessage('');
            inputRef.current?.focus();
        }
    }, [message, disabled, onSendMessage]);

    const handleKeyDown = useCallback(
        (event) => {
            // Send on Enter, new line on Shift+Enter
            if (event.key === 'Enter' && !event.shiftKey) {
                event.preventDefault();
                handleSend();
            }
        },
        [handleSend],
    );

    return (
        <div className="chat-input-container">
            <div className="input-wrapper">
                <InputText
                    ref={inputRef}
                    multiLine
                    placeholder={
                        disabled ? 'Connecting…' : 'Type a message…'
                    }
                    initialValue={message}
                    onChangeValue={setMessage}
                    onKeyDown={handleKeyDown}
                    disabled={disabled}
                    maxHeight={120}
                    rows={1}
                    enterKeyHint="send"
                    style={{
                        width: '100%',
                        padding: '12px 50px 12px 16px',
                        borderRadius: '24px',
                        border: '2px solid #e1e5e9',
                        resize: 'none',
                        fontSize: '14px',
                        lineHeight: '1.4',
                        outline: 'none',
                        transition: 'border-color 0.2s ease',
                    }}
                />

                <button
                    type="button"
                    onClick={handleSend}
                    disabled={!message.trim() || disabled}
                    className="send-button"
                    aria-label="Send message"
                >
                    <svg width="20" height="20" viewBox="0 0 24 24">
                        <path
                            fill="currentColor"
                            d="M2,21L23,12L2,3V10L17,12L2,14V21Z"
                        />
                    </svg>
                </button>
            </div>

            <div className="input-hint">
                Press Enter to send, Shift+Enter for new line
            </div>
        </div>
    );
}

Dynamic Input List

import InputText from '@acusti/input-text';
import { useState, useCallback } from 'react';

function TagsInput() {
    const [tags, setTags] = useState(['React', 'TypeScript']);

    const updateTag = useCallback(
        (index: number) => (value: string) => {
            setTags((prev) => {
                const newTags = [...prev];
                newTags[index] = value;
                return newTags;
            });
        },
        [],
    );

    const removeTag = useCallback((index: number) => {
        setTags((prev) => prev.filter((_, i) => i !== index));
    }, []);

    const addTag = useCallback(() => {
        setTags((prev) => [...prev, '']);
    }, []);

    const handleKeyDown = useCallback(
        (index: number) => (event) => {
            if (
                event.key === 'Backspace' &&
                !event.target.value &&
                tags.length > 1
            ) {
                event.preventDefault();
                removeTag(index);
            } else if (
                event.key === 'Enter' &&
                event.target.value.trim()
            ) {
                event.preventDefault();
                addTag();
            }
        },
        [tags.length, removeTag, addTag],
    );

    return (
        <div className="tags-input">
            <label>Tags</label>
            <div className="tags-list">
                {tags.map((tag, index) => (
                    <div key={index} className="tag-input-wrapper">
                        <InputText
                            initialValue={tag}
                            onChangeValue={updateTag(index)}
                            onKeyDown={handleKeyDown(index)}
                            placeholder="Enter tag…"
                            selectTextOnFocus
                            style={{
                                padding: '8px 12px',
                                border: '2px solid #e1e5e9',
                                borderRadius: '20px',
                                fontSize: '14px',
                                minWidth: '100px',
                            }}
                        />

                        {tags.length > 1 && (
                            <button
                                type="button"
                                onClick={() => removeTag(index)}
                                className="remove-tag"
                                aria-label="Remove tag"
                            >
                                ×
                            </button>
                        )}
                    </div>
                ))}

                <button type="button" onClick={addTag} className="add-tag">
                    + Add Tag
                </button>
            </div>

            <div className="tags-preview">
                <strong>Tags:</strong> {tags.filter(Boolean).join(', ')}
            </div>
        </div>
    );
}

Advanced Features

Automatic Resizing

The multi-line input automatically resizes using modern CSS field-sizing: content when supported, with a ResizeObserver-based fallback for older browsers:

// Automatic resizing with height limits
<InputText
    multiLine
    maxHeight={300} // Prevents growing beyond 300px
    rows={3} // Initial height (3 rows)
    placeholder="This textarea will grow as you type…"
/>

Smart Enter Key Handling

The component intelligently handles Enter key behavior based on context:

// Single-line: Enter always submits or blurs
<InputText
    submitOnEnter  // Enter submits form or blurs input
    type="search"
/>

// Multi-line: Enter adds newline, Cmd/Ctrl+Enter submits
<InputText
    multiLine
    submitOnEnter  // Cmd/Ctrl+Enter submits, Enter adds newline
/>

Browser Compatibility

  • Modern Browsers - Chrome, Firefox, Safari, Edge (latest versions)
  • CSS Features - Uses field-sizing: content when available
  • Fallback - ResizeObserver-based resizing for older browsers
  • Mobile - Touch-friendly with proper virtual keyboard hints
  • Accessibility - Full screen reader and keyboard navigation support

Styling

The component accepts standard CSS styling and works with any CSS framework:

/* Custom styling example */
.custom-input {
    --input-border-radius: 8px;
    --input-padding: 12px 16px;
    --input-font-size: 16px;
    --input-line-height: 1.5;

    padding: var(--input-padding);
    border-radius: var(--input-border-radius);
    font-size: var(--input-font-size);
    line-height: var(--input-line-height);

    border: 2px solid #e1e5e9;
    background: #ffffff;
    transition: all 0.2s ease;
}

.custom-input:focus {
    border-color: #007bff;
    box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
    outline: none;
}

.custom-input:disabled {
    background-color: #f8f9fa;
    border-color: #e9ecef;
    color: #6c757d;
    cursor: not-allowed;
}

/* Multi-line specific styles */
.custom-input[data-multiline='true'] {
    resize: vertical;
    min-height: 100px;
}

/* Double-click to edit styles */
.custom-input[readonly] {
    border-color: transparent;
    background: transparent;
    cursor: pointer;
}

.custom-input[readonly]:hover {
    background: #f8f9fa;
    border-color: #e9ecef;
}

Accessibility

The InputText component includes comprehensive accessibility features:

Keyboard Navigation

  • Tab Navigation - Proper tab order integration
  • Enter Key - Smart submission behavior
  • Escape Key - Cancels double-click editing
  • Standard Text Editing - All standard keyboard shortcuts work

Screen Reader Support

  • Semantic HTML - Uses proper input/textarea elements
  • Label Association - Works with label elements and aria-labelledby
  • Status Announcements - Changes are announced appropriately
  • Error States - Validation errors are properly communicated

Focus Management

  • Focus Indicators - Clear visual focus states
  • Focus Trapping - Proper focus behavior in modals/popovers
  • Auto Focus - Optional automatic focusing on mount

Implementation Example

// Accessible form implementation
<div className="form-field">
    <label htmlFor="user-bio" id="bio-label">
        Biography
        <span className="required" aria-label="required">
            *
        </span>
    </label>

    <InputText
        id="user-bio"
        name="biography"
        multiLine
        required
        aria-labelledby="bio-label"
        aria-describedby="bio-hint bio-error"
        aria-invalid={hasError}
        maxLength={500}
    />

    <div id="bio-hint" className="field-hint">
        Tell us about yourself in 500 characters or less
    </div>

    {hasError && (
        <div id="bio-error" className="error-message" role="alert">
            Biography is required and must be at least 10 characters
        </div>
    )}
</div>

Common Use Cases

  • Form Fields - Contact forms, user profiles, settings
  • Content Creation - Blog posts, comments, social media
  • Search Interfaces - Live search, filters, autocomplete
  • Inline Editing - Editable titles, descriptions, labels
  • Chat Applications - Message input with auto-resize
  • Configuration Panels - Settings, preferences, admin interfaces
  • Data Entry - Spreadsheet-like interfaces, bulk data input
  • Creative Tools - Code editors, markdown editors, note-taking

Demo

See the Storybook documentation and examples for interactive demonstrations of all InputText features and configurations.