AniUI

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-group

Usage#

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
InputGroup

Root container that provides the bordered row layout.

InputGroupAddon

Non-interactive addon area. Supports start and end alignment.

InputGroupInput

The text input element. Expands to fill available space.

InputGroupButton

Pressable button placed inside the input group.

InputGroupText

Styled text for use inside addons.

Props#

InputGroup#

PropTypeDefault
className
string
-
children
React.ReactNode
-

Also accepts all View props from React Native.

InputGroupAddon#

PropTypeDefault
align
"start" | "end"
"start"
className
string
-
children
React.ReactNode
-

Also accepts all View props from React Native.

InputGroupInput#

PropTypeDefault
className
string
-

Also accepts all TextInput props from React Native.

InputGroupButton#

PropTypeDefault
className
string
-
children
React.ReactNode
-

Also accepts all Pressable props from React Native.

InputGroupText#

PropTypeDefault
className
string
-

Also accepts all Text props from React Native.

Accessibility#

  • InputGroupButton uses accessibilityRole="button" and accessible={true}.
  • Minimum touch target of 48dp on interactive elements.
  • Use accessibilityLabel on 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} />;
}