דילוג לתוכן
uiify

Button

כפתור פעולה עם וריאציות וגדלים, כולל כפתור עם סמל בלבד.

דפדפנים
Chrome 123+Firefox 121+Safari 17.5+
שכבה
0
גודל חבילה דחוסה
340 B
מגבלת פריסה כפויה
0 ms
מגבלת Axe
0
תואם SSR
כן
חוזה מפורסם
סביבת בדיקות

מגרש ניסויים

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>
  );
}
/* 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;
  }
}
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.
selectedboolean—מגדיר data-selected לפריט שנבחר או לכפתור החלפה פעיל; <button> מובנה מקבל גם aria-pressed, אלא אם סופק מצב ARIA אחר.
disabledbooleanfalseמשבית את הכפתור: באמצעות המאפיין המובנה על <button>, או aria-disabled עם חסימת הפעלה על כל אלמנט אחר.
type"button" | "submit" | "reset""button"סוג הכפתור המובנה, המוחל רק כשהאלמנט המוצג הוא <button>. בתוך טופס יש להגדיר "submit".
asElementType"button"אלמנט או רכיב להצגה, למשל "a" או Link של הנתב. כל המאפיינים וה-ref מועברים אליו.
renderReactElement | ((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)
דף זה תורגם אוטומטית וטרם נבדק.