Input Group
Leading and trailing addons around a TextInput, with its own focus ring (React Native has no :focus-within). Backs prompt-input and every filter field.
- v0.2.1
- ui
- nativewind
- uniwind
- runs in Expo Go
Install
npx @react-native-reusables/cli@latest add https://rnraielements.com/r/nativewind/input-group.jsonnpx @react-native-reusables/cli@latest add https://rnraielements.com/r/uniwind/input-group.jsonDependencies
No required npm packages — this item installs through the registry dependencies below.
https://reactnativereusables.com/r/nativewind/text.json |
https://reactnativereusables.com/r/nativewind/input.json |
https://reactnativereusables.com/r/nativewind/button.json |
https://reactnativereusables.com/r/uniwind/text.json |
https://reactnativereusables.com/r/uniwind/input.json |
https://reactnativereusables.com/r/uniwind/button.json |
AI SDK peers:ai@^7.0.89@ai-sdk/react@^4.0.92
Source
import { Text, TextClassContext } from '@/registry/nativewind/components/ui/text';
import { cn } from '@/registry/nativewind/lib/utils';
import * as React from 'react';
import { Platform, TextInput, View, useColorScheme, type TextInputProps, type ViewProps } from 'react-native';
/**
* InputGroup — leading/trailing addons around a TextInput.
*
* RNR ships no `input-group`. Highest-leverage item of the whole gap: prompt-input is
* built on it, and so are the filter fields in command, attachments and
* environment-variables.
*
* It owns its own focus ring. React Native has no `:focus-within`, so the ring is driven
* by onFocus/onBlur state on the wrapper rather than by a CSS selector — without this the
* group simply never shows focus, silently.
*/
type InputGroupContextValue = { focused: boolean; setFocused: (v: boolean) => void };
const InputGroupContext = React.createContext<InputGroupContextValue | null>(null);
function useInputGroup() {
const ctx = React.useContext(InputGroupContext);
if (!ctx) throw new Error('InputGroup sub-components must be used within <InputGroup>');
return ctx;
}
function InputGroup({ className, ...props }: ViewProps) {
const [focused, setFocused] = React.useState(false);
const scheme = useColorScheme();
// The `ring-*` utility family (ring-2 width, ring-ring/50 color) is parsed by
// NativeWind v4's css-interop at render time, which races Expo Router's
// NavigationContainer init (the upstream nativewind css-interop race): on focus the
// conditional `ring-*` term first attaches and the re-render throws "Couldn't find a
// navigation context". The 2px focus ring therefore goes through the plain RN `style`
// prop — the engine-safe channel the placeholder color below already uses — as a 2px
// in-box border in the `--color-ring` token value at 50% opacity (ring light
// hsl(0 0% 63%), dark hsl(300 0% 45%)). If the theme tokens change, these literals
// change with them. border-ring (full ring color) stays on the className but is
// overridden by this borderColor, so it is cosmetic.
const focusedStyle = focused
? { borderColor: scheme === 'dark' ? 'hsla(300, 0%, 45%, 0.5)' : 'hsla(0, 0%, 63%, 0.5)', borderWidth: 2 }
: undefined;
return (
<InputGroupContext.Provider value={{ focused, setFocused }}>
<View
className={cn(
'flex-row items-center gap-2 rounded-md border border-input bg-background px-3',
'dark:bg-input/30',
focused && 'border-ring',
className,
)}
style={focusedStyle}
{...props}
/>
</InputGroupContext.Provider>
);
}
/** The text field. Reports focus up so the wrapper can draw the ring. */
function InputGroupInput({ className, onFocus, onBlur, ...props }: TextInputProps) {
const { setFocused } = useInputGroup();
const scheme = useColorScheme();
return (
<TextInput
className={cn(
'h-10 flex-1 py-2 text-base text-foreground sm:h-9',
Platform.select({ web: 'outline-none' }),
className,
)}
// PLACEHOLDER COLOR CANNOT BE A CLASS HERE. `placeholderClassName` is a
// Nativewind-only prop; uniwind 1.11.0 has no such prop anywhere (its own
// TextInput component uses `placeholderTextColorClassName`, and a raw
// react-native TextInput accepts neither) — so under the uniwind engine the
// prop was inert and placeholders fell to the platform default. This registry
// is engine-agnostic and cannot import uniwind's TextInput, so the color goes
// through the one hook both engines and all three platforms honor: the plain
// RN prop, carrying the exact --color-muted-foreground theme values (light
// hsl(0 0% 45.1%) = #737373, dark hsl(0 0% 63.9%) = #a3a3a3). If the theme
// tokens change, these two literals change with them.
placeholderTextColor={scheme === 'dark' ? 'hsl(0 0% 63.9%)' : 'hsl(0 0% 45.1%)'}
onFocus={(e) => {
setFocused(true);
onFocus?.(e);
}}
onBlur={(e) => {
setFocused(false);
onBlur?.(e);
}}
{...props}
/>
);
}
/** Non-interactive leading/trailing content — an icon, a `$` prefix, a unit. */
function InputGroupAddon({ className, ...props }: ViewProps) {
return (
<TextClassContext.Provider value="text-sm text-muted-foreground">
<View className={cn('shrink-0 flex-row items-center gap-1', className)} {...props} />
</TextClassContext.Provider>
);
}
/** Interactive trailing slot — a send button, a clear button. */
function InputGroupActions({ className, ...props }: ViewProps) {
return <View className={cn('shrink-0 flex-row items-center gap-1', className)} {...props} />;
}
function InputGroupText({ className, ...props }: React.ComponentProps<typeof Text>) {
return <Text className={cn('text-sm text-muted-foreground', className)} {...props} />;
}
export { InputGroup, InputGroupActions, InputGroupAddon, InputGroupInput, InputGroupText };
Theming
Themed via ring-ring, border-ring, border-input, bg-background, bg-input, text-foreground, text-muted-foreground.
Usage
() => (
<InputGroup>
<InputGroupAddon>
<Text className="text-sm text-muted-foreground">$</Text>
</InputGroupAddon>
<InputGroupInput placeholder="pnpm registry:build" />
<InputGroupAddon>
<FolderIcon size={14} className="text-muted-foreground" />
</InputGroupAddon>
</InputGroup>
)