דילוג לתוכן
uiify

Combobox

תיבת בחירה משולבת של ARIA עם ערך יחיד או ערכים מרובים וסינון.

דפדפנים
Chrome 114+Firefox 125+Safari 17+
שכבה
2
גודל חבילה דחוסה
5.53 kB
מגבלת פריסה כפויה
0 ms
מגבלת Axe
0
תואם SSR
כן
חוזה מפורסם
סביבת בדיקות

מגרש ניסויים

תפוח
בננה
דובדבן
ReactUI
React
UI
TypeScript
נגישות
"use client";

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

const skills = [
  { label: "React", value: "React" },
  { label: "UI", value: "UI" },
  { label: "TypeScript", value: "TypeScript" },
  { label: "Accessibility", value: "Accessibility" },
];

export default function Example() {
  return (
    <div className="">
      <Combobox.Root multiple defaultValue={["React", "UI"]} items={skills}>
        <Combobox.Control className="">
          <Combobox.Tags
            className=""
            slotProps={{
              tag: { className: "" },
              remove: { className: "" },
            }}
          />
          <Combobox.Input className="" aria-label="Add tag" placeholder="Add tag" />
        </Combobox.Control>
        <Combobox.Content className="">
          {(items) =>
            items.length === 0 ? (
              <Combobox.Empty>Nothing matched. Try another search.</Combobox.Empty>
            ) : (
              items.map((item) => <Combobox.Item className="" item={item} key={item.value} />)
            )
          }
        </Combobox.Content>
      </Combobox.Root>
    </div>
  );
}
/* Combobox — text input + listbox surface + option rows. The active option is
   tracked via aria-activedescendant, surfaced here as [data-highlighted]. */

@layer components {
  [data-uiify-combobox] {
    position: relative;
    inline-size: 100%;
    max-inline-size: 18rem;
  }

  [data-uiify-combobox] [data-part="control"] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs);
    inline-size: 100%;
    box-sizing: border-box;
    min-block-size: var(--uiify-control-size, var(--control-md));
    padding-block: var(--space-xs);
    padding-inline: var(--space-lg);
    border: var(--border-width) solid var(--input);

    /* A pill on one row; when tags wrap onto more rows the corners stay that same radius
       instead of stretching into an oval. */
    border-radius: calc(var(--uiify-control-size, var(--control-md)) / 2);
    background-color: var(--card);
    color: var(--foreground);
    transition: var(--transition-colors);
  }

  [data-uiify-combobox] [data-part="control"]:focus-within {
    border-color: var(--primary);
  }

  [data-uiify-combobox] [data-part="control"][data-disabled],
  [data-uiify-combobox] [data-part="control"]:has([data-part="input"]:disabled) {
    opacity: 0.5;
    cursor: not-allowed;
  }

  [data-uiify-combobox] [data-part="control"] [data-part="tags"] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs);
  }

  [data-uiify-combobox] [data-part="control"] [data-part="tag"] {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    padding-block: var(--space-xs);
    padding-inline: var(--space-sm);
    border-radius: var(--radius-xs);
    background-color: var(--accent);
    color: var(--accent-foreground);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
  }

  [data-uiify-combobox] [data-part="control"] [data-part="tag-remove"] {
    display: inline-grid;
    place-items: center;
    inline-size: 1.25rem;
    block-size: 1.25rem;
    flex: 0 0 1.25rem;
    padding: 0;
    border: 0;
    border-radius: var(--radius-xs);
    background: transparent;
    color: inherit;
    font: inherit;
    line-height: 1;
    cursor: pointer;
  }

  [data-uiify-combobox] [data-part="control"] [data-part="tag-remove"]:hover,
  [data-uiify-combobox] [data-part="control"] [data-part="tag-remove"]:focus-visible {
    background-color: var(--primary);
    color: var(--primary-foreground);
  }

  [data-uiify-combobox] [data-part="input"] {
    inline-size: 100%;
    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));
    transition: var(--transition-colors);
  }

  [data-uiify-combobox] [data-part="input"]:focus-visible {
    border-color: var(--primary);
  }

  [data-uiify-combobox] [data-part="control"] [data-part="input"] {
    flex: 1 1 5rem;
    min-inline-size: 5rem;
    inline-size: auto;
    block-size: auto;
    padding-block: var(--space-xs);
    padding-inline: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    outline: none;
    box-shadow: none;
  }

  [data-uiify-combobox] [data-part="content"]:where(:not([popover]), :popover-open) {
    display: flex;
  }

  [data-uiify-combobox] [data-part="content"] {
    flex-direction: column;
    min-inline-size: 12rem;
    max-block-size: min(24rem, calc(100dvb - 2rem));
    overflow-block: auto;
    overscroll-behavior: contain;
    padding: var(--space-xs);
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-lg);
    background-color: var(--card);
    color: var(--foreground);
    box-shadow: var(--shadow-popover);
    transform-origin: var(--transform-origin, top);
  }

  [data-uiify-combobox] [data-part="content"][data-state="closed"] {
    display: none;
  }

  [data-uiify-combobox] [data-part="item"] {
    display: flex;
    align-items: center;
    inline-size: 100%;
    padding-block: var(--space-sm);
    padding-inline: var(--space-md);
    border: 0;
    border-radius: calc(var(--radius-lg) - var(--space-xs));
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    text-align: start;
    cursor: pointer;
    user-select: none;
  }

  [data-uiify-combobox] [data-part="item"]:hover,
  [data-uiify-combobox] [data-part="item"]:focus-visible,
  [data-uiify-combobox] [data-part="item"][data-highlighted],
  [data-uiify-combobox] [data-part="item"][aria-selected="true"],
  [data-uiify-combobox] [data-part="item"][data-selected] {
    background-color: var(--accent);
    color: var(--accent-foreground);
    outline: none;
  }

  [data-uiify-combobox] [data-part="item"][aria-disabled="true"],
  [data-uiify-combobox] [data-part="item"][data-disabled] {
    opacity: 0.5;
    pointer-events: none;
  }

  [data-uiify-combobox] [data-part="empty"] {
    padding: var(--space-sm) var(--space-md);
    color: var(--muted-foreground);
    font-size: var(--text-base);
  }

  @media (prefers-reduced-motion: no-preference) {
    [data-uiify-combobox] [data-part="content"] {
      transition: var(--transition-presence);
    }

    [data-uiify-combobox] [data-part="content"][popover]:not(:popover-open) {
      opacity: 0;
      scale: var(--presence-scale);
    }

    @starting-style {
      [data-uiify-combobox] [data-part="content"]:popover-open {
        opacity: 0;
        scale: var(--presence-scale);
      }
    }
  }
}
"use client";

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

const skills = [
  { label: "React", value: "React" },
  { label: "UI", value: "UI" },
  { label: "TypeScript", value: "TypeScript" },
  { label: "Accessibility", value: "Accessibility" },
];

export default function Example() {
  return (
    <div className="relative w-full max-w-72">
      <Combobox.Root multiple defaultValue={["React", "UI"]} items={skills}>
        <Combobox.Control
          className="
            flex flex-wrap items-center gap-xs w-full box-border [min-block-size:var(--control-md)] py-xs px-lg
            border-[length:var(--border-width)] border-solid border-input rounded-full bg-card text-foreground
            [transition:var(--transition-colors)] focus-within:border-primary data-[disabled]:opacity-50
            data-[disabled]:cursor-not-allowed has-[[data-part=input]:disabled]:opacity-50
            has-[[data-part=input]:disabled]:cursor-not-allowed
          "
        >
          <Combobox.Tags
            className="flex flex-wrap items-center gap-xs"
            slotProps={{
              tag: {
                className:
                  "inline-flex items-center gap-xs py-xs px-sm rounded-xs bg-accent " +
                  "text-accent-foreground text-xs font-[number:var(--weight-medium)]",
              },
              remove: {
                className:
                  "inline-grid place-items-center size-5 flex-[0_0_1.25rem] p-0 border-0 rounded-xs " +
                  "bg-transparent text-inherit [font:inherit] leading-none cursor-pointer " +
                  "hover:bg-primary hover:text-primary-foreground focus-visible:bg-primary " +
                  "focus-visible:text-primary-foreground",
              },
            }}
          />
          <Combobox.Input
            className="
              w-full 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)]
              [transition:var(--transition-colors)] focus-visible:border-primary [[data-part=control]_&]:flex-[1_1_5rem]
              [[data-part=control]_&]:[min-inline-size:5rem] [[data-part=control]_&]:[inline-size:auto]
              [[data-part=control]_&]:[block-size:auto] [[data-part=control]_&]:py-xs [[data-part=control]_&]:px-0
              [[data-part=control]_&]:border-0 [[data-part=control]_&]:rounded-none
              [[data-part=control]_&]:bg-transparent [[data-part=control]_&]:[outline:none]
              [[data-part=control]_&]:[box-shadow:none]
            "
            aria-label="Add tag"
            placeholder="Add tag"
          />
        </Combobox.Control>
        <Combobox.Content
          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] flex-col [min-inline-size:12rem]
            [max-block-size:min(24rem,calc(100dvb_-_2rem))] overflow-y-auto overscroll-contain p-xs
            border-[length:var(--border-width)] border-solid border-border rounded-lg bg-card text-foreground
            shadow-popover [transform-origin:var(--transform-origin,top)] data-[state=closed]:hidden
            [&:where(:not([popover]),:popover-open)]:flex motion-safe:[transition:var(--transition-presence)]
            motion-safe:data-[state=closed]:opacity-0 motion-safe:data-[state=closed]:[scale:var(--presence-scale)]
            motion-safe:starting:[&:popover-open]:opacity-0
            motion-safe:starting:[&:popover-open]:[scale:var(--presence-scale)]
          "
        >
          {(items) =>
            items.length === 0 ? (
              <Combobox.Empty>Nothing matched. Try another search.</Combobox.Empty>
            ) : (
              items.map((item) => (
                <Combobox.Item
                  className="
                    flex items-center w-full py-sm px-md border-0 rounded-[calc(var(--radius-lg)_-_var(--space-xs))]
                    bg-transparent text-inherit [font:inherit] text-base [line-height:var(--leading-normal)] text-start
                    cursor-pointer select-none hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent
                    focus-visible:text-accent-foreground focus-visible:outline-none data-[highlighted]:bg-accent
                    data-[highlighted]:text-accent-foreground aria-[selected=true]:bg-accent
                    aria-[selected=true]:text-accent-foreground data-[selected]:bg-accent
                    data-[selected]:text-accent-foreground aria-[disabled=true]:opacity-50
                    aria-[disabled=true]:pointer-events-none data-[disabled]:opacity-50
                    data-[disabled]:pointer-events-none
                  "
                  item={item}
                  key={item.value}
                />
              ))
            )
          }
        </Combobox.Content>
      </Combobox.Root>
    </div>
  );
}

API

Combobox.Root

מאפייןטיפוסברירת מחדלתיאור
size"sm" | "md" | "lg""md"גובה הפקד וגודל הטקסט, בהתאמה ל-Button (32, 40 או 48 px); נכתבים ל-data-size, עם md כשלא הוגדר ערך.
childrenReactNode—חלקי Combobox: פקד, תגיות, קלט, כפתור פתיחה ותוכן.
items*ComboboxItemData[]—רשימת האפשרויות המלאה שהקלט מסנן.
defaultInputValuestring—הטקסט ההתחלתי בקלט כשהערך שלו אינו מבוקר.
defaultOpenboolean—קובע אם הרשימה מתחילה פתוחה כאשר מצב הפתיחה אינו מבוקר.
defaultValuestring | readonly string[]—הערך או הערכים הנבחרים בהצגה הראשונה כאשר הבחירה אינה מבוקרת.
multiplebooleanfalseמאפשר לבחור ולהסיר ערכים מרובים כתגיות.
allowCustomValuebooleanfalseמאפשר לבחור כערך טקסט שהוקלד ואינו נמצא ב-items.
addOnPastebooleantrueמוסיף ערכים מרובים המופרדים בתווים מתוך טקסט שהודבק במצב בחירה מרובה.
delimitersreadonly string[][","]תווים המאשרים או מפצלים ערכים מוקלדים במצב בחירה מרובה.
maxValuesnumber—המספר המרבי של ערכים שניתן לבחור במצב בחירה מרובה.
validateValue(value: string) => boolean—דוחה ערך מותאם אישית כשהיא מחזירה false.
filterFn(items: ComboboxItemData[], inputValue: string) => ComboboxItemData[]—מחליפה את המסנן המובנה שמצמצם את הפריטים בזמן ההקלדה.
inputValuestring—הטקסט בקלט, ההופך את הערך המוקלד למבוקר.
onInputValueChange(value: string) => void—נקראת עם הטקסט החדש בכל שינוי בקלט.
onOpenChange(open: boolean, details: OpenChangeDetails) => void—נקראת כשהרשימה נפתחת או נסגרת, עם סיבת השינוי.
onValueChange((value: string) => void) | ((value: string[]) => void)—נקראת עם הערך הנבחר או עם מערך הערכים הנבחרים המלא.
openboolean—קובע אם הרשימה פתוחה, והופך את מצב הפתיחה למבוקר.
valuestring | readonly string[]—הערך או הערכים הנבחרים, ההופכים את הבחירה למבוקרת.

Combobox.Control

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

Combobox.Tags

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

Combobox.Input

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

Combobox.Trigger

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

Combobox.Content

מאפייןטיפוסברירת מחדלתיאור
align"start" | "center" | "end"—יישור הרשימה ביחס לקלט לאורך הצד שנבחר.
children*(filteredItems: ComboboxItemData[]) => ReactNode—פונקציית תצוגה הנקראת עם הפריטים שנותרו לאחר הסינון.
classNamestring—המחלקה המוחלת על החלונית הקופצת של הרשימה.
side"top" | "right" | "bottom" | "left"—הצד של הקלט שבו ממוקמת הרשימה.

Combobox.Item

מאפייןטיפוסברירת מחדלתיאור
classNamestring—המחלקה המוחלת על אלמנט האפשרות.
item*ComboboxItemData—הפריט שאפשרות זו מציגה, בדרך כלל מתוך פונקציית התוכן.

Combobox.Empty

מאפייןטיפוסברירת מחדלתיאור
childrenReactNode—התוכן המוצג כשהסינון לא משאיר פריטים.
classNamestring—המחלקה המוחלת על אלמנט המצב הריק.

ComboboxItemData

מאפייןטיפוסברירת מחדלתיאור
disabledboolean—מונע בחירה בפריט זה.
label*string—הטקסט המוצג לפריט ומשמש להתאמה במסנן.
value*string—הערך המדווח כשהפריט נבחר.

מקלדת

מקשפעולה
↓ / ↑פתיחה ומעבר בין אפשרויות
Enterבחירה
Escapeסגירה
a–zהקלדה לסינון אפשרויות

נגישות

תיבת בחירה משולבת של ARIA עם listbox. האפשרות הפעילה מוכרזת באמצעות aria-activedescendant. התוכן חושף מצב פתוח או סגור; האפשרויות חושפות מצב מודגש, מושבת, ובמצב מרובה גם נבחר. השתמשו ב-Empty להסבר כשאין אפשרויות תואמות. הנפשת ההופעה של החלונית הקופצת מושבתת כאשר יש העדפה לתנועה מופחתת.

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

data-statedata-highlighteddata-disableddata-selected
דף זה תורגם אוטומטית וטרם נבדק.