Inputs

Field Shell

A field shell: the border wraps the label and the control together, while hint and error share one line below — the form never jumps when an error appears.

  • field
  • form
  • wrapper
  • error

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/field-002?lang=en

Only you and your teammates can see it.

draft
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 "field-002" (Field Shell) 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/field-002.json

Registry item: https://vibeui.ru/r/field-002.json
Installs to: components/vibeui/field-002.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 field shell: the border wraps the label and the control together, while hint and error share one line below — the form never jumps when an error appears.

A shell rather than a field: the border wraps the label and the control, one line below carries either the hint or the error, and a state badge fits on the right. Through children you drop in an input, a textarea or a select — the wrapper stays identical across the form. A server component, zero dependencies, one file, its own palette.

## 3. How to use it
import { Field002 } from "@/components/vibeui/field-002"

<Field002
  name="project"
  label="Project name"
  hint="Only you and your teammates can see it."
  badge="draft"
/>

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-field-002-* palette — do not swap it for your theme tokens (bg-background, border-input, text-muted-foreground and the like)
- the border on the shell rather than on the control: the input inside has none, otherwise a second border appears inside the first
- the shared slot for hint and error: because they swap instead of stacking, the form does not jump and the fields below stay put
- the :focus-within highlight together with the box-shadow ring — focus sits on the control, but the whole frame has to show it
- the error wiring: data-invalid on the root, aria-invalid on the control and role="alert" on the note — otherwise only sighted users get the message
- the small-caps label inside the frame: it is part of the field and must not read as a separate heading
- the <style> block inside the component — it holds the palette, the geometry and the focus behaviour

## 6. You may change
- the label and hint copy, the error message and the badge text
- the children content — a textarea, a select or your own control instead of the input
- the accent through the accent prop — it colours the border and ring on focus
- the name prop: the field id and the note id are built from it
- width and outer spacing through className

## 7. Rules
- Your own control in children needs the id `${name}-input`, otherwise clicking the label will not focus it.
- The error replaces the hint rather than joining it: if the rules still matter during the error, fold them into the error text.
- The badge is a state marker, not a button: do not put interactive content inside it.
- 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/field-002.json
https://vibeui.ru/r/field-002.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-field-002-*. Серверный: состояния нет, подсветка держится на :focus-within. Внутрь рамки через children кладут любой контрол — input, textarea или select; по умолчанию рендерится input с id из пропа name. Подсказка и ошибка занимают одно и то же место, ошибка включает data-invalid на корне, aria-invalid у поля и role="alert" у примечания.

Component source

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

import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react"

export type Field002Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "defaultValue"
> & {
  label?: string
  hint?: string
  error?: string
  badge?: string
  name?: string
  children?: ReactNode
  accent?: string
}

// Идея компонента: каркас, а не поле. Рамка обнимает подпись и контрол
// вместе, поэтому подпись читается как часть поля, а не как строка над ним.
// Внутрь кладут что угодно — input, textarea, select, — и обвязка остаётся
// одинаковой во всей форме. Служебная строка под рамкой одна: подсказка и
// ошибка занимают одно место, поэтому форма не прыгает при появлении ошибки.
const STYLES = `
:where([data-vibeui-block="field-002"]){
--vibeui-field-002-surface:oklch(1 0 0);
--vibeui-field-002-frame:oklch(0.985 0.003 265);
--vibeui-field-002-fg:oklch(0.24 0.014 265);
--vibeui-field-002-muted:oklch(0.55 0.014 265);
--vibeui-field-002-border:oklch(0.88 0.008 265);
--vibeui-field-002-shell:oklch(0.91 0.006 265);
--vibeui-field-002-accent:oklch(0.55 0.2 262);
--vibeui-field-002-danger:oklch(0.55 0.19 25);
--vibeui-field-002-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: каркас показывают поверх любого фона. */
[data-vibeui-block="field-002"]{
display:flex;flex-direction:column;gap:0.4375rem;
width:100%;max-width:21rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-field-002-surface);
border:1px solid var(--vibeui-field-002-shell);border-radius:0.875rem;
font-family:var(--vibeui-field-002-font);color:var(--vibeui-field-002-fg);
}
[data-vibeui-block="field-002"] *{box-sizing:border-box}
/* Рамка обнимает подпись и контрол: подпись — часть поля, а не строка над ним. */
[data-vibeui-block="field-002"] [data-part="frame"]{
display:flex;flex-direction:column;gap:0.125rem;
padding:0.4375rem 0.75rem 0.5rem;
background:var(--vibeui-field-002-frame);
border:1px solid var(--vibeui-field-002-border);border-radius:0.75rem;
transition:border-color .16s ease,box-shadow .16s ease;
}
[data-vibeui-block="field-002"] [data-part="frame"]:focus-within{
border-color:var(--vibeui-field-002-accent);
box-shadow:0 0 0 3px color-mix(in oklab,var(--vibeui-field-002-accent) 18%,transparent);
}
[data-vibeui-block="field-002"][data-invalid="true"] [data-part="frame"]{border-color:var(--vibeui-field-002-danger)}
[data-vibeui-block="field-002"][data-invalid="true"] [data-part="frame"]:focus-within{
box-shadow:0 0 0 3px color-mix(in oklab,var(--vibeui-field-002-danger) 18%,transparent);
}
[data-vibeui-block="field-002"] label{
font-size:0.6875rem;font-weight:650;letter-spacing:0.01em;
color:var(--vibeui-field-002-muted);
}
/* Контрол без собственной рамки: рамка одна на весь каркас. */
[data-vibeui-block="field-002"] :is(input,textarea,select){
width:100%;min-width:0;padding:0;margin:0;
border:0;background:none;color:inherit;font:inherit;font-size:0.875rem;line-height:1.6;
appearance:none;
}
[data-vibeui-block="field-002"] :is(input,textarea,select):focus{outline:none}
[data-vibeui-block="field-002"] textarea{resize:none;min-height:3rem}
[data-vibeui-block="field-002"] ::placeholder{color:var(--vibeui-field-002-muted)}
/* Одна служебная строка на подсказку и ошибку: форма не прыгает. */
[data-vibeui-block="field-002"] [data-part="foot"]{
display:flex;align-items:flex-start;justify-content:space-between;gap:0.75rem;
font-size:0.75rem;line-height:1.4;
}
[data-vibeui-block="field-002"] [data-part="note"]{margin:0;color:var(--vibeui-field-002-muted)}
[data-vibeui-block="field-002"] [data-part="error"]{margin:0;font-weight:600;color:var(--vibeui-field-002-danger)}
[data-vibeui-block="field-002"] [data-part="badge"]{
flex:none;padding:0.0625rem 0.375rem;border-radius:9999px;
background:color-mix(in oklab,var(--vibeui-field-002-accent) 12%,oklch(1 0 0));
color:var(--vibeui-field-002-accent);font-size:0.6875rem;font-weight:650;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="field-002"] *{animation:none!important;transition:none!important}}
`

/**
 * Каркас поля: рамка вокруг подписи и контрола, одна служебная строка снизу.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Field002({
  label = "Название проекта",
  hint = "Видно только вам и участникам команды.",
  error = "",
  badge = "черновик",
  name = "project",
  children,
  accent,
  className,
  style,
  ...props
}: Field002Props) {
  const noteId = `${name}-note`

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

  return (
    <>
      <style href="vibeui-field-002" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="field-002"
        data-invalid={error ? "true" : undefined}
        className={className}
        style={palette}
      >
        <div data-part="frame">
          <label htmlFor={`${name}-input`}>{label}</label>
          {children ?? (
            <input
              id={`${name}-input`}
              name={name}
              type="text"
              defaultValue="Каталог компонентов"
              placeholder="Как назовём"
              aria-invalid={error ? true : undefined}
              aria-describedby={hint || error ? noteId : undefined}
            />
          )}
        </div>
        <div data-part="foot">
          {error ? (
            <p id={noteId} data-part="error" role="alert">
              {error}
            </p>
          ) : (
            <p id={noteId} data-part="note">
              {hint}
            </p>
          )}
          {badge ? <span data-part="badge">{badge}</span> : null}
        </div>
      </div>
    </>
  )
}