Nhóm ô nhập
Gắn các biểu tượng, nút bấm và chú thích bàn phím vào các trường nhập liệu dành cho thanh tìm kiếm, trường URL và trình soạn thảo trò chuyện — với viền và bóng theo phong cách neobrutalist.
import { Search } from "lucide-react"
import {Input group gói một input hoặc textarea cùng các phần phụ trợ của nó — biểu tượng, chữ nội tuyến, nút, gợi ý phím — vào bên trong một khung điều khiển có viền duy nhất. Đây là cách kết hợp nội bộ giữa Input, Textarea và Button của chính thư viện này, tạo kiểu theo công thức neobrutalist: viền dày, bóng đổ đậm và kiểu chữ đậm.
Nên dùng khi:
- Ô tìm kiếm — biểu tượng kính lúp ở đầu cùng gợi ý
⌘Khoặc nút xóa ở cuối. - Input có tiền tố và hậu tố — phần chữ cố định như
https://,@hay.comđể người dùng chỉ phải gõ phần thay đổi. - Khung soạn tin và soạn prompt — một textarea kèm thanh nút ở
block-endvà một hành động gửi.
Cài đặt#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/input-group.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/input-group.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/input-group.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/input-group.json
Sao chép và dán đoạn mã sau vào dự án của bạn.
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded border-2 bg-input shadow-sm transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:outline-2 has-[[data-slot=input-group-control]:focus-visible]:outline-offset-2 has-[[data-slot=input-group-control]:focus-visible]:outline-primary has-[[data-slot][aria-invalid=true]]:border-destructive 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-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"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",
},
},
defaultVariants: {
align: "inline-start",
},
}
)
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
)
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none outline-none focus-visible:outline-none disabled:bg-transparent aria-invalid:outline-none dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none outline-none focus-visible:outline-none disabled:bg-transparent aria-invalid:outline-none dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}
Cập nhật các đường dẫn import cho phù hợp với thiết lập dự án của bạn.
Cách dùng#
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group"<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>Kết hợp#
Dùng cách kết hợp sau để xây dựng một InputGroup:
InputGroup
├── InputGroupInput or InputGroupTextarea
├── InputGroupAddon
├── InputGroupButton
└── InputGroupTextCăn chỉnh#
Dùng prop align trên InputGroupAddon để định vị addon so với ô nhập.
Để quản lý focus đúng cách, InputGroupAddon phải luôn được đặt sau
InputGroupInput hoặc InputGroupTextarea trong DOM. Dùng prop align để
định vị addon một cách trực quan.
inline-start#
Dùng align="inline-start" để định vị addon ở đầu ô nhập. Đây là giá trị mặc định.
import { SearchIcon } from "lucide-react"
import {inline-end#
Dùng align="inline-end" để định vị addon ở cuối ô nhập.
import { EyeOffIcon } from "lucide-react"
import {block-start#
Dùng align="block-start" để định vị addon phía trên ô nhập.
import { CopyIcon, FileCodeIcon } from "lucide-react"
import {block-end#
Dùng align="block-end" để định vị addon phía dưới ô nhập.
import {
Field,
FieldDescription,Ví dụ#
Biểu tượng#
import {
CheckIcon,
CreditCardIcon,Văn bản#
import {
InputGroup,
InputGroupAddon,Nút#
"use client"
import * as React from "react"Kbd#
import { SearchIcon } from "lucide-react"
import {Menu thả xuống#
"use client"
import { ChevronDownIcon, MoreHorizontal } from "lucide-react"Spinner#
import { LoaderIcon } from "lucide-react"
import {Textarea#
import {
IconBrandJavascript,
IconCopy,Ô nhập tùy chỉnh#
Thêm thuộc tính data-slot="input-group-control" vào ô nhập tùy chỉnh của bạn để tự động xử lý trạng thái focus.
Đây là một ví dụ về một textarea tùy chỉnh có thể thay đổi kích thước từ một thư viện của bên thứ ba.
"use client"
import TextareaAutosize from "react-textarea-autosize"RTL#
Để bật hỗ trợ RTL trong Neobrutalism, hãy xem hướng dẫn cấu hình RTL.
"use client"
import * as React from "react"Khả năng truy cập#
Không có mẫu widget ARIA nào dành cho input group — phần bọc ngoài render role="group" để công nghệ hỗ trợ liên kết các phần phụ trợ với điều khiển, còn việc truy cập bằng bàn phím là hành vi native: Tab lần lượt đi qua input và mọi InputGroupButton theo thứ tự DOM, đó cũng là lý do các phần phụ trợ nên nằm sau input (rồi định vị trực quan bằng align). Hai việc còn lại thuộc về bạn: chữ phụ trợ như https:// chỉ mang tính trực quan, không phải nhãn mà máy hiểu được, nên input vẫn cần một <Label> hoặc aria-label; và các nút chỉ có biểu tượng cần aria-label — bấm vào phần phụ trợ thì input được focus, nhưng đó là tiện lợi cho con trỏ, không phải một đường đi bằng bàn phím.
Tham chiếu API#
InputGroup#
Thành phần chính bao bọc các ô nhập và addon.
| Prop | Type | Default |
|---|---|---|
className | string |
<InputGroup>
<InputGroupInput />
<InputGroupAddon />
</InputGroup>InputGroupAddon#
Hiển thị biểu tượng, văn bản, nút hoặc nội dung khác bên cạnh các ô nhập.
Để điều hướng focus đúng cách, thành phần InputGroupAddon phải được đặt sau
ô nhập. Đặt prop align để định vị addon.
| Prop | Type | Default |
|---|---|---|
align | "inline-start" | "inline-end" | "block-start" | "block-end" | "inline-start" |
className | string |
<InputGroupAddon align="inline-end">
<SearchIcon />
</InputGroupAddon>Đối với <InputGroupInput />, dùng căn chỉnh inline-start hoặc inline-end. Đối với <InputGroupTextarea />, dùng căn chỉnh block-start hoặc block-end.
Thành phần InputGroupAddon có thể có nhiều thành phần InputGroupButton và biểu tượng.
<InputGroupAddon>
<InputGroupButton>Button</InputGroupButton>
<InputGroupButton>Button</InputGroupButton>
</InputGroupAddon>InputGroupButton#
Hiển thị các nút bên trong các nhóm ô nhập.
| Prop | Type | Default |
|---|---|---|
size | "xs" | "icon-xs" | "sm" | "icon-sm" | "xs" |
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "ghost" |
className | string |
<InputGroupButton>Button</InputGroupButton>
<InputGroupButton size="icon-xs" aria-label="Copy">
<CopyIcon />
</InputGroupButton>InputGroupInput#
Thay thế cho <Input /> khi xây dựng các nhóm ô nhập. Thành phần này đã được áp dụng sẵn các kiểu của nhóm ô nhập và dùng data-slot="input-group-control" hợp nhất để xử lý trạng thái focus.
| Prop | Type | Default |
|---|---|---|
className | string |
Tất cả các prop khác được truyền xuống thành phần <Input /> bên dưới.
<InputGroup>
<InputGroupInput placeholder="Enter text..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>InputGroupTextarea#
Thay thế cho <Textarea /> khi xây dựng các nhóm ô nhập. Thành phần này đã được áp dụng sẵn các kiểu của nhóm textarea và dùng data-slot="input-group-control" hợp nhất để xử lý trạng thái focus.
| Prop | Type | Default |
|---|---|---|
className | string |
Tất cả các prop khác được truyền xuống thành phần <Textarea /> bên dưới.
<InputGroup>
<InputGroupTextarea placeholder="Enter message..." />
<InputGroupAddon align="block-end">
<InputGroupButton>Send</InputGroupButton>
</InputGroupAddon>
</InputGroup>