Inputs

Group Invalid

The error belongs to the whole group, not to one half: a login without a domain and a domain without a login are the same wrong address.

  • inputgroup
  • error
  • validation
  • a11y

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/inputgroup-009?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 "inputgroup-009" (Group Invalid) 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/inputgroup-009.json

Registry item: https://vibeui.ru/r/inputgroup-009.json
Installs to: components/vibeui/inputgroup-009.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
The error belongs to the whole group, not to one half: a login without a domain and a domain without a login are the same wrong address.

A group whose error covers everything: red borders on every half, the ring on the group, one message for two fields. A server component, zero dependencies, one file.

## 3. How to use it
import { Inputgroup009 } from "@/components/vibeui/inputgroup-009"

<Inputgroup009
  name="corporate"
  label="Corporate email"
  error="No such address in your organisation."
/>

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-inputgroup-009-* palette — do not swap it for your theme tokens
- the error on the whole group: reddening one half misstates the cause
- the error ring on the group rather than on the halves — otherwise the neighbour's border clips it
- aria-invalid on both controls: it is the whole value that is wrong
- one message and a shared aria-describedby: otherwise the error is read twice
- role="alert" on the message: an error that appears must be announced immediately

## 6. You may change
- the label copy and the error text
- the domains array — your own mail domains
- the name prop — both form field names are built from it
- the accent through the accent prop

## 7. Rules
- The component validates nothing: pass error once the server answers.
- An empty error string returns the normal state with a hint — there is no separate flag.
- Do not show the error before the first submit: a red group on an untouched form is off-putting.
- 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/inputgroup-009.json
https://vibeui.ru/r/inputgroup-009.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-inputgroup-009-*. Серверный: состояние ошибки приходит пропом error. Красная граница ставится на всех детей группы, а кольцо — на саму группу, поэтому его не режет граница соседа. aria-invalid стоит на обоих контролах, aria-describedby у обоих указывает на одно сообщение с role="alert". Между половинами неинтерактивная @ с aria-hidden.

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 Inputgroup009Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  name?: string
  label?: string
  error?: string
  domains?: string[]
  accent?: string
}

// Идея компонента: ошибка принадлежит сцепке целиком, а не одной её половине.
// Логин без домена и домен без логина — одна и та же неправильная почта, и
// подчёркивать красным только левую часть значит врать. Поэтому красная
// рамка ставится на группу, aria-invalid — на оба поля, а aria-describedby
// у обоих указывает на одно сообщение: озвучено оно будет один раз, с какой
// бы половины ни начали.
const STYLES = `
:where([data-vibeui-block="inputgroup-009"]){
--vibeui-inputgroup-009-surface:oklch(1 0 0);
--vibeui-inputgroup-009-shell:oklch(0.91 0.006 265);
--vibeui-inputgroup-009-fg:oklch(0.23 0.014 265);
--vibeui-inputgroup-009-muted:oklch(0.55 0.014 265);
--vibeui-inputgroup-009-field:oklch(0.99 0.002 265);
--vibeui-inputgroup-009-fixed:oklch(0.96 0.004 265);
--vibeui-inputgroup-009-border:oklch(0.86 0.008 265);
--vibeui-inputgroup-009-accent:oklch(0.52 0.17 265);
--vibeui-inputgroup-009-bad:oklch(0.55 0.2 25);
--vibeui-inputgroup-009-radius:0.75rem;
--vibeui-inputgroup-009-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="inputgroup-009"]{
display:flex;flex-direction:column;gap:0.4375rem;
width:100%;max-width:23rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-inputgroup-009-surface);
border:1px solid var(--vibeui-inputgroup-009-shell);border-radius:0.875rem;
font-family:var(--vibeui-inputgroup-009-font);color:var(--vibeui-inputgroup-009-fg);
}
[data-vibeui-block="inputgroup-009"] *{box-sizing:border-box}
[data-vibeui-block="inputgroup-009"] label{font-size:0.8125rem;font-weight:600}
[data-vibeui-block="inputgroup-009"] [data-part="group"]{
display:flex;align-items:stretch;border-radius:var(--vibeui-inputgroup-009-radius);
}
[data-vibeui-block="inputgroup-009"] [data-part="group"] > *{
position:relative;height:2.75rem;
border:1px solid var(--vibeui-inputgroup-009-border);
border-radius:0;margin-left:-1px;font:inherit;font-size:0.875rem;color:inherit;
}
[data-vibeui-block="inputgroup-009"] [data-part="group"] > *:first-child{
margin-left:0;
border-radius:var(--vibeui-inputgroup-009-radius) 0 0 var(--vibeui-inputgroup-009-radius);
}
[data-vibeui-block="inputgroup-009"] [data-part="group"] > *:last-child{
border-radius:0 var(--vibeui-inputgroup-009-radius) var(--vibeui-inputgroup-009-radius) 0;
}
[data-vibeui-block="inputgroup-009"] [data-part="group"] > *:focus{
z-index:2;outline:2px solid var(--vibeui-inputgroup-009-accent);outline-offset:-1px;
border-color:var(--vibeui-inputgroup-009-accent);
}
/* Ошибка красит всю сцепку, а не половину: значение неверно целиком.
   Кольцо рисуется на группе, поэтому его не режет граница соседа. */
[data-vibeui-block="inputgroup-009"] [data-part="group"][data-bad="1"]{
box-shadow:0 0 0 3px color-mix(in oklab,var(--vibeui-inputgroup-009-bad) 16%,transparent);
}
[data-vibeui-block="inputgroup-009"] [data-part="group"][data-bad="1"] > *{
border-color:var(--vibeui-inputgroup-009-bad);
}
[data-vibeui-block="inputgroup-009"] [data-part="group"][data-bad="1"] > *:focus{
outline-color:var(--vibeui-inputgroup-009-bad);
}
[data-vibeui-block="inputgroup-009"] input{
flex:1;min-width:0;padding:0 0.75rem;background:var(--vibeui-inputgroup-009-field);
}
[data-vibeui-block="inputgroup-009"] [data-part="at"]{
flex:none;display:grid;place-items:center;width:2rem;
background:var(--vibeui-inputgroup-009-fixed);
color:var(--vibeui-inputgroup-009-muted);font-weight:600;
}
[data-vibeui-block="inputgroup-009"] select{
appearance:none;flex:none;width:10ch;padding:0 1.375rem 0 0.625rem;cursor:pointer;
background:var(--vibeui-inputgroup-009-fixed);
background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);
background-position:calc(100% - 0.9rem) 50%,calc(100% - 0.6rem) 50%;
background-size:0.3rem 0.3rem,0.3rem 0.3rem;
background-repeat:no-repeat;
}
[data-vibeui-block="inputgroup-009"] [data-part="error"]{
display:flex;align-items:flex-start;gap:0.375rem;margin:0;
font-size:0.75rem;line-height:1.4;color:var(--vibeui-inputgroup-009-bad);
}
[data-vibeui-block="inputgroup-009"] [data-part="error"] svg{
flex:none;width:0.875rem;height:0.875rem;margin-top:0.0625rem;display:block;
}
[data-vibeui-block="inputgroup-009"] [data-part="hint"]{
margin:0;font-size:0.75rem;line-height:1.4;color:var(--vibeui-inputgroup-009-muted);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="inputgroup-009"] *{animation:none!important;transition:none!important}}
`

/**
 * Сцепка с ошибкой на всей группе: красная рамка и одно сообщение на два поля.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Inputgroup009({
  name = "corporate",
  label = "Корпоративная почта",
  error = "Такой почты нет в вашей организации — проверьте имя и домен.",
  domains = ["vibeui.ru", "vibeui.com", "team.local"],
  accent,
  className,
  style,
  ...props
}: Inputgroup009Props) {
  const palette = {
    ...(accent ? { "--vibeui-inputgroup-009-accent": accent } : null),
    ...style,
  } as CSSProperties

  const bad = Boolean(error)
  const described = bad ? `${name}-error` : `${name}-hint`

  return (
    <>
      <style href="vibeui-inputgroup-009" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="inputgroup-009"
        className={className}
        style={palette}
      >
        <label htmlFor={`${name}-user`}>{label}</label>
        <div data-part="group" data-bad={bad ? "1" : "0"}>
          <input
            id={`${name}-user`}
            name={`${name}-user`}
            type="text"
            autoComplete="username"
            spellCheck={false}
            placeholder="anna.orlova"
            defaultValue="anna.orlova"
            aria-invalid={bad}
            aria-describedby={described}
          />
          <span data-part="at" aria-hidden="true">
            @
          </span>
          <select
            name={`${name}-domain`}
            aria-label="Домен"
            defaultValue={domains[0]}
            aria-invalid={bad}
            aria-describedby={described}
          >
            {domains.map((domain) => (
              <option key={domain} value={domain}>
                {domain}
              </option>
            ))}
          </select>
        </div>
        {bad ? (
          <p data-part="error" id={`${name}-error`} role="alert">
            <svg
              viewBox="0 0 16 16"
              fill="none"
              stroke="currentColor"
              strokeWidth="1.6"
              aria-hidden="true"
            >
              <circle cx="8" cy="8" r="6.25" />
              <path d="M8 4.75v4" strokeLinecap="round" />
              <circle
                cx="8"
                cy="11"
                r="0.75"
                fill="currentColor"
                stroke="none"
              />
            </svg>
            {error}
          </p>
        ) : (
          <p data-part="hint" id={`${name}-hint`}>
            Имя и домен уходят двумя полями формы.
          </p>
        )}
      </div>
    </>
  )
}