Form
טופס עם התנהגות אימות.
- דפדפנים
- Chrome 123+Firefox 121+Safari 17.5+
- שכבה
- 0
- גודל חבילה דחוסה
- 194 B
- מגבלת פריסה כפויה
- 0 ms
- מגבלת Axe
- 0
- תואם SSR
- כן
- חוזה מפורסם
- סביבת בדיקות
מגרש ניסויים
import { Button, Field, Form } from "@gunkin/uiify";
export default function Example() {
return (
<Form>
<Field.Root className="" id="email">
<Field.Label className="" fieldId="email">
Email
</Field.Label>
<Field.Control className="" as="input" fieldId="email" name="email" type="email" />
<Button className="" type="submit">
Subscribe
</Button>
</Field.Root>
</Form>
);
}/* Button — pill-shaped and monochrome: a filled foreground action by default, secondary for
the tonal one, outline for the bordered one, ghost for a surface that only appears on
hover, link for inline text actions.
Filled variants are lit from above by two layers that sit between the fill and the label:
- `::before`, the wash: a blurred copy of the shape raised by half its height, so only its
soft lower edge glows inside the upper half. `filter` runs before `clip-path`, which lets
one box blur itself and still be cut back to the button outline.
- `::after`, the rim: a half-pixel ring whose colour fades out towards the bottom. A
box-shadow cannot take a gradient and `border-image` ignores the radius, so the ring is
a gradient box with its content box masked out. */
@layer components {
:where([data-uiify-button]) {
--uiify-button-rim: rgb(255 255 255 / 20%);
--uiify-button-wash: light-dark(rgb(255 255 255 / 15%), rgb(255 255 255 / 30%));
position: relative;
isolation: isolate;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
min-block-size: var(--control-md);
/* Never narrower than tall: an icon size only has to drop the padding to be a circle. */
min-inline-size: var(--control-md);
padding-block: 0;
padding-inline: 1.25rem;
border: var(--border-width) solid transparent;
border-radius: var(--radius-full);
background-color: var(--foreground);
background-clip: padding-box;
color: var(--background);
font-size: var(--text-base);
font-weight: var(--weight-medium);
line-height: 1.5rem;
text-decoration: none;
cursor: pointer;
transition: var(--transition-colors);
}
:where([data-uiify-button])::before,
:where([data-uiify-button])::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
pointer-events: none;
}
:where([data-uiify-button])::before {
background-color: var(--uiify-button-wash);
filter: blur(10px);
translate: 0 -50%;
clip-path: inset(50% 0 -50% 0 round var(--radius-full));
}
:where([data-uiify-button])::after {
padding: 0.5px;
background-image: linear-gradient(to bottom, var(--uiify-button-rim), transparent 75%);
mask:
linear-gradient(#fff 0 0) content-box exclude,
linear-gradient(#fff 0 0);
}
/* Sizes: md is the default metrics above. */
[data-uiify-button]:is([data-size="sm"], [data-size="icon-sm"]) {
min-block-size: var(--control-sm);
min-inline-size: var(--control-sm);
padding-inline: 0.75rem;
gap: 0.25rem;
font-size: var(--text-sm);
line-height: 1.25rem;
}
[data-uiify-button]:is([data-size="lg"], [data-size="icon-lg"]) {
min-block-size: var(--control-lg);
min-inline-size: var(--control-lg);
padding-inline: 1.75rem;
font-size: var(--text-lg);
line-height: 1.75rem;
}
/* Icon sizes: the same three heights with no padding, for a single icon. */
[data-uiify-button][data-size^="icon"] {
padding-inline: 0;
}
@media (any-hover: hover) and (any-pointer: fine) {
[data-uiify-button]:hover:not(:disabled, [data-disabled]) {
background-color: color-mix(in oklch, var(--foreground) 90%, var(--background));
}
}
[data-uiify-button]:active:not(:disabled, [data-disabled]) {
background-color: color-mix(in oklch, var(--foreground) 80%, var(--background));
}
[data-uiify-button]:disabled,
[data-uiify-button][data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
/* Secondary: tonal low-emphasis fill. */
[data-uiify-button][data-variant="secondary"] {
--uiify-button-rim: light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 10%));
--uiify-button-wash: light-dark(rgb(255 255 255 / 20%), rgb(255 255 255 / 5%));
background-color: color-mix(in oklch, var(--foreground) 10%, transparent);
color: var(--foreground);
}
/* Outline: no fill and no wash, a hairline border in the text colour. Its rim is a full
border-width ring laid over that border, so the top edge reads slightly stronger and
fades into the plain border below. */
[data-uiify-button][data-variant="outline"] {
--uiify-button-rim: color-mix(in oklch, var(--foreground) 25%, transparent);
border-color: color-mix(in oklch, var(--foreground) 20%, transparent);
background-color: transparent;
color: var(--foreground);
}
[data-uiify-button][data-variant="outline"]::before {
content: none;
}
[data-uiify-button][data-variant="outline"]::after {
inset: calc(-1 * var(--border-width));
padding: var(--border-width);
}
[data-uiify-button][data-variant="outline"]:active:not(:disabled, [data-disabled]) {
background-color: color-mix(in oklch, var(--foreground) 16%, transparent);
}
/* Ghost: text only; the tonal surface appears on hover. Flat, so no light layers. */
[data-uiify-button][data-variant="ghost"] {
background-color: transparent;
color: var(--foreground);
}
[data-uiify-button][data-variant="ghost"]::before,
[data-uiify-button][data-variant="ghost"]::after {
content: none;
}
[data-uiify-button][data-variant="ghost"]:active:not(:disabled, [data-disabled]) {
background-color: color-mix(in oklch, var(--foreground) 16%, transparent);
}
[data-uiify-button][data-variant="secondary"]:active:not(:disabled, [data-disabled]) {
background-color: color-mix(in oklch, var(--foreground) 26%, transparent);
}
/* Destructive: irreversible or dangerous actions. */
[data-uiify-button][data-variant="destructive"] {
--uiify-button-wash: rgb(255 255 255 / 15%);
background-color: var(--destructive);
color: var(--destructive-foreground);
}
[data-uiify-button][data-variant="destructive"]:active:not(:disabled, [data-disabled]) {
background-color: color-mix(in oklch, var(--destructive), black 20%);
}
@media (any-hover: hover) and (any-pointer: fine) {
[data-uiify-button][data-variant="outline"]:hover:not(:disabled, [data-disabled]),
[data-uiify-button][data-variant="ghost"]:hover:not(:disabled, [data-disabled]) {
background-color: color-mix(in oklch, var(--foreground) 10%, transparent);
}
[data-uiify-button][data-variant="secondary"]:hover:not(:disabled, [data-disabled]) {
background-color: color-mix(in oklch, var(--foreground) 20%, transparent);
}
[data-uiify-button][data-variant="destructive"]:hover:not(:disabled, [data-disabled]) {
background-color: color-mix(in oklch, var(--destructive), black 12%);
}
}
}
@layer components {
/* Selected: the chosen item of a set or a toggle that is on, filled with the brand colour.
The state is read from whatever already carries it: the `selected` prop, a pressed toggle,
the current page, or — for a label styled as a button — the checked native control right
before it, so a toggle works with no JavaScript. */
[data-uiify-button]:is([data-selected], [aria-pressed="true"], [aria-current="page"]),
:checked + [data-uiify-button] {
--uiify-button-rim: rgb(255 255 255 / 20%);
--uiify-button-wash: rgb(255 255 255 / 15%);
border-color: transparent;
background-color: var(--primary);
color: var(--primary-foreground);
}
@media (any-hover: hover) and (any-pointer: fine) {
[data-uiify-button]:is([data-selected], [aria-pressed="true"], [aria-current="page"]):hover:not(
:disabled,
[data-disabled]
),
:checked:not(:disabled) + [data-uiify-button]:hover {
background-color: color-mix(in oklch, var(--primary) 88%, var(--primary-foreground));
}
}
/* A label styled as a button follows the native control it belongs to. */
:disabled + [data-uiify-button] {
opacity: 0.5;
cursor: not-allowed;
}
[data-uiify-button][hidden] {
display: none;
}
}
@layer components {
/* Link: an inline text action with no surface, on a button or an anchor alike. */
[data-uiify-button][data-variant="link"],
[data-uiify-button][data-variant="link"]:active {
padding-inline: 0;
border: 0;
border-radius: 0;
background: transparent;
color: var(--primary);
text-decoration: underline;
text-underline-offset: 0.2em;
}
[data-uiify-button][data-variant="link"] {
min-inline-size: 0;
}
@media (any-hover: hover) and (any-pointer: fine) {
[data-uiify-button][data-variant="link"]:hover {
padding-inline: 0;
border: 0;
border-radius: 0;
background: transparent;
color: var(--primary);
text-decoration: underline;
text-underline-offset: 0.2em;
}
}
[data-uiify-button][data-variant="link"]::before,
[data-uiify-button][data-variant="link"]::after {
content: none;
}
[data-uiify-button][data-variant="link"]:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 3px;
}
}
/* Text fields — Input and Textarea share the same surface. Invalid state keys
off the [data-invalid] attribute the components emit. Textarea autosize uses
the native field-sizing (Chrome 123+) when the component sets data-autosize. */
@layer components {
:where([data-uiify-input], [data-uiify-textarea]) {
display: block;
inline-size: 100%;
max-inline-size: 24rem;
padding-block: var(--space-sm);
padding-inline: var(--space-md);
border: var(--border-width) solid var(--input);
border-radius: var(--radius-md);
background-color: var(--card);
color: var(--foreground);
font-size: var(--uiify-control-text);
line-height: var(--leading-normal);
transition: var(--transition-colors);
}
:where([data-uiify-input], [data-uiify-textarea])::placeholder {
color: var(--muted-foreground);
}
:where([data-uiify-input], [data-uiify-textarea]):focus-visible {
border-color: var(--primary);
}
:where([data-uiify-input], [data-uiify-textarea]):disabled,
:where([data-uiify-input], [data-uiify-textarea])[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
:where([data-uiify-input], [data-uiify-textarea])[data-invalid],
:where([data-uiify-input], [data-uiify-textarea]):user-invalid {
border-color: var(--destructive);
}
:where([data-uiify-input], [data-uiify-textarea])[data-invalid]:focus-visible {
outline-color: var(--destructive);
}
/* Input is a pill sized like Button: --control-md tall and base text. Textarea keeps its own
rounded rectangle. */
[data-uiify-input] {
box-sizing: border-box;
block-size: var(--uiify-control-size, var(--control-md));
padding-block: 0;
padding-inline: var(--space-lg);
border-radius: var(--radius-full);
font-size: var(--uiify-control-font, var(--text-base));
}
[data-uiify-textarea] {
min-block-size: 4lh;
resize: block;
}
[data-uiify-textarea][data-autosize] {
field-sizing: content;
resize: none;
}
[data-uiify-field] {
display: grid;
gap: var(--space-sm);
inline-size: 100%;
max-inline-size: 24rem;
}
[data-uiify-field] [data-part="label"] {
color: var(--foreground);
font-size: var(--text-sm);
font-weight: var(--weight-medium);
line-height: var(--leading-normal);
}
[data-uiify-field] [data-part="label"][data-required]::after {
content: " *";
color: var(--destructive);
}
[data-uiify-field] [data-part="description"] {
margin: 0;
color: var(--muted-foreground);
font-size: var(--text-xs);
line-height: var(--leading-normal);
}
[data-uiify-field] [data-part="error"] {
margin: 0;
color: var(--destructive);
font-size: var(--text-xs);
line-height: var(--leading-normal);
}
/* Shared presence fade when an error message is mounted. */
@media (prefers-reduced-motion: no-preference) {
[data-uiify-field] [data-part="error"] {
transition: opacity var(--duration-fast) var(--ease-out);
}
@starting-style {
[data-uiify-field] [data-part="error"] {
opacity: 0;
}
}
}
}import { Button, Field, Form } from "@gunkin/uiify";
export default function Example() {
return (
<Form>
<Field.Root className="grid gap-sm [inline-size:100%] [max-inline-size:24rem]" id="email">
<Field.Label
className={
"text-foreground text-sm font-[number:var(--weight-medium)] [line-height:var(--leading-normal)] data-[re" +
"quired]:after:content-[\"_*\"] data-[required]:after:text-destructive"
}
fieldId="email"
>
Email
</Field.Label>
<Field.Control
className="
focus-visible:[outline:none]
focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_20%,_transparent)] block
[inline-size:100%] [max-inline-size:24rem] box-border [block-size:var(--control-md)] py-0 px-lg
border-[length:var(--border-width)] border-solid border-input rounded-full bg-card text-foreground
[font-size:var(--text-base)] [line-height:var(--leading-normal)] [transition:var(--transition-colors)]
placeholder:text-muted-foreground focus-visible:border-primary disabled:opacity-50
disabled:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed
data-[invalid]:border-destructive user-invalid:border-destructive
data-[invalid]:focus-visible:outline-destructive
"
as="input"
fieldId="email"
name="email"
type="email"
/>
<Button
className={
"relative isolate inline-flex items-center justify-center gap-1.5 min-h-10 min-w-10 px-5 py-px border-[l" +
"ength:var(--border-width)] border-solid border-transparent rounded-full bg-clip-padding text-base font-" +
"medium leading-6 no-underline cursor-pointer [transition:var(--transition-colors)] focus-visible:[outli" +
"ne:none] focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_20%,_transpa" +
"rent)] disabled:opacity-50 disabled:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:curso" +
"r-not-allowed [&:is([data-selected],[aria-pressed=true],[aria-current=page])]:[--uiify-button-rim:rgb(2" +
"55_255_255_/_20%)] [&:is([data-selected],[aria-pressed=true],[aria-current=page])]:[--uiify-button-wash" +
":rgb(255_255_255_/_15%)] [&:is([data-selected],[aria-pressed=true],[aria-current=page])]:border-transpa" +
"rent [&:is([data-selected],[aria-pressed=true],[aria-current=page])]:bg-primary [&:is([data-selected],[" +
"aria-pressed=true],[aria-current=page])]:text-primary-foreground [:checked+&]:[--uiify-button-rim:rgb(2" +
"55_255_255_/_20%)] [:checked+&]:[--uiify-button-wash:rgb(255_255_255_/_15%)] [:checked+&]:border-transp" +
"arent [:checked+&]:bg-primary [:checked+&]:text-primary-foreground [@media_(any-hover:_hover)_and_(any-" +
"pointer:_fine)]:[&:is([data-selected],[aria-pressed=true],[aria-current=page]):hover:not(:disabled,[dat" +
"a-disabled])]:[background-color:color-mix(in_oklch,_var(--primary)_88%,_var(--primary-foreground))] [@m" +
"edia_(any-hover:_hover)_and_(any-pointer:_fine)]:[:checked:not(:disabled)+&:hover]:[background-color:co" +
"lor-mix(in_oklch,_var(--primary)_88%,_var(--primary-foreground))] bg-foreground text-background [@media" +
"_(any-hover:_hover)_and_(any-pointer:_fine)]:hover:[&:not(:disabled,[data-disabled])]:[background-color" +
":color-mix(in_oklch,_var(--foreground)_90%,_var(--background))] active:[&:not(:disabled,[data-disabled]" +
")]:[background-color:color-mix(in_oklch,_var(--foreground)_80%,_var(--background))]"
}
type="submit"
>
Subscribe
</Button>
</Field.Root>
</Form>
);
}API
Form
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
onSubmit / onReset / action / method / ref | native form props | — | אירועי טופס תקניים ו-ref של ה-DOM מועברים הלאה. Formik ו-React Hook Form מנהלים ערכים, שגיאות ואיפוס באמצעות ממשקי ה-API הרגילים שלהם. |
validationBehavior | "native" | "aria" | "native" | קובע אם הדפדפן מדווח בעצמו על הפרות אילוצים, גם ללא JavaScript, או שהאימות נשען על מאפייני ARIA; הערך "aria" מגדיר noValidate. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
מקלדת
| מקש | פעולה |
|---|---|
| Tab | העברת המיקוד אל הפקד או ממנו |
| Enter / Space | הפעלה כשהפקד ממוקד (פקדים דמויי כפתור) |
נגישות
משתמש ב-HTML סמנטי ככל האפשר. בדקו את הסימון המרונדר במגרש הניסויים — ARIA נוסף רק כאשר האלמנטים המובנים אינם מספיקים.
נקודות חיבור לעיצוב
data-disabled