Bỏ qua đến nội dung

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, TextareaButton 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 ý ⌘K hoặ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-end và một hành động gửi.

Cài đặt

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/input-group.json
npx shadcn@latest add https://neobrutalism.com/r/base/input-group.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/input-group.json
bunx --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.

components/ui/input-group.tsx
"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
└── InputGroupText

Căn chỉnh

Dùng prop align trên InputGroupAddon để định vị addon so với ô nhập.

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 {
"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.

PropTypeDefault
classNamestring
<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.

PropTypeDefault
align"inline-start" | "inline-end" | "block-start" | "block-end""inline-start"
classNamestring
<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.

PropTypeDefault
size"xs" | "icon-xs" | "sm" | "icon-sm""xs"
variant"default" | "destructive" | "outline" | "secondary" | "ghost" | "link""ghost"
classNamestring
<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.

PropTypeDefault
classNamestring

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.

PropTypeDefault
classNamestring

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>