Input Group
Add addons, buttons, and helper content to inputs.
$
Web preview — components render natively on iOS & Android
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
export function MyScreen() {
return (
<InputGroup>
<InputGroupAddon>
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
<InputGroupInput placeholder="0.00" keyboardType="numeric" />
</InputGroup>
);
}Installation#
npx @aniui/cli add input-groupUsage#
app/index.tsx
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
export function MyScreen() {
return (
<InputGroup>
<InputGroupAddon>
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
<InputGroupInput placeholder="0.00" keyboardType="numeric" />
</InputGroup>
);
}Suffix Addon#
.com
Web preview — components render natively on iOS & Android
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
export function MyScreen() {
return (
<InputGroup>
<InputGroupInput placeholder="you@example.com" keyboardType="email-address" />
<InputGroupAddon align="end">
<InputGroupText>@gmail.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
);
}Button Addon#
Web preview — components render natively on iOS & Android
import {
InputGroup,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group";
import { Text } from "react-native";
export function MyScreen() {
return (
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupButton onPress={() => console.log("search")}>
<Text className="text-sm font-medium text-primary">Go</Text>
</InputGroupButton>
</InputGroup>
);
}Components#
InputGroup is a compound component made up of several parts:
ComponentDescription
InputGroupRoot container that provides the bordered row layout.
InputGroupAddonNon-interactive addon area. Supports start and end alignment.
InputGroupInputThe text input element. Expands to fill available space.
InputGroupButtonPressable button placed inside the input group.
InputGroupTextStyled text for use inside addons.
Props#
InputGroup#
PropTypeDefault
classNamestring-childrenReact.ReactNode-Also accepts all View props from React Native.
InputGroupAddon#
PropTypeDefault
align"start" | "end""start"classNamestring-childrenReact.ReactNode-Also accepts all View props from React Native.
InputGroupInput#
PropTypeDefault
classNamestring-Also accepts all TextInput props from React Native.
InputGroupButton#
PropTypeDefault
classNamestring-childrenReact.ReactNode-Also accepts all Pressable props from React Native.
InputGroupText#
PropTypeDefault
classNamestring-Also accepts all Text props from React Native.
Accessibility#
InputGroupButtonusesaccessibilityRole="button"andaccessible={true}.- Minimum touch target of 48dp on interactive elements.
- Use
accessibilityLabelon addons that contain icons instead of text.
Source#
components/ui/input-group.tsx
import React, { createContext, useContext, useState, useCallback } from "react";
import { View, Text, TextInput, Pressable } from "react-native";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { useThemeColors } from "@/components/ui/theme-provider";
// Focus context — container shows ring when child input is focused
const FocusCtx = createContext<{ focused: boolean; setFocused: (v: boolean) => void }>({
focused: false,
setFocused: () => {},
});
export interface InputGroupProps extends React.ComponentPropsWithoutRef<typeof View> {
className?: string;
children?: React.ReactNode;
}
export function InputGroup({ className, children, ...props }: InputGroupProps) {
const [focused, setFocused] = useState(false);
return (
<FocusCtx.Provider value={{ focused, setFocused }}>
<View
className={cn(
"flex-row items-center rounded-md border bg-background",
focused ? "border-ring ring-1 ring-ring" : "border-input",
className
)}
{...props}
>
{children}
</View>
</FocusCtx.Provider>
);
}
export interface InputGroupAddonProps extends React.ComponentPropsWithoutRef<typeof View> {
className?: string;
align?: "start" | "end";
children?: React.ReactNode;
}
export function InputGroupAddon({ className, align = "start", children, ...props }: InputGroupAddonProps) {
return (
<View
className={cn(
"items-center justify-center px-3 self-stretch bg-muted/40",
align === "start" ? "border-e border-input rounded-s-md" : "border-s border-input rounded-e-md",
className
)}
{...props}
>
{children}
</View>
);
}
export interface InputGroupInputProps extends React.ComponentPropsWithoutRef<typeof TextInput> {
className?: string;
}
export function InputGroupInput({ className, onFocus, onBlur, ...props }: InputGroupInputProps) {
const { setFocused } = useContext(FocusCtx);
const colors = useThemeColors();
const handleFocus = useCallback((e: Parameters<NonNullable<typeof onFocus>>[0]) => {
setFocused(true);
onFocus?.(e);
}, [setFocused, onFocus]);
const handleBlur = useCallback((e: Parameters<NonNullable<typeof onBlur>>[0]) => {
setFocused(false);
onBlur?.(e);
}, [setFocused, onBlur]);
return (
<TextInput
className={cn("flex-1 min-h-12 px-3 text-base text-foreground", className)}
placeholderTextColor={colors.mutedForeground}
onFocus={handleFocus}
onBlur={handleBlur}
{...props}
/>
);
}
export interface InputGroupTextareaProps extends React.ComponentPropsWithoutRef<typeof TextInput> {
className?: string;
}
export function InputGroupTextarea({ className, onFocus, onBlur, ...props }: InputGroupTextareaProps) {
const { setFocused } = useContext(FocusCtx);
const colors = useThemeColors();
const handleFocus = useCallback((e: Parameters<NonNullable<typeof onFocus>>[0]) => {
setFocused(true);
onFocus?.(e);
}, [setFocused, onFocus]);
const handleBlur = useCallback((e: Parameters<NonNullable<typeof onBlur>>[0]) => {
setFocused(false);
onBlur?.(e);
}, [setFocused, onBlur]);
return (
<TextInput
className={cn("flex-1 min-h-24 px-3 py-3 text-base text-foreground", className)}
placeholderTextColor={colors.mutedForeground}
multiline
textAlignVertical="top"
onFocus={handleFocus}
onBlur={handleBlur}
{...props}
/>
);
}
const buttonVariants = cva("items-center justify-center active:opacity-70", {
variants: {
size: {
xs: "px-2.5 min-h-8",
sm: "px-3 min-h-10",
"icon-xs": "h-8 w-8",
"icon-sm": "h-10 w-10",
},
variant: {
ghost: "bg-transparent",
default: "bg-primary rounded-md",
secondary: "bg-secondary rounded-md",
outline: "border border-input rounded-md",
},
},
defaultVariants: { size: "xs", variant: "ghost" },
});
const buttonTextVariants = cva("text-xs font-medium", {
variants: {
variant: {
ghost: "text-muted-foreground",
default: "text-primary-foreground",
secondary: "text-secondary-foreground",
outline: "text-foreground",
},
},
defaultVariants: { variant: "ghost" },
});
export interface InputGroupButtonProps
extends React.ComponentPropsWithoutRef<typeof Pressable>,
VariantProps<typeof buttonVariants> {
className?: string;
textClassName?: string;
children?: React.ReactNode;
}
export function InputGroupButton({
className,
textClassName,
size,
variant,
children,
...props
}: InputGroupButtonProps) {
return (
<Pressable
className={cn(buttonVariants({ size, variant }), className)}
accessible={true}
accessibilityRole="button"
{...props}
>
{typeof children === "string" ? (
<Text className={cn(buttonTextVariants({ variant }), textClassName)}>{children}</Text>
) : (
children
)}
</Pressable>
);
}
export interface InputGroupTextProps extends React.ComponentPropsWithoutRef<typeof Text> {
className?: string;
}
export function InputGroupText({ className, ...props }: InputGroupTextProps) {
return <Text className={cn("text-sm text-muted-foreground", className)} {...props} />;
}

