Input Group
Add addons, buttons, and helper content to inputs.
12 results
https://
This is content in a tooltip.
Auto
Agent
Manual
import { css } from 'remix/component'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,import { css } from 'remix/component'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '#app/ui/dropdown-menu.tsx'
import { ArrowUpIcon, CheckIcon, InfoIcon, PlusIcon, SearchIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
inputGroupButtonVariants,
} from '#app/ui/input-group.tsx'
import { Separator } from '#app/ui/separator.tsx'
import { Tooltip, TooltipContent, TooltipTrigger } from '#app/ui/tooltip.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '24rem', gap: '1.5rem' })
const srOnly = css({
position: 'absolute',
width: '1px',
height: '1px',
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
borderWidth: 0,
})
const tightInput = css({ paddingLeft: '0.25rem' })
const roundedFull = css({ borderRadius: '9999px' })
const roundedMenu = css({ '--radius': '0.95rem' })
const pushRight = css({ marginLeft: 'auto' })
const separatorStyles = css({ height: '1rem' })
const badgeStyles = css({
display: 'flex',
width: '1rem',
height: '1rem',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '9999px',
backgroundColor: 'var(--primary)',
color: 'var(--primary-foreground)',
})
const badgeIconStyles = css({ width: '0.75rem', height: '0.75rem' })
export function InputGroupDemo() {
return () => (
<div mix={gridStyles}>
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="example.com" mix={tightInput} />
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
data-slot="button"
data-size="icon-xs"
mix={[inputGroupButtonVariants({ size: 'icon-xs' }), roundedFull]}
>
<InfoIcon />
</TooltipTrigger>
<TooltipContent>This is content in a tooltip.</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder="Ask, Search or Chat..." />
<InputGroupAddon align="block-end">
<InputGroupButton variant="outline" size="icon-xs" mix={roundedFull}>
<PlusIcon />
</InputGroupButton>
<DropdownMenu>
<DropdownMenuTrigger
data-slot="button"
data-size="xs"
mix={inputGroupButtonVariants({ variant: 'ghost' })}
>
Auto
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start" mix={roundedMenu}>
<DropdownMenuItem>Auto</DropdownMenuItem>
<DropdownMenuItem>Agent</DropdownMenuItem>
<DropdownMenuItem>Manual</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupText mix={pushRight}>52% used</InputGroupText>
<Separator orientation="vertical" mix={separatorStyles} />
<InputGroupButton variant="default" size="icon-xs" disabled mix={roundedFull}>
<ArrowUpIcon />
<span mix={srOnly}>Send</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="@shadcn" />
<InputGroupAddon align="inline-end">
<div mix={badgeStyles}>
<CheckIcon mix={badgeIconStyles} />
</div>
</InputGroupAddon>
</InputGroup>
</div>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/input-group.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/input-group.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/input-group.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/input-group.jsonInstall the following dependencies:
$ bun add remix$ npm install remix$ pnpm add remix$ yarn add remixCopy and paste the following code into your project.
import { css, on, type CSSMixinDescriptor, type Handle, type Props } from 'remix/component'
import { buttonVariants, type ButtonProps, type ButtonVariant } from './button.tsx'
import { Input, type InputProps } from './input.tsx'
import { Textarea, type TextareaProps } from './textarea.tsx'
export type InputGroupAddonAlign = 'inline-start' | 'inline-end' | 'block-start' | 'block-end'
export type InputGroupButtonSize = 'xs' | 'sm' | 'icon-xs' | 'icon-sm'
export interface InputGroupProps extends Props<'div'> {}
export interface InputGroupAddonProps extends Props<'div'> {
align?: InputGroupAddonAlign
}
export interface InputGroupButtonProps extends Omit<ButtonProps, 'size'> {
size?: InputGroupButtonSize
}
export interface InputGroupTextProps extends Props<'span'> {}
export type InputGroupInputProps = InputProps
export interface InputGroupTextareaProps extends TextareaProps {}
const inputGroupStyles = css({
position: 'relative',
display: 'flex',
width: '100%',
alignItems: 'center',
borderRadius: 'calc(var(--radius) - 2px)',
borderWidth: '1px',
borderColor: 'var(--input)',
'--tw-shadow': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
boxShadow: 'var(--tw-ring-shadow), var(--tw-shadow)',
transitionProperty: 'color, box-shadow',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
outlineStyle: 'none',
height: '2.25rem',
minWidth: 0,
'.dark &': { backgroundColor: 'color-mix(in oklab, var(--input) 30%, transparent)' },
'&:has(>textarea)': { height: 'auto' },
'&:has(>[data-align="block-start"])': { height: 'auto', flexDirection: 'column' },
'&:has(>[data-align="block-end"])': { height: 'auto', flexDirection: 'column' },
'&:has([data-slot="input-group-control"]:focus-visible)': {
borderColor: 'var(--ring)',
'--tw-ring-shadow':
'0 0 0 3px var(--tw-ring-color, color-mix(in oklab, var(--ring) 50%, transparent))',
},
'&:has([data-slot][aria-invalid="true"])': {
borderColor: 'var(--destructive)',
'--tw-ring-color': 'color-mix(in oklab, var(--destructive) 20%, transparent)',
},
'.dark &:has([data-slot][aria-invalid="true"])': {
'--tw-ring-color': 'color-mix(in oklab, var(--destructive) 40%, transparent)',
},
})
export function InputGroup(handle: Handle<InputGroupProps>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="input-group" role="group" {...props} mix={[inputGroupStyles, mix]} />
}
}
const addonBase = css({
display: 'flex',
height: 'auto',
cursor: 'text',
alignItems: 'center',
justifyContent: 'center',
gap: '0.5rem',
paddingBlock: '0.375rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
fontWeight: 500,
color: 'var(--muted-foreground)',
userSelect: 'none',
'[data-slot="input-group"][data-disabled="true"] &': { opacity: 0.5 },
// `!important`: Tailwind's `[&>kbd]:` beats Kbd's own radius; Kbd styles sit in a later layer.
'& > kbd': { borderRadius: 'calc(var(--radius) - 5px) !important' },
'& > svg:not([class*="size-"])': { width: '1rem', height: '1rem' },
})
const addonAligns: Record<InputGroupAddonAlign, CSSMixinDescriptor> = {
'inline-start': css({
order: '-9999',
paddingLeft: '0.75rem',
'&:has(>button)': { marginLeft: '-0.45rem' },
'&:has(>kbd)': { marginLeft: '-0.35rem' },
}),
'inline-end': css({
order: '9999',
paddingRight: '0.75rem',
'&:has(>button)': { marginRight: '-0.45rem' },
'&:has(>kbd)': { marginRight: '-0.35rem' },
}),
'block-start': css({
order: '-9999',
width: '100%',
justifyContent: 'flex-start',
paddingInline: '0.75rem',
paddingTop: '0.75rem',
'[data-slot="input-group"]:has(>input) &': { paddingTop: '0.625rem' },
}),
'block-end': css({
order: '9999',
width: '100%',
justifyContent: 'flex-start',
paddingInline: '0.75rem',
paddingBottom: '0.75rem',
'[data-slot="input-group"]:has(>input) &': { paddingBottom: '0.625rem' },
}),
}
/** The `inputGroupAddonVariants()` of shadcn/ui, as a mix. */
export function inputGroupAddonVariants({ align = 'inline-start' }: { align?: InputGroupAddonAlign } = {}) {
return [addonBase, addonAligns[align]]
}
/** Clicking the addon (but not a button inside it) focuses the group's input. */
const focusInputOnClick = on<HTMLDivElement>('click', (event) => {
if ((event.target as HTMLElement).closest('button')) return
event.currentTarget.parentElement?.querySelector('input')?.focus()
})
export function InputGroupAddon(handle: Handle<InputGroupAddonProps>) {
return () => {
let { align = 'inline-start', mix, ...props } = handle.props
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
{...props}
mix={[inputGroupAddonVariants({ align }), focusInputOnClick, mix]}
/>
)
}
}
const buttonBase = css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
'--tw-shadow': '0 0 #0000',
})
const buttonSizes: Record<InputGroupButtonSize, CSSMixinDescriptor> = {
xs: css({
height: '1.5rem',
gap: '0.25rem',
borderRadius: 'calc(var(--radius) - 5px)',
paddingInline: '0.5rem',
'&:has(>svg)': { paddingInline: '0.5rem' },
'& > svg:not([class*="size-"])': { width: '0.875rem', height: '0.875rem' },
}),
sm: css({
height: '2rem',
gap: '0.375rem',
borderRadius: 'calc(var(--radius) - 2px)',
paddingInline: '0.625rem',
'&:has(>svg)': { paddingInline: '0.625rem' },
}),
'icon-xs': css({
width: '1.5rem',
height: '1.5rem',
borderRadius: 'calc(var(--radius) - 5px)',
padding: 0,
'&:has(>svg)': { padding: 0 },
}),
'icon-sm': css({
width: '2rem',
height: '2rem',
padding: 0,
'&:has(>svg)': { padding: 0 },
}),
}
/**
* The full InputGroupButton look (Button's `variant` styles plus the input-group sizes) as a mix,
* shadcn's `asChild` equivalent: `<TooltipTrigger mix={inputGroupButtonVariants({ size: 'icon-xs' })}>`.
*/
export function inputGroupButtonVariants({
variant = 'ghost',
size = 'xs',
}: { variant?: ButtonVariant; size?: InputGroupButtonSize } = {}) {
return [buttonVariants({ variant }), buttonBase, buttonSizes[size]]
}
export function InputGroupButton(handle: Handle<InputGroupButtonProps>) {
return () => {
let { type = 'button', variant = 'ghost', size = 'xs', mix, ...props } = handle.props
return (
<button
type={type}
data-slot="button"
data-variant={variant}
data-size={size}
{...props}
mix={[inputGroupButtonVariants({ variant, size }), mix]}
/>
)
}
}
const textStyles = css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
color: 'var(--muted-foreground)',
'& svg': { pointerEvents: 'none' },
'& svg:not([class*="size-"])': { width: '1rem', height: '1rem' },
})
export function InputGroupText(handle: Handle<InputGroupTextProps>) {
return () => {
let { mix, ...props } = handle.props
return <span {...props} mix={[textStyles, mix]} />
}
}
const controlStyles = {
flex: '1 1 0%',
borderRadius: 0,
borderWidth: 0,
backgroundColor: 'transparent',
'--tw-shadow': '0 0 #0000',
'&:focus-visible': { '--tw-ring-shadow': '0 0 #0000' },
'.dark &': { backgroundColor: 'transparent' },
}
const inputStyles = css({
...controlStyles,
// The group's `has-[>[data-align=…]]:[&>input]:` rules. They live on the input so they come
// after the Input's own padding in the cascade.
'[data-slot="input-group"]:has(>[data-align="inline-start"]) > &': { paddingLeft: '0.5rem' },
'[data-slot="input-group"]:has(>[data-align="inline-end"]) > &': { paddingRight: '0.5rem' },
'[data-slot="input-group"]:has(>[data-align="block-start"]) > &': { paddingBottom: '0.75rem' },
'[data-slot="input-group"]:has(>[data-align="block-end"]) > &': { paddingTop: '0.75rem' },
})
export function InputGroupInput(handle: Handle<InputGroupInputProps>) {
return () => {
let { mix, ...props } = handle.props
return <Input data-slot="input-group-control" {...props} mix={[inputStyles, mix]} />
}
}
const textareaStyles = css({
...controlStyles,
resize: 'none',
paddingBlock: '0.75rem',
})
export function InputGroupTextarea(handle: Handle<InputGroupTextareaProps>) {
return () => {
let { mix, ...props } = handle.props
return <Textarea data-slot="input-group-control" {...props} mix={[textareaStyles, mix]} />
}
}Update the import paths to match your project setup.
Usage
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from '#app/ui/input-group.tsx'<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>Examples
Icon
styleName="radix-nova"
import { css } from 'remix/component'
import {
CheckIcon,
CreditCardIcon,
InfoIcon,import { css } from 'remix/component'
import {
CheckIcon,
CreditCardIcon,
InfoIcon,
MailIcon,
SearchIcon,
StarIcon,
} from '#app/ui/icons.tsx'
import { InputGroup, InputGroupAddon, InputGroupInput } from '#app/ui/input-group.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '24rem', gap: '1.5rem' })
export function InputGroupIcon() {
return () => (
<div mix={gridStyles}>
<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput type="email" placeholder="Enter your email" />
<InputGroupAddon>
<MailIcon />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Card number" />
<InputGroupAddon>
<CreditCardIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<CheckIcon />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Card number" />
<InputGroupAddon align="inline-end">
<StarIcon />
<InfoIcon />
</InputGroupAddon>
</InputGroup>
</div>
)
}Text
styleName="radix-nova"
$
USD
https://
.com
@company.com
120 characters left
import { css } from 'remix/component'
import {
InputGroup,
InputGroupAddon,
InputGroupInput,import { css } from 'remix/component'
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from '#app/ui/input-group.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '24rem', gap: '1.5rem' })
const tightInput = css({ paddingLeft: '0.125rem' })
const counterStyles = css({
fontSize: '0.75rem',
lineHeight: 'calc(1 / 0.75)',
color: 'var(--muted-foreground)',
})
export function InputGroupTextExample() {
return () => (
<div mix={gridStyles}>
<InputGroup>
<InputGroupAddon>
<InputGroupText>$</InputGroupText>
</InputGroupAddon>
<InputGroupInput placeholder="0.00" />
<InputGroupAddon align="inline-end">
<InputGroupText>USD</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupAddon>
<InputGroupText>https://</InputGroupText>
</InputGroupAddon>
<InputGroupInput placeholder="example.com" mix={tightInput} />
<InputGroupAddon align="inline-end">
<InputGroupText>.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Enter your username" />
<InputGroupAddon align="inline-end">
<InputGroupText>@company.com</InputGroupText>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder="Enter your message" />
<InputGroupAddon align="block-end">
<InputGroupText mix={counterStyles}>120 characters left</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
)
}Button
styleName="radix-nova"
Your connection is not secure.
You should not enter any sensitive information on this site.
https://
import { css, on, type Handle } from 'remix/component'
import { CheckIcon, CopyIcon, InfoIcon, StarIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,import { css, on, type Handle } from 'remix/component'
import { CheckIcon, CopyIcon, InfoIcon, StarIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
inputGroupButtonVariants,
} from '#app/ui/input-group.tsx'
import { Popover, PopoverContent, PopoverTrigger } from '#app/ui/popover.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '24rem', gap: '1.5rem' })
const pillGroup = css({ '--radius': '9999px' })
const prefixStyles = css({ paddingLeft: '0.375rem', color: 'var(--muted-foreground)' })
const popoverStyles = css({
display: 'flex',
flexDirection: 'column',
gap: '0.25rem',
borderRadius: 'calc(var(--radius) + 4px)',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
})
const titleStyles = css({ fontWeight: 500 })
const starStyles = css({
'&[data-favorite="true"]': {
fill: 'oklch(54.6% 0.245 262.881)',
stroke: 'oklch(54.6% 0.245 262.881)',
},
})
export function InputGroupButtonExample(handle: Handle) {
let isCopied = false
let isFavorite = false
let resetCopied: ReturnType<typeof setTimeout> | undefined
handle.signal.addEventListener('abort', () => clearTimeout(resetCopied))
return () => (
<div mix={gridStyles}>
<InputGroup>
<InputGroupInput placeholder="https://x.com/shadcn" readOnly />
<InputGroupAddon align="inline-end">
<InputGroupButton
aria-label="Copy"
title="Copy"
size="icon-xs"
mix={on('click', async () => {
await navigator.clipboard.writeText('https://x.com/shadcn')
isCopied = true
handle.update()
clearTimeout(resetCopied)
resetCopied = setTimeout(() => {
isCopied = false
handle.update()
}, 2000)
})}
>
{isCopied ? <CheckIcon /> : <CopyIcon />}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup mix={pillGroup}>
<InputGroupAddon>
<Popover>
<PopoverTrigger
data-slot="button"
data-size="icon-xs"
mix={inputGroupButtonVariants({ variant: 'secondary', size: 'icon-xs' })}
>
<InfoIcon />
</PopoverTrigger>
<PopoverContent align="start" mix={popoverStyles}>
<p mix={titleStyles}>Your connection is not secure.</p>
<p>You should not enter any sensitive information on this site.</p>
</PopoverContent>
</Popover>
</InputGroupAddon>
<InputGroupAddon mix={prefixStyles}>https://</InputGroupAddon>
<InputGroupInput id="input-secure-19" />
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
mix={on('click', () => {
isFavorite = !isFavorite
handle.update()
})}
>
<StarIcon data-favorite={String(isFavorite)} mix={starStyles} />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Type to search..." />
<InputGroupAddon align="inline-end">
<InputGroupButton variant="secondary">Search</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}Dropdown
styleName="radix-nova"
Settings
Copy path
Open location
Documentation
Blog Posts
Changelog
import { css } from 'remix/component'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,import { css } from 'remix/component'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '#app/ui/dropdown-menu.tsx'
import { ChevronDownIcon, MoreHorizontalIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
inputGroupButtonVariants,
} from '#app/ui/input-group.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '24rem', gap: '1rem' })
const roundedGroup = css({ '--radius': '1rem' })
const roundedMenu = css({ '--radius': '0.95rem' })
const searchInStyles = css({
paddingRight: '0.375rem',
fontSize: '0.75rem',
lineHeight: 'calc(1 / 0.75)',
})
const chevronStyles = css({ width: '0.75rem', height: '0.75rem' })
export function InputGroupDropdown() {
return () => (
<div mix={gridStyles}>
<InputGroup>
<InputGroupInput placeholder="Enter file name" />
<InputGroupAddon align="inline-end">
<DropdownMenu>
<DropdownMenuTrigger
aria-label="More"
data-slot="button"
data-size="icon-xs"
mix={inputGroupButtonVariants({ variant: 'ghost', size: 'icon-xs' })}
>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Copy path</DropdownMenuItem>
<DropdownMenuItem>Open location</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</InputGroupAddon>
</InputGroup>
<InputGroup mix={roundedGroup}>
<InputGroupInput placeholder="Enter search query" />
<InputGroupAddon align="inline-end">
<DropdownMenu>
<DropdownMenuTrigger
data-slot="button"
data-size="xs"
mix={[inputGroupButtonVariants({ variant: 'ghost' }), searchInStyles]}
>
Search In... <ChevronDownIcon mix={chevronStyles} />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" mix={roundedMenu}>
<DropdownMenuItem>Documentation</DropdownMenuItem>
<DropdownMenuItem>Blog Posts</DropdownMenuItem>
<DropdownMenuItem>Changelog</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</InputGroupAddon>
</InputGroup>
</div>
)
}Spinner
styleName="radix-nova"
Saving...
Please wait...
import { css } from 'remix/component'
import { LoaderIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,import { css } from 'remix/component'
import { LoaderIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from '#app/ui/input-group.tsx'
import { Spinner } from '#app/ui/spinner.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '24rem', gap: '1rem' })
const spinStyles = css({ animation: 'spin 1s linear infinite' })
const mutedStyles = css({ color: 'var(--muted-foreground)' })
export function InputGroupSpinner() {
return () => (
<div mix={gridStyles}>
<InputGroup data-disabled="true">
<InputGroupInput placeholder="Searching..." disabled />
<InputGroupAddon align="inline-end">
<Spinner />
</InputGroupAddon>
</InputGroup>
<InputGroup data-disabled="true">
<InputGroupInput placeholder="Processing..." disabled />
<InputGroupAddon>
<Spinner />
</InputGroupAddon>
</InputGroup>
<InputGroup data-disabled="true">
<InputGroupInput placeholder="Saving changes..." disabled />
<InputGroupAddon align="inline-end">
<InputGroupText>Saving...</InputGroupText>
<Spinner />
</InputGroupAddon>
</InputGroup>
<InputGroup data-disabled="true">
<InputGroupInput placeholder="Refreshing data..." disabled />
<InputGroupAddon>
<LoaderIcon mix={spinStyles} />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupText mix={mutedStyles}>Please wait...</InputGroupText>
</InputGroupAddon>
</InputGroup>
</div>
)
}Textarea
styleName="radix-nova"
Line 1, Column 1
script.js
import { css } from 'remix/component'
import { CopyIcon, CornerDownLeftIcon, FileCodeIcon, RefreshCwIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,import { css } from 'remix/component'
import { CopyIcon, CornerDownLeftIcon, FileCodeIcon, RefreshCwIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupTextarea,
} from '#app/ui/input-group.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '28rem', gap: '1rem' })
const editorStyles = css({ minHeight: '200px' })
// `className="border-t"` / `"border-b"`; the addon's `[.border-t]:pt-3` rule comes along.
const borderTop = css({ borderTopWidth: '1px', paddingTop: '0.75rem' })
const borderBottom = css({ borderBottomWidth: '1px', paddingBottom: '0.75rem' })
const monoStyles = css({
fontFamily: 'var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace)',
fontWeight: 500,
})
const pushRight = css({ marginLeft: 'auto' })
export function InputGroupTextareaExample() {
return () => (
<div mix={gridStyles}>
<InputGroup>
<InputGroupTextarea
id="textarea-code-32"
placeholder="console.log('Hello, world!');"
mix={editorStyles}
/>
<InputGroupAddon align="block-end" mix={borderTop}>
<InputGroupText>Line 1, Column 1</InputGroupText>
<InputGroupButton size="sm" variant="default" mix={pushRight}>
Run <CornerDownLeftIcon />
</InputGroupButton>
</InputGroupAddon>
<InputGroupAddon align="block-start" mix={borderBottom}>
<InputGroupText mix={monoStyles}>
<FileCodeIcon />
script.js
</InputGroupText>
<InputGroupButton size="icon-xs" mix={pushRight}>
<RefreshCwIcon />
</InputGroupButton>
<InputGroupButton variant="ghost" size="icon-xs">
<CopyIcon />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}Custom Input
Add the data-slot="input-group-control" attribute to your custom input for automatic focus state handling. Here's an example of a custom resizable textarea from a third-party library.
import { css } from 'remix/component'
import { InputGroup, InputGroupAddon, InputGroupButton } from '#app/ui/input-group.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '24rem', gap: '1.5rem' })
// shadcn uses react-textarea-autosize; a native textarea with `field-sizing: content` grows the same way.import { css } from 'remix/component'
import { InputGroup, InputGroupAddon, InputGroupButton } from '#app/ui/input-group.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '24rem', gap: '1.5rem' })
// shadcn uses react-textarea-autosize; a native textarea with `field-sizing: content` grows the same way.
const autosizeStyles = css({
display: 'flex',
fieldSizing: 'content',
minHeight: '4rem',
width: '100%',
resize: 'none',
borderRadius: 'calc(var(--radius) - 2px)',
backgroundColor: 'transparent',
paddingInline: '0.75rem',
paddingBlock: '0.625rem',
fontSize: '1rem',
lineHeight: 1.5,
transitionProperty: 'color, box-shadow',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
outlineStyle: 'none',
'@media (min-width: 48rem)': { fontSize: '0.875rem', lineHeight: 'calc(1.25 / 0.875)' },
})
const pushRight = css({ marginLeft: 'auto' })
export function InputGroupCustom() {
return () => (
<div mix={gridStyles}>
<InputGroup>
<textarea
data-slot="input-group-control"
placeholder="Autoresize textarea..."
mix={autosizeStyles}
/>
<InputGroupAddon align="block-end">
<InputGroupButton size="sm" variant="default" mix={pushRight}>
Submit
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}Label
We'll use this to send you notifications
import { css } from 'remix/component'
import { InfoIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,import { css } from 'remix/component'
import { InfoIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
inputGroupButtonVariants,
} from '#app/ui/input-group.tsx'
import { Label } from '#app/ui/label.tsx'
import { Tooltip, TooltipContent, TooltipTrigger } from '#app/ui/tooltip.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '24rem', gap: '1rem' })
const foregroundStyles = css({ color: 'var(--foreground)' })
const helpButtonStyles = css({ marginLeft: 'auto', borderRadius: '9999px' })
export function InputGroupLabel() {
return () => (
<div mix={gridStyles}>
<InputGroup>
<InputGroupInput id="email" placeholder="shadcn" />
<InputGroupAddon>
<Label htmlFor="email">@</Label>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput id="email-2" placeholder="shadcn@vercel.com" />
<InputGroupAddon align="block-start">
<Label htmlFor="email-2" mix={foregroundStyles}>
Email
</Label>
<Tooltip>
<TooltipTrigger
aria-label="Help"
data-slot="button"
data-size="icon-xs"
mix={[inputGroupButtonVariants({ variant: 'ghost', size: 'icon-xs' }), helpButtonStyles]}
>
<InfoIcon />
</TooltipTrigger>
<TooltipContent>
<p>We'll use this to send you notifications</p>
</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</div>
)
}Tooltip
Password must be at least 8 characters
We'll use this to send you notifications
Click for help with API keys
import { css } from 'remix/component'
import { HelpCircleIcon, InfoIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,import { css } from 'remix/component'
import { HelpCircleIcon, InfoIcon } from '#app/ui/icons.tsx'
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
inputGroupButtonVariants,
} from '#app/ui/input-group.tsx'
import { Tooltip, TooltipContent, TooltipTrigger } from '#app/ui/tooltip.tsx'
const gridStyles = css({ display: 'grid', width: '100%', maxWidth: '24rem', gap: '1rem' })
export function InputGroupTooltip() {
return () => (
<div mix={gridStyles}>
<InputGroup>
<InputGroupInput placeholder="Enter password" type="password" />
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
aria-label="Info"
data-slot="button"
data-size="icon-xs"
mix={inputGroupButtonVariants({ variant: 'ghost', size: 'icon-xs' })}
>
<InfoIcon />
</TooltipTrigger>
<TooltipContent>
<p>Password must be at least 8 characters</p>
</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Your email address" />
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
aria-label="Help"
data-slot="button"
data-size="icon-xs"
mix={inputGroupButtonVariants({ variant: 'ghost', size: 'icon-xs' })}
>
<HelpCircleIcon />
</TooltipTrigger>
<TooltipContent>
<p>We'll use this to send you notifications</p>
</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="Enter API key" />
<InputGroupAddon>
<Tooltip>
<TooltipTrigger
aria-label="Help"
data-slot="button"
data-size="icon-xs"
mix={inputGroupButtonVariants({ variant: 'ghost', size: 'icon-xs' })}
>
<HelpCircleIcon />
</TooltipTrigger>
<TooltipContent side="left">
<p>Click for help with API keys</p>
</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</div>
)
}API Reference
InputGroup
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
InputGroupAddon
| Prop | Type | Default |
|---|---|---|
align | InputGroupAddonAlign | "inline-start" |
mix | MixValue | - |
InputGroupButton
| Prop | Type | Default |
|---|---|---|
size | InputGroupButtonSize | "xs" |
variant | ButtonVariant | "ghost" |
mix | MixValue | - |
InputGroupText
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
InputGroupInput
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
InputGroupTextarea
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |