Inputs

Tag Input

A tag input: Enter and comma add, Backspace on an empty field removes the last one — no aiming at the little cross.

  • tags
  • chips
  • input
  • form

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/tags-001?lang=en

формабез зависимостей

Enter или запятая добавляют тег2 / 8

Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "tags-001" (Tag Input) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/tags-001.json

Registry item: https://vibeui.ru/r/tags-001.json
Installs to: components/vibeui/tags-001.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
A tag input: Enter and comma add, Backspace on an empty field removes the last one — no aiming at the little cross.

A tag input: chips with a remove cross, Enter and comma to add, a counter and a limit. Zero dependencies, one file.

## 3. How to use it
import { Tags001 } from "@/components/vibeui/tags-001"

<Tags001 label="Tags" defaultValue={["form"]} max={8} />

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-tags-001-* palette — do not swap it for your theme tokens
- its own light surface: the field gets shown over any background
- adding on both Enter and comma: both habits are equally common
- Backspace on an empty field — otherwise you have to aim at the cross
- silently dropping duplicates: a repeated tag is noise, not a user error
- the aria-label on the cross naming the tag
- the counter at the limit: otherwise the field just stops accepting input

## 6. You may change
- label, hint and the initial tags
- max — the count limit
- the accent through the accent prop
- the field width

## 7. Rules
- There are no suggestions: to pick from a known list use autocomplete-001.
- Case is not normalised: "Form" and "form" stay distinct — fold them yourself if that is wrong.
- The value is not exposed: add your own handler or a hidden field for the form.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/tags-001.json
https://vibeui.ru/r/tags-001.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-tags-001-*. Клиентский: "use client" ради ввода. Теги и черновик хранятся раздельно, повторы отсекаются молча. Поле ввода растягивается по остатку строки и переносится вместе с тегами, а счётчик показывает предел.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

"use client"

import { useId, useState } from "react"
import type {
  ComponentPropsWithoutRef,
  CSSProperties,
  KeyboardEvent,
} from "react"

export type Tags001Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "defaultValue"
> & {
  label?: string
  hint?: string
  defaultValue?: string[]
  max?: number
  accent?: string
}

// Идея компонента: поле тегов. Тег добавляется по Enter и по запятой — обе
// привычки встречаются одинаково часто. Backspace на пустом поле удаляет
// последний тег: без этого приходится целиться в крестик. Повторы отсекаются
// молча, потому что второй такой же тег не ошибка пользователя, а шум.
const STYLES = `
:where([data-vibeui-block="tags-001"]){
--vibeui-tags-001-bg:oklch(1 0 0);
--vibeui-tags-001-surface:oklch(1 0 0);
--vibeui-tags-001-shell:oklch(0.9 0.006 265);
--vibeui-tags-001-fg:oklch(0.24 0.014 265);
--vibeui-tags-001-muted:oklch(0.56 0.014 265);
--vibeui-tags-001-border:oklch(0.88 0.008 265);
--vibeui-tags-001-chip:oklch(0.96 0.004 265);
--vibeui-tags-001-accent:oklch(0.55 0.2 262);
--vibeui-tags-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: поле показывают поверх любого фона. */
[data-vibeui-block="tags-001"]{
display:flex;flex-direction:column;gap:0.375rem;
padding:0.875rem;
background:var(--vibeui-tags-001-surface);
border:1px solid var(--vibeui-tags-001-shell);border-radius:0.875rem;
width:100%;max-width:22rem;box-sizing:border-box;
font-family:var(--vibeui-tags-001-font);color:var(--vibeui-tags-001-fg);
}
[data-vibeui-block="tags-001"] label{font-size:0.8125rem;font-weight:600}
[data-vibeui-block="tags-001"] [data-part="field"]{
display:flex;flex-wrap:wrap;align-items:center;gap:0.375rem;
min-height:2.5rem;padding:0.3125rem 0.5rem;box-sizing:border-box;
background:var(--vibeui-tags-001-bg);
border:1px solid var(--vibeui-tags-001-border);border-radius:0.625rem;
}
[data-vibeui-block="tags-001"] [data-part="field"]:focus-within{
border-color:var(--vibeui-tags-001-accent);
box-shadow:0 0 0 2px oklch(0.55 0.2 262 / 22%);
}
[data-vibeui-block="tags-001"] [data-part="chip"]{
display:inline-flex;align-items:center;gap:0.25rem;
height:1.625rem;padding:0 0.25rem 0 0.5rem;border-radius:0.4375rem;
background:var(--vibeui-tags-001-chip);font-size:0.8125rem;
}
[data-vibeui-block="tags-001"] [data-part="chip"] button{
appearance:none;border:0;background:none;cursor:pointer;
width:1.125rem;height:1.125rem;border-radius:0.3125rem;
color:var(--vibeui-tags-001-muted);font:inherit;font-size:0.875rem;line-height:1;
}
[data-vibeui-block="tags-001"] [data-part="chip"] button:hover{color:var(--vibeui-tags-001-fg)}
[data-vibeui-block="tags-001"] [data-part="chip"] button:focus-visible{outline:2px solid var(--vibeui-tags-001-accent);outline-offset:1px}
/* Поле тянется по остатку строки, но не выталкивает теги на свою строку. */
[data-vibeui-block="tags-001"] input{
flex:1 1 6rem;min-width:6rem;
appearance:none;border:0;background:none;outline:none;
height:1.625rem;color:inherit;font:inherit;font-size:0.875rem;
}
[data-vibeui-block="tags-001"] [data-part="hint"]{
display:flex;justify-content:space-between;gap:0.75rem;
margin:0;font-size:0.75rem;color:var(--vibeui-tags-001-muted);
}
[data-vibeui-block="tags-001"] [data-part="counter"]{font-variant-numeric:tabular-nums}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="tags-001"] *{animation:none!important;transition:none!important}}
`

/**
 * Поле тегов: Enter и запятая добавляют, Backspace на пустом поле удаляет.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Tags001({
  label = "Теги компонента",
  hint = "Enter или запятая добавляют тег",
  defaultValue = ["форма", "без зависимостей"],
  max = 8,
  accent,
  className,
  style,
  ...props
}: Tags001Props) {
  const id = useId()
  const [tags, setTags] = useState(defaultValue)
  const [draft, setDraft] = useState("")

  const add = (value: string) => {
    const tag = value.trim().replace(/,+$/, "")
    if (!tag || tags.length >= max || tags.includes(tag)) return setDraft("")
    setTags([...tags, tag])
    setDraft("")
  }

  const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    if (event.key === "Enter" || event.key === ",") {
      event.preventDefault()
      add(draft)
      return
    }
    if (event.key === "Backspace" && draft === "" && tags.length > 0) {
      setTags(tags.slice(0, -1))
    }
  }

  const palette = {
    ...(accent ? { "--vibeui-tags-001-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-tags-001" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="tags-001"
        className={className}
        style={palette}
      >
        <label htmlFor={id}>{label}</label>
        <div data-part="field">
          {tags.map((tag) => (
            <span key={tag} data-part="chip">
              {tag}
              <button
                type="button"
                aria-label={`Убрать тег ${tag}`}
                onClick={() => setTags(tags.filter((item) => item !== tag))}
              >
                ×
              </button>
            </span>
          ))}
          <input
            id={id}
            type="text"
            value={draft}
            placeholder={tags.length >= max ? "Достигнут предел" : "Добавить…"}
            disabled={tags.length >= max}
            aria-describedby={`${id}-hint`}
            onChange={(event) => setDraft(event.target.value)}
            onKeyDown={onKeyDown}
            onBlur={() => add(draft)}
          />
        </div>
        <p id={`${id}-hint`} data-part="hint">
          {hint}
          <span data-part="counter">
            {tags.length} / {max}
          </span>
        </p>
      </div>
    </>
  )
}