Inputs

Unit Affix

A field with a text prefix on the left and a unit picker on the right inside one border: value and unit are entered together and submitted as two fields.

  • field
  • unit
  • prefix
  • 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/field-005?lang=en

Значение и единица уходят двумя полями формы — пересчитывать ничего не нужно.

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-005" (Unit Affix) 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-005.json

Registry item: https://vibeui.ru/r/field-005.json
Installs to: components/vibeui/field-005.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 with a text prefix on the left and a unit picker on the right inside one border: value and unit are entered together and submitted as two fields.

A numeric field sharing one border with a text prefix on the left and a native unit select on the right. The prefix is part of the sentence and stays uneditable; the unit is chosen in place and submitted as its own form field. A server component, zero dependencies, one file, its own palette.

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

<Field005
  name="limit"
  label="Per-file limit"
  prefix="at most"
  units={["KB", "MB", "GB"]}
/>

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-005-* palette — do not swap it for your theme tokens (bg-muted, border-input and the like)
- the border on the group: neither the input nor the select has one, otherwise two more appear inside it
- the native select for the unit — keyboard support, type-ahead and the system wheel on phones all come with it
- the :focus-within highlight and the ring: focus can sit on the number or on the unit, and the whole group has to show it
- user-select:none and aria-hidden on the prefix: it is not selected and not read apart from the label
- overflow:hidden on the group — it clips the prefix and unit backgrounds to the border radius
- the <style> block inside the component — it holds the palette, the geometry and the select arrow

## 6. You may change
- the label copy and the prefix text
- the units array — your own units of measurement
- the starting number in defaultValue
- the accent through the accent prop — it colours the border and ring on focus
- the name prop: both form field names are built from it

## 7. Rules
- The unit is submitted as a separate `${name}-unit` field. Converting the value on the client is unnecessary — that is the server's job.
- Do not put a button in prefix: it is a non-interactive part of the border, and a target inside a field frame confuses.
- Keep the units list short: long captions stretch the select and eat the room the number needs.
- 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-005.json
https://vibeui.ru/r/field-005.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-field-005-*. Серверный: подсветка группы держится на :focus-within. Рамка живёт на группе, у input и select её нет. Приставка — неинтерактивный span с user-select:none, окончание — нативный select с appearance:none и стрелкой из двух граней. Значение и единица уходят двумя полями формы: name и `${name}-unit`.

Component source

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

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Field005Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "defaultValue"
> & {
  label?: string
  prefix?: string
  units?: string[]
  defaultValue?: number
  name?: string
  accent?: string
}

// Идея компонента: окончание, которое можно выбрать. Приставка слева —
// обычный текст и часть формулировки («не более»), а справа стоит настоящий
// select единицы: значение и единица вводятся в одном месте и уезжают на
// сервер двумя полями. Так не приходится угадывать, мегабайты перед вами или
// гигабайты, и не нужен отдельный ряд под выбор единицы.
const STYLES = `
:where([data-vibeui-block="field-005"]){
--vibeui-field-005-bg:oklch(1 0 0);
--vibeui-field-005-surface:oklch(1 0 0);
--vibeui-field-005-fill:oklch(0.975 0.004 265);
--vibeui-field-005-fg:oklch(0.24 0.014 265);
--vibeui-field-005-muted:oklch(0.55 0.014 265);
--vibeui-field-005-border:oklch(0.88 0.008 265);
--vibeui-field-005-shell:oklch(0.91 0.006 265);
--vibeui-field-005-accent:oklch(0.52 0.14 175);
--vibeui-field-005-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: поле показывают поверх любого фона. */
[data-vibeui-block="field-005"]{
display:flex;flex-direction:column;gap:0.4375rem;
width:100%;max-width:21rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-field-005-surface);
border:1px solid var(--vibeui-field-005-shell);border-radius:0.875rem;
font-family:var(--vibeui-field-005-font);color:var(--vibeui-field-005-fg);
}
[data-vibeui-block="field-005"] *{box-sizing:border-box}
[data-vibeui-block="field-005"] label{font-size:0.8125rem;font-weight:600}
/* Рамка одна на всю группу, у частей её нет — иначе рамка в рамке. */
[data-vibeui-block="field-005"] [data-part="group"]{
display:flex;align-items:stretch;height:2.625rem;overflow:hidden;
background:var(--vibeui-field-005-bg);
border:1px solid var(--vibeui-field-005-border);border-radius:0.75rem;
transition:border-color .16s ease,box-shadow .16s ease;
}
[data-vibeui-block="field-005"] [data-part="group"]:focus-within{
border-color:var(--vibeui-field-005-accent);
box-shadow:0 0 0 3px color-mix(in oklab,var(--vibeui-field-005-accent) 18%,transparent);
}
/* Приставка неинтерактивна: её не выделяют и не копируют со значением. */
[data-vibeui-block="field-005"] [data-part="prefix"]{
display:flex;align-items:center;padding:0 0.625rem;
background:var(--vibeui-field-005-fill);
border-inline-end:1px solid var(--vibeui-field-005-border);
font-size:0.8125rem;color:var(--vibeui-field-005-muted);
user-select:none;white-space:nowrap;
}
[data-vibeui-block="field-005"] input{
flex:1;min-width:0;padding:0 0.625rem;
border:0;background:none;color:inherit;
font:inherit;font-size:0.9375rem;font-weight:600;
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="field-005"] input:focus{outline:none}
[data-vibeui-block="field-005"] input::-webkit-outer-spin-button,
[data-vibeui-block="field-005"] input::-webkit-inner-spin-button{appearance:none;margin:0}
/* Единица — настоящий select: клавиатура и мобильное колесо достаются даром. */
[data-vibeui-block="field-005"] select{
appearance:none;flex:none;padding:0 1.75rem 0 0.75rem;
border:0;border-inline-start:1px solid var(--vibeui-field-005-border);
background:var(--vibeui-field-005-fill);color:inherit;cursor:pointer;
font:inherit;font-size:0.8125rem;font-weight:650;
}
[data-vibeui-block="field-005"] select:focus{outline:none}
[data-vibeui-block="field-005"] [data-part="unit"]{position:relative;display:flex}
[data-vibeui-block="field-005"] [data-part="unit"]::after{
content:"";position:absolute;right:0.6875rem;top:50%;pointer-events:none;
width:0.375rem;height:0.375rem;margin-top:-0.25rem;
border-right:1.5px solid var(--vibeui-field-005-muted);
border-bottom:1.5px solid var(--vibeui-field-005-muted);
transform:rotate(45deg);
}
[data-vibeui-block="field-005"] [data-part="unit"]:focus-within::after{border-color:var(--vibeui-field-005-accent)}
[data-vibeui-block="field-005"] [data-part="hint"]{
margin:0;font-size:0.75rem;line-height:1.4;color:var(--vibeui-field-005-muted);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="field-005"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_UNITS = ["КБ", "МБ", "ГБ"]

/**
 * Поле с текстовой приставкой слева и выбором единицы справа в одной рамке.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Field005({
  label = "Лимит на один файл",
  prefix = "не более",
  units = DEFAULT_UNITS,
  defaultValue = 25,
  name = "limit",
  accent,
  className,
  style,
  ...props
}: Field005Props) {
  const palette = {
    ...(accent ? { "--vibeui-field-005-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-field-005" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="field-005"
        className={className}
        style={palette}
      >
        <label htmlFor={`${name}-value`}>{label}</label>
        <div data-part="group">
          <span data-part="prefix" aria-hidden="true">
            {prefix}
          </span>
          <input
            id={`${name}-value`}
            name={name}
            type="number"
            min={1}
            inputMode="numeric"
            defaultValue={defaultValue}
            aria-describedby={`${name}-hint`}
          />
          <span data-part="unit">
            <select
              name={`${name}-unit`}
              defaultValue={units[1] ?? units[0]}
              aria-label="Единица измерения"
            >
              {units.map((unit) => (
                <option key={unit} value={unit}>
                  {unit}
                </option>
              ))}
            </select>
          </span>
        </div>
        <p id={`${name}-hint`} data-part="hint">
          Значение и единица уходят двумя полями формы — пересчитывать ничего не
          нужно.
        </p>
      </div>
    </>
  )
}