Naar inhoud

Invoergroep

Voeg pictogrammen, knoppen en toetsenbordhints toe aan invoervelden voor zoekbalken, URL-velden en chatvensters — met neobrutalistische randen en schaduwen.

import { Search } from "lucide-react"

import {

De input group verpakt een input of textarea samen met zijn add-ons — iconen, inline tekst, knoppen, sneltoetshints — in één omrand besturingselement. Het is een eigen compositie van Input, Textarea en Button uit deze bibliotheek, vormgegeven volgens het neobrutalistische recept: dikke randen, scherpe schaduwen en vette typografie.

Ideaal voor:

  • Zoekvelden — een zoekicoon vooraan plus een ⌘K-hint of wisknop achteraan.
  • Inputs met prefix of suffix — vaste tekst als https://, @ of .com, zodat gebruikers alleen het variabele deel typen.
  • Chat- en promptcomposers — een textarea met een block-end-werkbalk met knoppen en een verzendactie.

Installatie

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

Kopieer en plak de volgende code in je project.

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,
}

Werk de importpaden bij zodat ze overeenkomen met je projectconfiguratie.

Gebruik

import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
  InputGroupText,
  InputGroupTextarea,
} from "@/components/ui/input-group"
<InputGroup>
  <InputGroupInput placeholder="Search..." />
  <InputGroupAddon>
    <SearchIcon />
  </InputGroupAddon>
</InputGroup>

Compositie

Gebruik de volgende compositie om een InputGroup te bouwen:

InputGroup
├── InputGroupInput or InputGroupTextarea
├── InputGroupAddon
├── InputGroupButton
└── InputGroupText

Uitlijning

Gebruik de align-prop op InputGroupAddon om de add-on te positioneren ten opzichte van het invoerveld.

inline-start

Gebruik align="inline-start" om de add-on aan het begin van het invoerveld te positioneren. Dit is de standaard.

import { SearchIcon } from "lucide-react"

import {

inline-end

Gebruik align="inline-end" om de add-on aan het einde van het invoerveld te positioneren.

import { EyeOffIcon } from "lucide-react"

import {

block-start

Gebruik align="block-start" om de add-on boven het invoerveld te positioneren.

import { CopyIcon, FileCodeIcon } from "lucide-react"

import {

block-end

Gebruik align="block-end" om de add-on onder het invoerveld te positioneren.

import {
  Field,
  FieldDescription,

Voorbeelden

Pictogram

import {
  CheckIcon,
  CreditCardIcon,

Tekst

import {
  InputGroup,
  InputGroupAddon,

Knop

"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,

Aangepast invoerveld

Voeg het data-slot="input-group-control"-attribuut toe aan je aangepaste invoerveld voor automatische afhandeling van de focusstaat.

Hier is een voorbeeld van een aangepaste, in grootte aanpasbare textarea uit een externe bibliotheek.

"use client"

import TextareaAutosize from "react-textarea-autosize"

RTL

Om RTL-ondersteuning in Neobrutalism in te schakelen, zie de RTL-configuratiegids.

"use client"

import * as React from "react"

Toegankelijkheid

Er bestaat geen ARIA-widgetpatroon voor input groups — de wrapper rendert role="group" zodat ondersteunende technologie de add-ons aan het besturingselement koppelt, en de toetsenbordtoegang is native: Tab bereikt de input en elke InputGroupButton in DOM-volgorde — precies waarom add-ons ná de input horen (visueel gepositioneerd met align). Twee dingen blijven aan jou: add-on-tekst zoals https:// is visueel, geen programmatisch label, dus de input heeft nog steeds een <Label> of aria-label nodig; en knoppen met alleen een icoon hebben een aria-label nodig — klikken op een add-on geeft de input focus, maar dat is een gemak voor de aanwijzer, geen toetsenbordroute.

API-referentie

InputGroup

Het hoofdcomponent dat invoervelden en add-ons omhult.

PropTypeDefault
classNamestring
<InputGroup>
  <InputGroupInput />
  <InputGroupAddon />
</InputGroup>

InputGroupAddon

Toont pictogrammen, tekst, knoppen of andere inhoud naast invoervelden.

PropTypeDefault
align"inline-start" | "inline-end" | "block-start" | "block-end""inline-start"
classNamestring
<InputGroupAddon align="inline-end">
  <SearchIcon />
</InputGroupAddon>

Gebruik voor <InputGroupInput /> de uitlijning inline-start of inline-end. Gebruik voor <InputGroupTextarea /> de uitlijning block-start of block-end.

Het InputGroupAddon-component kan meerdere InputGroupButton-componenten en pictogrammen bevatten.

<InputGroupAddon>
  <InputGroupButton>Button</InputGroupButton>
  <InputGroupButton>Button</InputGroupButton>
</InputGroupAddon>

InputGroupButton

Toont knoppen binnen invoergroepen.

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

Vervanging voor <Input /> bij het bouwen van invoergroepen. Dit component heeft de invoergroepstijlen vooraf toegepast en gebruikt de uniforme data-slot="input-group-control" voor de afhandeling van de focusstaat.

PropTypeDefault
classNamestring

Alle andere props worden doorgegeven aan het onderliggende <Input />-component.

<InputGroup>
  <InputGroupInput placeholder="Enter text..." />
  <InputGroupAddon>
    <SearchIcon />
  </InputGroupAddon>
</InputGroup>

InputGroupTextarea

Vervanging voor <Textarea /> bij het bouwen van invoergroepen. Dit component heeft de textarea-groepstijlen vooraf toegepast en gebruikt de uniforme data-slot="input-group-control" voor de afhandeling van de focusstaat.

PropTypeDefault
classNamestring

Alle andere props worden doorgegeven aan het onderliggende <Textarea />-component.

<InputGroup>
  <InputGroupTextarea placeholder="Enter message..." />
  <InputGroupAddon align="block-end">
    <InputGroupButton>Send</InputGroupButton>
  </InputGroupAddon>
</InputGroup>