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
Use it with AI
- 1. Copy the link.
- 2. Write to your agent in your own words and drop the link into the sentence.
- 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
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.jsonhttps://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>
</>
)
}