Button
כפתור פעולה עם וריאציות וגדלים, כולל כפתור עם סמל בלבד.
- דפדפנים
- Chrome 123+, Firefox 121+, Safari 17.5+
- שכבה
- 0
- גודל חבילה דחוסה
- 340 B
- מגבלת פריסה כפויה
- 0 ms
- מגבלת Axe
- 0
- תואם SSR
- כן
- חוזה מפורסם
- סביבת בדיקות
מגרש ניסויים
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
import { Button, Spinner } from "@gunkin/uiify";
export default function Example() {
const loading = false;
return (
<Button className="" disabled={loading}>
<span>Save</span>
{loading ? <Spinner className="" size="sm" /> : null}
</Button>
);
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
/* 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;
}
}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
import { Button, Spinner } from "@gunkin/uiify";
export default function Example() {
const loading = false;
return (
<Button
className={
"relative isolate inline-flex items-center justify-center gap-1.5 min-h-10 min-w-10 px-5 py-px border-[lengt" +
"h:var(--border-width)] border-solid border-transparent rounded-full bg-clip-padding text-base font-medium l" +
"eading-6 no-underline cursor-pointer [transition:var(--transition-colors)] focus-visible:[outline:none] foc" +
"us-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_20%,_transparent)] disabled:" +
"opacity-50 disabled:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed [&:is(" +
"[data-selected],[aria-pressed=true],[aria-current=page])]:[--uiify-button-rim:rgb(255_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-transparent [&:is([data-selected],[aria" +
"-pressed=true],[aria-current=page])]:bg-primary [&:is([data-selected],[aria-pressed=true],[aria-current=pag" +
"e])]:text-primary-foreground [:checked+&]:[--uiify-button-rim:rgb(255_255_255_/_20%)] [:checked+&]:[--uiify" +
"-button-wash:rgb(255_255_255_/_15%)] [:checked+&]:border-transparent [:checked+&]:bg-primary [:checked+&]:t" +
"ext-primary-foreground [@media_(any-hover:_hover)_and_(any-pointer:_fine)]:[&:is([data-selected],[aria-pres" +
"sed=true],[aria-current=page]):hover:not(:disabled,[data-disabled])]:[background-color:color-mix(in_oklch,_" +
"var(--primary)_88%,_var(--primary-foreground))] [@media_(any-hover:_hover)_and_(any-pointer:_fine)]:[:check" +
"ed:not(:disabled)+&:hover]:[background-color:color-mix(in_oklch,_var(--primary)_88%,_var(--primary-foregrou" +
"nd))] bg-foreground text-background [@media_(any-hover:_hover)_and_(any-pointer:_fine)]:hover:[&:not(:disab" +
"led,[data-disabled])]:[background-color:color-mix(in_oklch,_var(--foreground)_90%,_var(--background))] acti" +
"ve:[&:not(:disabled,[data-disabled])]:[background-color:color-mix(in_oklch,_var(--foreground)_80%,_var(--ba" +
"ckground))]"
}
disabled={loading}
>
<span>Save</span>
{loading ? (
<Spinner
className={
"relative inline-flex items-center [gap:0.5rem] text-primary before:flex-none before:block before:[inlin" +
"e-size:2rem] before:[block-size:2rem] before:content-[''] before:rounded-[50%] before:[background:conic" +
"-gradient(_from_calc(-90deg_+_var(--uiify-spinner-tail)),_currentcolor_0_var(--uiify-spinner-length),_t" +
"ransparent_0_)] before:[mask:radial-gradient(farthest-side,_transparent_calc(100%_-_4px),_#000_calc(100" +
"%_-_3px))] before:[animation:uiify-spinner-rotate_2s_linear_infinite,_uiify-spinner-orbit_1.5s_ease-in-" +
"out_infinite] data-[value]:before:[--uiify-spinner-tail:0deg] data-[value]:before:[--uiify-spinner-leng" +
"th:calc(attr(data-value_type(<number>),_0)_*_3.6deg)] data-[value]:before:[background:conic-gradient(_f" +
"rom_0deg,_currentcolor_0_var(--uiify-spinner-length),_color-mix(in_oklch,_currentcolor_20%,_transparent" +
")_0_)] data-[value]:before:[animation:none] data-[size=sm]:before:[inline-size:1.25rem] data-[size=sm]:" +
"before:[block-size:1.25rem] data-[size=sm]:before:[mask:radial-gradient(farthest-side,_transparent_calc" +
"(100%_-_3px),_#000_calc(100%_-_1px))] data-[size=lg]:before:[inline-size:6.25rem] data-[size=lg]:before" +
":[block-size:6.25rem] data-[size=lg]:before:[mask:radial-gradient(farthest-side,_transparent_calc(100%_" +
"-_4px),_#000_calc(100%_-_3px))] data-[align=center]:[margin-inline:auto] data-[align=fixed]:[inset-bloc" +
"k-start:50%] data-[align=fixed]:[inset-inline-start:50%] data-[align=fixed]:[translate:-50%_-50%] data-" +
"[align=absolute]:[inset-block-start:50%] data-[align=absolute]:[inset-inline-start:50%] data-[align=abs" +
"olute]:[translate:-50%_-50%] data-[align=fixed]:fixed data-[align=absolute]:absolute motion-reduce:[&:n" +
"ot([data-value])::before]:[animation:uiify-spinner-orbit_2.5s_ease-in-out_infinite]"
}
size="sm"
/>
) : null}
</Button>
);
}API
Button
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
variant | "secondary" | "outline" | "ghost" | "destructive" | "link" | — | ההדגשה החזותית הנכתבת ל-data-variant. יש להשמיט אותה לפעולה הראשית בעלת הרקע המלא. |
size | "sm" | "md" | "lg" | "icon-sm" | "icon" | "icon-lg" | — | גובה וריווח פנימי באמצעות data-size (32, 44 או 56 px, עם md כברירת מחדל); גדלי אייקונים מבטלים את הריווח הפנימי הצדדי ומחייבים aria-label. |
selected | boolean | — | מגדיר data-selected לפריט שנבחר או לכפתור החלפה פעיל; <button> מובנה מקבל גם aria-pressed, אלא אם סופק מצב ARIA אחר. |
disabled | boolean | false | משבית את הכפתור: באמצעות המאפיין המובנה על <button>, או aria-disabled עם חסימת הפעלה על כל אלמנט אחר. |
type | "button" | "submit" | "reset" | "button" | סוג הכפתור המובנה, המוחל רק כשהאלמנט המוצג הוא <button>. בתוך טופס יש להגדיר "submit". |
as | ElementType | "button" | אלמנט או רכיב להצגה, למשל "a" או Link של הנתב. כל המאפיינים וה-ref מועברים אליו. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
מקלדת
| מקש | פעולה |
|---|---|
| Enter / Space | הפעלה |
| Tab | מיקוד |
נגישות
מרנדר <button type="button"> מובנה: הדפדפן מספק מיקוד, Enter ו-Space, והכפתור לעולם אינו שולח טופס בטעות. עם as או render הוא שומר על תפקיד האלמנט, כך שעוגן נשאר קישור. disabled מובנה בכפתורים, ובמקומות אחרים נעשה שימוש ב-aria-disabled עם tabindex="-1". selected הופך ל-aria-pressed; קישור נבחר צריך aria-current. כפתורים עם סמל בלבד צריכים aria-label.
נקודות חיבור לעיצוב
data-uiify-buttondata-disableddata-selecteddata-variantdata-size--uiify-button-wash (colour of the glow in the upper half)--uiify-button-rim (colour of the top edge highlight)