Buttons

Validity Gated Submit

A submit button locked until the form is ready: a :has(:invalid) rule dims it without JS, and the hint below always keeps its space.

  • button
  • submit
  • form
  • validation

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/button-059?lang=en

Enter an address to send

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 "button-059" (Validity Gated Submit) 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/button-059.json

Registry item: https://vibeui.ru/r/button-059.json
Installs to: components/vibeui/button-059.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 submit button locked until the form is ready: a :has(:invalid) rule dims it without JS, and the hint below always keeps its space.

A one-field form whose button knows on its own whether submission is ready. While the field is empty or the address is wrong, a :has(:invalid) rule dims the button and reveals the hint beneath it; the moment the browser calls the field valid, the button lights up. Not a line of JS — the browser computes the state. Zero dependencies, one file.

## 3. How to use it
import { Button059 } from "@/components/vibeui/button-059"

<Button059 label="Work email" submitLabel="Send request" onSubmit={send} />

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 :has(:invalid) rule on the form: that is the mechanic, swapping it for JS state removes the point
- native validation through required and type: the browser knows more about address formats than a hand-rolled check
- visual dimming instead of the disabled attribute — a disabled button could not explain what is missing
- the hint keeping its space (visibility rather than display): otherwise the layout jumps on every keystroke
- the field and the button inside one <form>: without a form the :has(:invalid) selector has nothing to hang on
- the label tied to the input by id — without it clicking the label does not focus the field

## 6. You may change
- the field label through the label prop
- the button label through the submitLabel prop
- the hint text through the hint prop
- the accent colour through the accent prop
- the onSubmit handler and the rest of the native form props: action, method, noValidate

## 7. Rules
- The component does not guard against double submission: locking the button after a successful submit is your app's job.
- Do not put noValidate on the form — it switches off the whole :has(:invalid) mechanic along with validation.
- The field id is fixed: with several forms on a page, make it a prop.
- 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/button-059.json
https://vibeui.ru/r/button-059.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-button-059-*. Серверный компонент, JS не нужен: корень — <form>, а правило form:has(:invalid) гасит кнопку и показывает подсказку. Валидность считает браузер по required и type="email", а не наш код. Подсказка занимает место всегда (visibility, а не display), поэтому раскладка не прыгает при смене статуса. Кнопка остаётся кликабельной для скринридера — вместо disabled используется визуальное гашение, и попытка отправки покажет системное сообщение о поле.

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 Button059Props = Omit<
  ComponentPropsWithoutRef<"form">,
  "children"
> & {
  label?: string
  placeholder?: string
  /** Подсказка под кнопкой, пока форма не готова к отправке. */
  hint?: string
  submitLabel?: string
  accent?: string
}

// Идея компонента: кнопка отправки, которая сама знает, готова ли форма.
// Правило form:has(:invalid) гасит её без единой строки JS — валидность
// считает браузер по required и type, а не наш обработчик. Подсказка под
// кнопкой держит место всегда, поэтому раскладка не прыгает при смене статуса.
const STYLES = `
:where([data-vibeui-block="button-059"]){
--vibeui-button-059-surface:oklch(1 0 0);
--vibeui-button-059-border:oklch(0.88 0.006 265);
--vibeui-button-059-fg:oklch(0.24 0.02 265);
--vibeui-button-059-muted:oklch(0.57 0.014 265);
--vibeui-button-059-accent:oklch(0.5 0.16 150);
--vibeui-button-059-accent-fg:oklch(0.99 0.01 150);
--vibeui-button-059-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="button-059"]{
display:flex;flex-direction:column;gap:0.5rem;box-sizing:border-box;
width:100%;max-width:21rem;padding:0.875rem;
border:1px solid var(--vibeui-button-059-border);border-radius:0.875rem;
background:var(--vibeui-button-059-surface);color:var(--vibeui-button-059-fg);
font-family:var(--vibeui-button-059-font);
}
[data-vibeui-block="button-059"] label{font-size:0.75rem;font-weight:600;color:var(--vibeui-button-059-muted)}
[data-vibeui-block="button-059"] input{
appearance:none;box-sizing:border-box;width:100%;
height:2.5rem;padding:0 0.75rem;border-radius:0.5rem;
border:1px solid var(--vibeui-button-059-border);
background:var(--vibeui-button-059-surface);color:inherit;
font:inherit;font-size:0.875rem;
transition:border-color .16s ease;
}
[data-vibeui-block="button-059"] input:focus-visible{
outline:2px solid var(--vibeui-button-059-accent);outline-offset:1px;
border-color:var(--vibeui-button-059-accent);
}
[data-vibeui-block="button-059"] [data-part="submit"]{
appearance:none;border:0;cursor:pointer;box-sizing:border-box;
display:inline-flex;align-items:center;justify-content:center;gap:0.5rem;
width:100%;height:2.625rem;border-radius:0.625rem;
background:var(--vibeui-button-059-accent);color:var(--vibeui-button-059-accent-fg);
font:inherit;font-size:0.875rem;font-weight:650;line-height:1;
transition:background-color .16s ease,opacity .16s ease;
}
/* Кнопка гаснет, пока в форме есть невалидное поле. Без JS. */
[data-vibeui-block="button-059"]:has(:invalid) [data-part="submit"]{
cursor:not-allowed;opacity:.45;
background:var(--vibeui-button-059-muted);
}
[data-vibeui-block="button-059"] [data-part="submit"]:focus-visible{outline:2px solid var(--vibeui-button-059-accent);outline-offset:2px}
[data-vibeui-block="button-059"] [data-part="hint"]{
min-height:1rem;font-size:0.75rem;color:var(--vibeui-button-059-muted);text-align:center;
visibility:hidden;
}
[data-vibeui-block="button-059"]:has(:invalid) [data-part="hint"]{visibility:visible}
[data-vibeui-block="button-059"] [data-part="lock"]{
flex:none;width:0.75rem;height:0.75rem;border-radius:2px;
background:currentColor;opacity:.8;
clip-path:polygon(0 40%,100% 40%,100% 100%,0 100%);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="button-059"] *{animation:none!important;transition:none!important}}
`

/**
 * Кнопка отправки, заблокированная до валидности формы — средствами :has().
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Button059({
  label = "Рабочая почта",
  placeholder = "name@company.com",
  hint = "Введите адрес, чтобы отправить",
  submitLabel = "Отправить заявку",
  accent,
  className,
  style,
  ...props
}: Button059Props) {
  const palette = {
    ...(accent ? { "--vibeui-button-059-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-button-059" precedence="medium">
        {STYLES}
      </style>
      <form
        {...props}
        data-vibeui-block="button-059"
        className={className}
        style={palette}
      >
        <label htmlFor="vibeui-button-059-email">{label}</label>
        <input
          id="vibeui-button-059-email"
          name="email"
          type="email"
          required
          placeholder={placeholder}
        />
        <button type="submit" data-part="submit">
          <span data-part="lock" aria-hidden="true" />
          {submitLabel}
        </button>
        <p data-part="hint">{hint}</p>
      </form>
    </>
  )
}