Input
Input and InputGroup components for text input with icons, loaders, and buttons
registry/preskok/ui/preskok-ui/input.tsx
"use client"
import { twMerge } from "cn"
import { Group, type GroupProps } from "react-aria-components/Group"
import {
Input as InputPrimitive,
type InputProps as PrimitiveInputProps,
} from "react-aria-components/Input"
import { cx } from "@/lib/primitive"
import {
Button,
type ButtonProps,
} from "@/components/ui/preskok-ui/button"
interface InputProps extends PrimitiveInputProps {
ref?: React.RefObject<HTMLInputElement>
}
export function Input({ className, ref, ...props }: InputProps) {
return (
<span data-slot="control" className="relative block w-full">
<InputPrimitive
ref={ref}
className={cx(
"relative block w-full appearance-none rounded-lg px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2.5)-1px)] sm:px-[calc(--spacing(3)-1px)] sm:py-[calc(--spacing(1.5)-1px)]",
"text-foreground placeholder:text-muted-foreground text-base/6 sm:text-sm/6",
"border-input enabled:hover:border-muted-foreground/30 border",
"focus:border-ring/70 focus:ring-ring/20 focus:enabled:hover:border-ring/80 focus:ring-3 focus:outline-hidden",
"invalid:border-destructive/70 focus:invalid:border-destructive/70 focus:invalid:ring-destructive/20 invalid:enabled:hover:border-destructive/80 focus:invalid:enabled:hover:border-destructive/80",
"[&::-ms-reveal]:hidden [&::-webkit-search-cancel-button]:hidden",
"disabled:bg-muted disabled:opacity-50 forced-colors:in-disabled:text-[GrayText]",
"in-disabled:bg-muted in-disabled:opacity-50 forced-colors:in-disabled:text-[GrayText]",
"dark:scheme-dark",
className
)}
{...props}
/>
</span>
)
}
interface InputGroupProps extends Omit<GroupProps, "className"> {
className?: string
}
export function InputGroup({ className, ...props }: InputGroupProps) {
return (
<Group
data-slot="input-group"
className={twMerge(
"group/input-group border-input relative flex h-11 w-full min-w-0 items-center rounded-lg border transition-colors outline-none sm:h-9",
"hover:border-muted-foreground/30",
"has-[:disabled]:bg-muted has-[:disabled]:opacity-50",
"has-[[data-slot=input-group-control]:focus-visible]:border-ring/70 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/20 has-[[data-slot=input-group-control]:focus-visible]:ring-3",
"has-[[data-slot][aria-invalid=true]]:border-destructive/70 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:ring-3",
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col",
"has-[>[data-align=block-end]]:[&_[data-slot=input-group-control]]:pt-3 has-[>[data-align=block-start]]:[&_[data-slot=input-group-control]]:pb-3",
"has-[>[data-align=inline-end]]:[&_[data-slot=input-group-control]]:pr-1.5 has-[>[data-align=inline-start]]:[&_[data-slot=input-group-control]]:pl-1.5",
"[&>[data-slot=control]]:min-w-0 [&>[data-slot=control]]:flex-1 [&>[data-slot=control]]:self-stretch",
"[&_[data-slot=input-group-control]]:h-full [&_[data-slot=input-group-control]]:rounded-none [&_[data-slot=input-group-control]]:border-0 [&_[data-slot=input-group-control]]:bg-transparent [&_[data-slot=input-group-control]]:shadow-none [&_[data-slot=input-group-control]]:ring-0",
"[&_[data-slot=input-group-control]]:focus:border-transparent [&_[data-slot=input-group-control]]:focus:ring-0 [&_[data-slot=input-group-control]]:focus:outline-hidden",
"[&_[data-slot=input-group-control]]:invalid:border-transparent [&_[data-slot=input-group-control]]:focus:invalid:border-transparent [&_[data-slot=input-group-control]]:focus:invalid:ring-0",
"[&_[data-slot=input-group-control]]:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
interface InputGroupAddonProps extends React.ComponentProps<"div"> {
align?: "inline-start" | "inline-end" | "block-start" | "block-end"
}
const inputGroupAddonStyles = {
"inline-start": "order-first pl-2 has-[>button]:pl-px has-[>kbd]:pl-1.5",
"inline-end": "order-last pr-1.25 has-[>button]:pr-px has-[>kbd]:pr-1.5",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
}
export function InputGroupAddon({
className,
align = "inline-start",
onClick,
...props
}: InputGroupAddonProps) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={twMerge(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none",
"group-data-[disabled=true]/input-group:opacity-50",
"[&>kbd]:rounded-[calc(var(--radius-lg)-5px)] [&>svg:not([class*='size-'])]:size-4",
inputGroupAddonStyles[align],
className
)}
onClick={(event) => {
onClick?.(event)
if (
event.defaultPrevented ||
(event.target as HTMLElement).closest("button")
) {
return
}
event.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
}
interface InputGroupButtonProps extends Omit<ButtonProps, "size"> {
size?: "xs" | "sm" | "icon-xs" | "icon-sm"
}
const inputGroupButtonSizes = {
xs: {
button: "xs",
className:
"h-6 gap-1 rounded-[calc(var(--radius-lg)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
},
sm: {
button: "sm",
className:
"h-7 rounded-[calc(var(--radius-lg)-2px)] px-2.5 [&>svg:not([class*='size-'])]:size-4",
},
"icon-xs": {
button: "sq-xs",
className: "size-6 rounded-[calc(var(--radius-lg)-3px)] p-0",
},
"icon-sm": {
button: "sq-sm",
className: "size-8 rounded-[calc(var(--radius-lg)-2px)] p-0",
},
} as const
export function InputGroupButton({
className,
type = "button",
intent = "plain",
size = "xs",
...props
}: InputGroupButtonProps) {
const sizeStyles = inputGroupButtonSizes[size]
return (
<Button
type={type}
data-slot="input-group-button"
data-size={size}
intent={intent}
size={sizeStyles.button}
className={cx("shrink-0 shadow-none", sizeStyles.className, className)}
{...props}
/>
)
}
export function InputGroupText({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="input-group-text"
className={twMerge(
"text-muted-foreground flex items-center gap-2 text-sm",
"[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
export function InputGroupInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="input-group-control"
className={cx("flex-1", className)}
{...props}
/>
)
}
No selection tags yet — empty means no evidence, not lack of support.
DependenciesBundle weight · light
react-aria-components