Input
inputuiText input — single-line. Three sizes, three variants (outlined / filled / borderless), error / warning status, prefix / suffix nodes, addonBefore / addonAfter, allow-clear, password toggle, char count, and composite InputGroup with addons and action buttons.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/input.json$npx shadcn@latest add https://uipkge.dev/r/react/input.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/input.json$bunx shadcn@latest add https://uipkge.dev/r/react/input.jsonNamed registry:
npx shadcn@latest add @uipkge-react/inputInstalls to:components/ui/input/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
size | Size | — | optional |
variant | Variant | — | optional |
status | Status | — | optional |
prefixLeading content. A node wins over the `prefixIcon` shorthand. | React.ReactNode | — | optional |
suffix | React.ReactNode | — | optional |
prefixIconShorthand for an icon at the start/end (e.g. `prefixIcon={<Mail />}`). | React.ReactNode | — | optional |
suffixIcon | React.ReactNode | — | optional |
addonBefore | React.ReactNode | — | optional |
addonAfter | React.ReactNode | — | optional |
allowClear | boolean | — | optional |
showCount | boolean | — | optional |
showPasswordToggle | boolean | — | optional |
classNameWrapper className. Falls through to the bordered control, not the <input>. | string | — | optional |
npm dependencies
Used by
access-request-workflowai-llm-chatapi-keysarticle-editor-toolbaraudio-waveform-transcriptaudit-event-streamaudit-logauth-password-resetauth-sign-inauth-sign-upbank-account-connectorblog-post-card-gridbookmark-manager-gridcap-table-ownership-summarycareers-job-boardcart-drawercdc-replication-streamchat-two-panecoming-sooncommercial-invoice-generatorcommute-reimbursement-claimscompliance-sanctions-screenercontact-01contact-uscontent-calendar-matrixcrm-sales-call-workbenchcron-job-schedulercrypto-wallet-portfoliocta-split-demo-request-modaldata-catalog-lineagedata-contract-governancedata-explorerdata-retention-policy-managerdata-tabledata-table-pagedatabase-schema-viewerdbt-model-graphdecision-matrix-tabledefi-staking-yield-vaultdemo-sandbox-paneldns-record-managerdoctor-appointment-schedulerdriver-inspection-checklistemail-campaign-composeremail-template-previeweremployee-org-chart-treeenterprise-workspace-switcherenvironment-variables-managerevent-calendar-dashboardexit-intentexit-intent-newsletterfaq-01faq-searchable-accordion-workbenchfeature-flags-managerfeature-store-registryfeatures-01field-inspection-managerfile-managerfooterfooter-01footer-mega-newsletterform-layoutsfreight-quote-calculatorgift-card-balance-checkergraphql-query-explorerherohero-waitlist-glowhotel-booking-reservationinboxintegrations-directoryintegrations-search-directoryinventory-reorder-forecastinvite-membersinvoice-creator-wizardkafka-stream-monitorkanban-task-boardknowledge-base-hublead-capture-popuplesson-video-playerloan-calculatorlogin-01login-02matrix-swot-analysismcp-server-registry-managermedia-asset-gallerymultimodal-vision-playgroundnda-agreement-generatornewsletternewsletter-inline-bandnewsletter-issue-archivenewsletter-signupnewsletter-split-previewonboarding-wizardorder-management-pagepatient-intake-formpayment-formpomodoro-focus-timerprivacy-policy-generatorproduct-detail-pageproduct-qa-communityproject-roadmappurchase-order-receivingrag-pipeline-visualizerreferral-program-widgetregister-01returns-portalreverse-etl-sync-managerrich-text-editorsafe-note-calculatorsearch-results-pagesecrets-rotation-schedulerseo-audit-checklistsettings-pageshipment-tracking-timelineshipping-label-printersidebarsocial-media-post-schedulersso-identity-provider-configstock-transfer-managerstore-pickup-curbsidesupport-ticket-detailsynthetic-data-generatortechnical-specs-sheettelehealth-video-roomtime-off-portaltime-tracker-timesheettrademark-patent-portfoliotransaction-feedtransfertriage-queue-dashboardutm-campaign-buildervector-embeddings-inspectorvitals-tracker-chartvulnerability-scanner-reportwaitlistwaitlist-referral-positionwarehouse-inventory-gridwave-picking-consolewebhook-testerwebhookswholesale-b2b-quick-order
Files installed (5)
components/ui/input/Input.tsx10 kB
'use client' import * as React from 'react' import { X, Eye, EyeOff } from 'lucide-react' import { cn } from '@/lib/utils' type Size = 'small' | 'middle' | 'large' type Variant = 'outlined' | 'filled' | 'borderless' type Status = 'error' | 'warning' export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> { size?: Size variant?: Variant status?: Status /** Leading content. A node wins over the `prefixIcon` shorthand. */ prefix?: React.ReactNode suffix?: React.ReactNode /** Shorthand for an icon at the start/end (e.g. `prefixIcon={<Mail />}`). */ prefixIcon?: React.ReactNode suffixIcon?: React.ReactNode addonBefore?: React.ReactNode addonAfter?: React.ReactNode allowClear?: boolean showCount?: boolean showPasswordToggle?: boolean /** Wrapper className. Falls through to the bordered control, not the <input>. */ className?: string } const sizeClasses: Record<Size, string> = { small: 'h-8 text-xs', middle: 'h-9 text-base md:text-sm', large: 'h-11 text-base', } const Input = React.forwardRef<HTMLInputElement, InputProps>( ( { size = 'middle', variant = 'outlined', status, prefix, suffix, prefixIcon, suffixIcon, addonBefore, addonAfter, allowClear, showCount, showPasswordToggle, className, type = 'text', disabled, readOnly, maxLength, value, defaultValue, onChange, onFocus, onBlur, id, 'aria-invalid': ariaInvalid, ...rest }, ref, ) => { const innerRef = React.useRef<HTMLInputElement | null>(null) // Merge the forwarded ref with our internal ref (needed for focus()). const setRefs = React.useCallback( (node: HTMLInputElement | null) => { innerRef.current = node if (typeof ref === 'function') ref(node) else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = node }, [ref], ) const isControlled = value !== undefined const [internal, setInternal] = React.useState<string>(defaultValue != null ? String(defaultValue) : '') const currentValue = isControlled ? String(value ?? '') : internal const [focused, setFocused] = React.useState(false) const [hovered, setHovered] = React.useState(false) const [passwordVisible, setPasswordVisible] = React.useState(false) const isPassword = type === 'password' const hasPrefix = !!prefix || !!prefixIcon const hasSuffix = !!suffix || !!suffixIcon const hasAddonBefore = !!addonBefore const hasAddonAfter = !!addonAfter const hasPasswordToggle = !!showPasswordToggle && isPassword const hasCount = !!showCount && maxLength != null const hasRightConfig = !!allowClear || hasPasswordToggle || hasCount const currentLength = currentValue.length const showClear = !!allowClear && currentValue.length > 0 && (focused || hovered) && !disabled && !readOnly const computedType = !isPassword ? type : passwordVisible ? 'text' : 'password' const wrapperRounded = hasAddonBefore && hasAddonAfter ? 'rounded-none' : hasAddonBefore ? 'rounded-l-none rounded-r-md' : hasAddonAfter ? 'rounded-r-none rounded-l-md' : 'rounded-md' const variantMap: Record<Variant, string> = { outlined: 'border-input bg-transparent shadow-xs', filled: 'border-transparent bg-muted/50 shadow-none', borderless: 'border-transparent bg-transparent shadow-none', } const statusMap: Record<Status, string> = { error: 'border-destructive focus-within:border-destructive focus-within:ring-destructive/20 dark:focus-within:ring-destructive/40', warning: 'border-warning focus-within:border-warning focus-within:ring-warning/20', } const wrapperClasses = cn( 'flex w-full items-center gap-1.5 overflow-hidden border transition-[color,box-shadow] outline-none', sizeClasses[size], variantMap[variant], status ? statusMap[status] : '', !status ? 'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]' : '', disabled ? 'pointer-events-none opacity-50 cursor-not-allowed bg-muted/30' : '', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', wrapperRounded, className, ) function addonClasses(position: 'before' | 'after') { const roundedClass = position === 'before' ? 'rounded-l-md rounded-r-none border-r-0' : 'rounded-r-md rounded-l-none border-l-0' return cn( 'flex items-center bg-muted px-3 text-sm text-muted-foreground border border-input', roundedClass, sizeClasses[size], ) } const sidePad = size === 'small' ? { l: 'pl-2', r: 'pr-2' } : size === 'large' ? { l: 'pl-3', r: 'pr-3' } : { l: 'pl-2.5', r: 'pr-2.5' } const hasLeft = hasPrefix const hasRight = hasSuffix || hasRightConfig const inputPadding = !hasLeft && !hasRight ? cn(sidePad.l, sidePad.r) : hasLeft && !hasRight ? cn('pl-0', sidePad.r) : !hasLeft && hasRight ? cn(sidePad.l, 'pr-0') : 'px-0' function emit(next: string) { if (!isControlled) setInternal(next) } function handleChange(e: React.ChangeEvent<HTMLInputElement>) { emit(e.target.value) onChange?.(e) } function handleClear() { const node = innerRef.current if (node) { // Use the native setter so a controlled parent's onChange fires with a // real event (React tracks the value on the DOM node's prototype). const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set setter?.call(node, '') node.dispatchEvent(new Event('input', { bubbles: true })) node.focus() } emit('') } function togglePassword() { setPasswordVisible((v) => !v) innerRef.current?.focus() } // status=error implies invalid for AT; also set on the native input (not only the wrapper). const resolvedAriaInvalid = status === 'error' ? true : ariaInvalid return ( <div className="flex w-full"> {hasAddonBefore && <div className={addonClasses('before')}>{addonBefore}</div>} <div className={wrapperClasses} data-uipkge="" data-slot="input" aria-invalid={resolvedAriaInvalid} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} onClick={() => innerRef.current?.focus()} > {hasPrefix && ( <span className={cn('text-muted-foreground pointer-events-none shrink-0 select-none', sidePad.l)}> {prefixIcon ?? prefix} </span> )} <input id={id} ref={setRefs} value={currentValue} type={computedType} disabled={disabled} readOnly={readOnly} maxLength={maxLength} aria-invalid={resolvedAriaInvalid} onChange={handleChange} onFocus={(e) => { setFocused(true) onFocus?.(e) }} onBlur={(e) => { setFocused(false) onBlur?.(e) }} className={cn( 'w-full min-w-0 flex-1 bg-transparent outline-none', 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground', 'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium', 'disabled:cursor-not-allowed', inputPadding, )} {...rest} /> {/* Only mount the right rail when it has content — an empty padded flex child steals horizontal space from plain inputs. Built-in actions render first, then the user's suffix, so the suffix is always rightmost. */} {hasRight && ( <div className={cn('flex shrink-0 items-center gap-1', sidePad.r)}> {showClear && ( <button type="button" aria-label="Clear input" className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 shrink-0 rounded p-0.5 transition-colors focus-visible:ring-1 focus-visible:outline-none" onMouseDown={(e) => e.preventDefault()} onClick={handleClear} > <X className="size-4" aria-hidden="true" /> </button> )} {hasPasswordToggle && !disabled && !readOnly && ( <button type="button" aria-label={passwordVisible ? 'Hide password' : 'Show password'} aria-pressed={passwordVisible} className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 shrink-0 rounded p-0.5 transition-colors focus-visible:ring-1 focus-visible:outline-none" onMouseDown={(e) => e.preventDefault()} onClick={togglePassword} > {passwordVisible ? ( <EyeOff className="size-4" aria-hidden="true" /> ) : ( <Eye className="size-4" aria-hidden="true" /> )} </button> )} {hasCount && ( <span className="text-muted-foreground pointer-events-none text-xs select-none"> {currentLength}/{maxLength} </span> )} {hasSuffix && ( <span className="text-muted-foreground pointer-events-none select-none">{suffixIcon ?? suffix}</span> )} </div> )} </div> {hasAddonAfter && <div className={addonClasses('after')}>{addonAfter}</div>} </div> ) }, ) Input.displayName = 'Input' export { Input }components/ui/input/InputGroup.tsx1.5 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' export interface InputGroupProps extends React.HTMLAttributes<HTMLDivElement> { size?: 'small' | 'middle' | 'large' disabled?: boolean } const sizeClasses: Record<string, string> = { small: 'h-8 text-xs', middle: 'h-9 text-sm', large: 'h-11 text-base', } export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>( ({ className, size = 'middle', disabled, children, ...props }, ref) => { return ( <div ref={ref} data-uipkge="" data-slot="input-group" data-size={size} data-disabled={disabled ? '' : undefined} className={cn( 'group/input-group border-input bg-background relative flex w-full items-stretch rounded-md border shadow-xs transition-[color,box-shadow]', 'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px] focus-within:outline-none', '[&_[data-slot=input]]:rounded-none [&_[data-slot=input]]:border-0 [&_[data-slot=input]]:bg-transparent [&_[data-slot=input]]:shadow-none [&_[data-slot=input]]:focus-within:ring-0', '[&_input]:h-full [&_input]:flex-1 [&_input]:border-0 [&_input]:bg-transparent [&_input]:px-3 [&_input]:text-sm [&_input]:outline-none [&_input]:focus-visible:ring-0', disabled && 'bg-muted/30 pointer-events-none cursor-not-allowed opacity-50', sizeClasses[size], className, )} {...props} > {children} </div> ) }, ) InputGroup.displayName = 'InputGroup'components/ui/input/InputGroupAddon.tsx0.9 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' export interface InputGroupAddonProps extends React.HTMLAttributes<HTMLDivElement> { align?: 'inline' | 'block' } export const InputGroupAddon = React.forwardRef<HTMLDivElement, InputGroupAddonProps>( ({ className, align = 'inline', children, ...props }, ref) => { return ( <div ref={ref} data-uipkge="" data-slot="input-group-addon" className={cn( 'text-muted-foreground flex shrink-0 items-center justify-center px-3 text-sm select-none', 'border-input first:rounded-l-[calc(var(--radius)-1px)] last:rounded-r-[calc(var(--radius)-1px)]', 'border-r first:border-l-0 last:border-r-0', className, )} {...props} > {children} </div> ) }, ) InputGroupAddon.displayName = 'InputGroupAddon'components/ui/input/InputGroupButton.tsx1.5 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' export interface InputGroupButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: 'default' | 'secondary' | 'ghost' | 'outline' } const variantClasses: Record<string, string> = { default: 'bg-primary text-primary-foreground hover:bg-primary/90', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', outline: 'border-l border-input hover:bg-accent hover:text-accent-foreground', } export const InputGroupButton = React.forwardRef<HTMLButtonElement, InputGroupButtonProps>( ({ className, variant = 'ghost', disabled, type = 'button', children, ...props }, ref) => { return ( <button ref={ref} type={type} data-uipkge="" data-slot="input-group-button" disabled={disabled} className={cn( 'inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 px-3 text-sm font-medium transition-colors select-none', 'first:rounded-l-[calc(var(--radius)-1px)] last:rounded-r-[calc(var(--radius)-1px)]', 'focus-visible:ring-ring focus-visible:ring-1 focus-visible:outline-none', 'disabled:pointer-events-none disabled:opacity-50', variantClasses[variant], className, )} {...props} > {children} </button> ) }, ) InputGroupButton.displayName = 'InputGroupButton'components/ui/input/index.ts0.3 kB
export { Input, type InputProps } from './Input' export { InputGroup, type InputGroupProps } from './InputGroup' export { InputGroupAddon, type InputGroupAddonProps } from './InputGroupAddon' export { InputGroupButton, type InputGroupButtonProps } from './InputGroupButton'
Raw manifest:https://uipkge.dev/r/react/input.json