דילוג לתוכן
uiify

Select

מעטפת לאלמנט select המובנה.

דפדפנים
Chrome 135+Firefox 121+
למה הגרסה הזאת?

Firefox keeps the native select control. This custom styling uses ::picker(select), :open, and option::checkmark, which Firefox does not support yet.

Safari 17+
למה הגרסה הזאת?

Safari keeps the native select control. This custom styling uses ::picker(select), :open, and option::checkmark, which Safari does not support yet.

שכבה
0
גודל חבילה דחוסה
535 B
מגבלת פריסה כפויה
0 ms
מגבלת Axe
0
תואם SSR
כן
חוזה מפורסם
סביבת בדיקות

מגרש ניסויים

import { Select } from "@gunkin/uiify";

export default function Example() {
  return (
    <Select.Root className="" name="plan" aria-label="Plan" defaultValue="free">
      <Select.Content>
        <Select.Item className="" value="free">
          Free
        </Select.Item>
        <Select.Item className="" value="pro">
          Pro
        </Select.Item>
      </Select.Content>
    </Select.Root>
  );
}
@layer components {
  [data-uiify-select] {
    inline-size: 100%;

    /* base-select sizes the closed control to its selected content rather than
       to the widest option, so the control would resize as the value changes.
       A floor keeps it stable. */
    min-inline-size: 12rem;
    max-inline-size: 16rem;
    display: flex;
    align-items: center;
    gap: var(--space-md);
    box-sizing: border-box;
    block-size: var(--uiify-control-size, var(--control-md));
    padding-block: 0;
    padding-inline: var(--space-lg);
    border: var(--border-width) solid var(--input);
    border-radius: var(--radius-full);
    background-color: var(--card);
    color: var(--foreground);
    font-size: var(--uiify-control-font, var(--text-base));
    line-height: var(--leading-normal);
    cursor: pointer;
    transition: var(--transition-colors);
  }

  [data-uiify-select]:focus-visible {
    border-color: var(--primary);
  }

  [data-uiify-select]:disabled,
  [data-uiify-select][data-disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  /* Chevron — a border-corner mark, same technique as the accordion's
     disclosure triangle, replacing the browser's default picker-icon glyph
     (which renders as a crude filled triangle with no theming hook). */
  [data-uiify-select]::picker-icon {
    content: "";
    display: inline-block;
    inline-size: 0.3rem;
    block-size: 0.3rem;

    /* Safari's user-agent sheet pads ::picker-icon, which stretched the corner into a
       chevron about twice Chromium's size. */
    padding: 0;
    margin-block-end: 0.1rem;
    margin-inline-start: auto;
    border-inline-end: 2px solid var(--muted-foreground);
    border-block-end: 2px solid var(--muted-foreground);
    rotate: 45deg;
    transition: rotate var(--duration-fast) var(--ease-out);
  }

  [data-uiify-select]:open::picker-icon {
    margin-block-end: -0.2rem;
    rotate: 225deg;
  }

  /* Picker surface — matches [data-uiify-menu] in menu.css. `inset-block-start` is set
     explicitly with an anchor() offset, not `margin-block-start`: this box
     is already anchor-positioned by the browser's implicit select/picker
     association, and a plain margin on it is accepted (computed style
     reflects it) but has no visible effect. The shared native geometry lives
     in behavior.css; this picker keeps its own explicit gap. */
  [data-uiify-select]::picker(select) {
    min-inline-size: 12rem;
    max-block-size: min(24rem, calc(100dvb - 2rem));
    padding: var(--space-xs);
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-lg);
    background-color: var(--popover);
    color: var(--popover-foreground);
    box-shadow: var(--shadow-popover);
    overscroll-behavior: contain;
    inset-block-start: calc(anchor(bottom) + var(--space-sm));
  }

  @media (prefers-reduced-motion: no-preference) {
    [data-uiify-select]::picker(select) {
      transition: var(--transition-presence);
    }

    [data-uiify-select]:not(:open)::picker(select) {
      opacity: 0;
      scale: var(--presence-scale);
    }

    @starting-style {
      [data-uiify-select]:open::picker(select) {
        opacity: 0;
        scale: var(--presence-scale);
      }
    }
  }

  /* Group label — matches [data-uiify-menu][data-part="label"], including its padding-inline:
     var(--space-md) inset. <option> is a real DOM child of <optgroup>, so
     that same inline padding would otherwise stack with option's own
     padding-inline and double-indent every grouped item (measured directly:
     +12px versus an ungrouped option) — the option rule just below cancels
     it back out with an equal negative margin plus a matching width bump,
     restoring the same effective inset ungrouped options have. */
  [data-uiify-select] optgroup {
    padding-block-start: var(--space-xs);
    padding-inline: var(--space-md);
    color: var(--muted-foreground);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
  }

  [data-uiify-select] optgroup option {
    margin-inline: calc(-1 * var(--space-md));
    inline-size: calc(100% + 2 * var(--space-md));
  }

  /* Option — matches [data-uiify-menu][data-part="item"]. `color` is set explicitly, not left
     to inherit: a grouped <option> is a DOM child of the muted, semibold
     <optgroup> above, and CSS inheritance would otherwise hand it that same
     muted color (font-weight is unaffected only because the browser's own
     UA stylesheet resets it on <option> directly, which beats inheritance;
     color has no such reset). */
  [data-uiify-select] option {
    display: flex;
    align-items: center;
    column-gap: var(--space-md);
    padding-block: var(--space-sm);
    padding-inline: var(--space-md);
    border-radius: calc(var(--radius-lg) - var(--space-xs));
    color: var(--foreground);
    cursor: pointer;
    font-size: var(--text-base) !important;
  }

  /* :focus-visible, not :focus: the browser gives the currently-selected
     option real DOM focus the instant the picker opens (a11y default — a
     keyboard/screen-reader user should land on their current value), so
     :focus alone made the selected row look permanently "hovered" even
     before the pointer touched anything, indistinguishable from actually
     hovering a different row. :focus-visible's browser heuristic only
     matches once the user actually drives focus with the keyboard, leaving
     an untouched selection to show nothing but its checkmark. */
  [data-uiify-select] option:hover,
  [data-uiify-select] option:focus-visible {
    background-color: var(--accent);
    color: var(--accent-foreground);
  }

  [data-uiify-select] option:disabled {
    opacity: 0.5;
    pointer-events: none;
  }

  /* Selection marker — matches the menu's checkbox item-indicator (filled
     accent square, not the radio dot): ::checkmark only ever exists on the
     selected option, so there's no separate "unchecked" state to draw. The
     check is punched out of the square with an even-odd clip-path polygon
     (the zero-width bridge from the corner to the check cancels itself)
     instead of typeset as a glyph: U+2713 falls back to a different font in
     each engine, and in Safari it sat above and left of centre. The polygon is
     the Checkbox checkmark's, placed in the same 1rem inset. The hole shows the
     popover surface, which is what the theme's --primary-foreground already
     contrasts with. */
  [data-uiify-select] option::checkmark {
    order: -1;
    box-sizing: border-box;
    inline-size: 1.25rem;
    block-size: 1.25rem;
    flex: 0 0 1.25rem;
    display: block;
    border-radius: var(--radius-xs);
    background: var(--primary);
    content: "";
    clip-path: polygon(
      evenodd,
      0 0,
      100% 0,
      100% 100%,
      0 100%,
      0 0,
      32.72% 47.12%,
      26% 57.2%,
      44.24% 74%,
      74% 36.56%,
      66.32% 28.88%,
      44.24% 58.64%,
      32.72% 47.12%
    );
  }

  [data-uiify-select] hr {
    margin-block: var(--space-xs);
    border: 0;
    border-block-start: var(--border-width) solid var(--border);
  }

  @media (prefers-reduced-motion: reduce) {
    [data-uiify-select]::picker-icon,
    [data-uiify-select]::picker(select) {
      transition: none;
    }
  }
}
import { Select } from "@gunkin/uiify";

export default function Example() {
  return (
    <Select.Root
      className="
        [&[data-positioning=native]]:[--uiify-anchor-offset:var(--space-sm)]
        [&_[data-positioning=native]]:[--uiify-anchor-offset:var(--space-sm)]
        [&[popover]:popover-open:not([data-positioning=native])]:[margin:revert]
        [&_[popover]:popover-open:not([data-positioning=native])]:[margin:revert] focus-visible:[outline:none]
        focus-visible:[box-shadow:0_0_0_var(--ring-width)_color-mix(in_oklch,_var(--ring)_20%,_transparent)]
        [inline-size:100%] [min-inline-size:12rem] [max-inline-size:16rem] flex items-center gap-md 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)]
        cursor-pointer [transition:var(--transition-colors)] focus-visible:border-primary disabled:opacity-50
        disabled:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed
        [&::picker-icon]:content-[''] [&::picker-icon]:inline-block [&::picker-icon]:[inline-size:0.3rem]
        [&::picker-icon]:[block-size:0.3rem] [&::picker-icon]:p-0 [&::picker-icon]:[margin-block-end:0.1rem]
        [&::picker-icon]:[margin-inline-start:auto] [&::picker-icon]:[border-inline-end-width:2px]
        [&::picker-icon]:[border-inline-end-style:solid]
        [&::picker-icon]:[border-inline-end-color:var(--muted-foreground)] [&::picker-icon]:[border-block-end-width:2px]
        [&::picker-icon]:[border-block-end-style:solid]
        [&::picker-icon]:[border-block-end-color:var(--muted-foreground)] [&::picker-icon]:[rotate:45deg]
        [&::picker-icon]:[transition:rotate_var(--duration-fast)_var(--ease-out)]
        [&:open::picker-icon]:[margin-block-end:-0.2rem] [&:open::picker-icon]:[rotate:225deg]
        [&::picker(select)]:[min-inline-size:12rem] [&::picker(select)]:[max-block-size:min(24rem,_calc(100dvb_-_2rem))]
        [&::picker(select)]:p-xs [&::picker(select)]:border-[length:var(--border-width)]
        [&::picker(select)]:border-solid [&::picker(select)]:border-border [&::picker(select)]:rounded-lg
        [&::picker(select)]:bg-popover [&::picker(select)]:text-popover-foreground [&::picker(select)]:shadow-popover
        [&::picker(select)]:overscroll-contain
        [&::picker(select)]:[inset-block-start:calc(anchor(bottom)_+_var(--space-sm))]
        motion-safe:[&::picker(select)]:[transition:var(--transition-presence)]
        motion-safe:[&:not(:open)::picker(select)]:opacity-0
        motion-safe:[&:not(:open)::picker(select)]:[scale:var(--presence-scale)]
        motion-safe:starting:[&:open::picker(select)]:opacity-0
        motion-safe:starting:[&:open::picker(select)]:[scale:var(--presence-scale)]
        [&_optgroup_option]:[margin-inline:calc(-1_*_var(--space-md))]
        [&_optgroup_option]:[inline-size:calc(100%_+_2_*_var(--space-md))]
        motion-reduce:[&::picker-icon]:[transition:none] motion-reduce:[&::picker(select)]:[transition:none]
      "
      name="plan"
      aria-label="Plan"
      defaultValue="free"
    >
      <Select.Content>
        <Select.Item
          className={
            "flex items-center gap-x-md py-sm px-md [border-radius:calc(var(--radius-lg)_-_var(--space-xs))] text-fo" +
            "reground cursor-pointer text-base! hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent" +
            " focus-visible:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none [&::checkmark]:[" +
            "order:-1] [&::checkmark]:box-border [&::checkmark]:[inline-size:1.25rem] [&::checkmark]:[block-size:1.2" +
            "5rem] [&::checkmark]:[flex:0_0_1.25rem] [&::checkmark]:block [&::checkmark]:rounded-xs [&::checkmark]:b" +
            "g-primary [&::checkmark]:content-[''] [&::checkmark]:[clip-path:polygon(evenodd,0_0,100%_0,100%_100%,0_" +
            "100%,0_0,32.72%_47.12%,26%_57.2%,44.24%_74%,74%_36.56%,66.32%_28.88%,44.24%_58.64%,32.72%_47.12%)]"
          }
          value="free"
        >
          Free
        </Select.Item>
        <Select.Item
          className={
            "flex items-center gap-x-md py-sm px-md [border-radius:calc(var(--radius-lg)_-_var(--space-xs))] text-fo" +
            "reground cursor-pointer text-base! hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent" +
            " focus-visible:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none [&::checkmark]:[" +
            "order:-1] [&::checkmark]:box-border [&::checkmark]:[inline-size:1.25rem] [&::checkmark]:[block-size:1.2" +
            "5rem] [&::checkmark]:[flex:0_0_1.25rem] [&::checkmark]:block [&::checkmark]:rounded-xs [&::checkmark]:b" +
            "g-primary [&::checkmark]:content-[''] [&::checkmark]:[clip-path:polygon(evenodd,0_0,100%_0,100%_100%,0_" +
            "100%,0_0,32.72%_47.12%,26%_57.2%,44.24%_74%,74%_36.56%,66.32%_28.88%,44.24%_58.64%,32.72%_47.12%)]"
          }
          value="pro"
        >
          Pro
        </Select.Item>
      </Select.Content>
    </Select.Root>
  );
}

API

Select.Root

מאפייןטיפוסברירת מחדלתיאור
size"sm" | "md" | "lg""md"גובה הפקד וגודל הטקסט, בהתאמה ל-Button (32, 40 או 48 px); נכתבים ל-data-size, עם md כשלא הוגדר ערך.
childrenReactNode—חלקי Select, בדרך כלל Select.Content המכיל פריטים וקבוצות.
valuestring—הערך הנבחר, ההופך את תיבת הבחירה למבוקרת.
defaultValuestring—הערך הנבחר בהצגה הראשונה; מכאן והלאה תיבת הבחירה המובנית מנהלת את הבחירה, ולכן היא פועלת ללא JavaScript.
onValueChange(value: string) => void—נקראת עם הערך החדש כשהבחירה משתנה.
namestring—שם שדה הטופס הנשלח עם הערך הנבחר.
disabledbooleanfalseמשבית את תיבת הבחירה המובנית ומגדיר את המאפיין data-disabled.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

Select.Content

מאפייןטיפוסברירת מחדלתיאור
childrenReactNode—פריטים, קבוצות ומפרידים; החלק מציג את ילדיו ללא הוספת אלמנט.

Select.Item

מאפייןטיפוסברירת מחדלתיאור
childrenReactNode—הטקסט המוצג לאפשרות זו.
disabledbooleanfalseמונע בחירה באפשרות זו.
value*string—הערך הנשלח כשאפשרות זו נבחרת.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

Select.Group

מאפייןטיפוסברירת מחדלתיאור
childrenReactNode—הפריטים השייכים לקבוצה זו.
label*string—כותרת הקבוצה המוצגת על ידי קבוצת האפשרויות המובנית.
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

Select.Separator

מאפייןטיפוסברירת מחדלתיאור
renderReactElement | ((props, state) => ReactElement)—החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות.

מקלדת

מקשפעולה
Arrow keysשינוי האפשרות בעת מיקוד
Spaceפתיחת הבורר המובנה (תלוי בפלטפורמה)

נגישות

אלמנט <select> מובנה עם טקסט בלבד; הדפדפן אחראי לבחירה, לקלט מקלדת, למעבר בהקלדה ולשליחת הטופס. מנועים תומכים משפרים רק את מראה הבורר.

נקודות חיבור לעיצוב

data-disabled
דף זה תורגם אוטומטית וטרם נבדק.