Combobox
תיבת בחירה משולבת של ARIA עם ערך יחיד או ערכים מרובים וסינון.
- דפדפנים
- Chrome 114+Firefox 125+Safari 17+
- שכבה
- 2
- גודל חבילה דחוסה
- 5.53 kB
- מגבלת פריסה כפויה
- 0 ms
- מגבלת Axe
- 0
- תואם SSR
- כן
- חוזה מפורסם
- סביבת בדיקות
מגרש ניסויים
"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 כשלא הוגדר ערך. |
children | ReactNode | — | חלקי Combobox: פקד, תגיות, קלט, כפתור פתיחה ותוכן. |
items* | ComboboxItemData[] | — | רשימת האפשרויות המלאה שהקלט מסנן. |
defaultInputValue | string | — | הטקסט ההתחלתי בקלט כשהערך שלו אינו מבוקר. |
defaultOpen | boolean | — | קובע אם הרשימה מתחילה פתוחה כאשר מצב הפתיחה אינו מבוקר. |
defaultValue | string | readonly string[] | — | הערך או הערכים הנבחרים בהצגה הראשונה כאשר הבחירה אינה מבוקרת. |
multiple | boolean | false | מאפשר לבחור ולהסיר ערכים מרובים כתגיות. |
allowCustomValue | boolean | false | מאפשר לבחור כערך טקסט שהוקלד ואינו נמצא ב-items. |
addOnPaste | boolean | true | מוסיף ערכים מרובים המופרדים בתווים מתוך טקסט שהודבק במצב בחירה מרובה. |
delimiters | readonly string[] | [","] | תווים המאשרים או מפצלים ערכים מוקלדים במצב בחירה מרובה. |
maxValues | number | — | המספר המרבי של ערכים שניתן לבחור במצב בחירה מרובה. |
validateValue | (value: string) => boolean | — | דוחה ערך מותאם אישית כשהיא מחזירה false. |
filterFn | (items: ComboboxItemData[], inputValue: string) => ComboboxItemData[] | — | מחליפה את המסנן המובנה שמצמצם את הפריטים בזמן ההקלדה. |
inputValue | string | — | הטקסט בקלט, ההופך את הערך המוקלד למבוקר. |
onInputValueChange | (value: string) => void | — | נקראת עם הטקסט החדש בכל שינוי בקלט. |
onOpenChange | (open: boolean, details: OpenChangeDetails) => void | — | נקראת כשהרשימה נפתחת או נסגרת, עם סיבת השינוי. |
onValueChange | ((value: string) => void) | ((value: string[]) => void) | — | נקראת עם הערך הנבחר או עם מערך הערכים הנבחרים המלא. |
open | boolean | — | קובע אם הרשימה פתוחה, והופך את מצב הפתיחה למבוקר. |
value | string | readonly string[] | — | הערך או הערכים הנבחרים, ההופכים את הבחירה למבוקרת. |
Combobox.Control
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
children | ReactNode | — | התגיות והקלט המוצגים בתוך הפקד החזותי המשותף. |
className | string | — | המחלקה המוחלת על אלמנט הפקד. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Combobox.Tags
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
children | ReactNode | — | תוכן המצורף אחרי התגיות הניתנות להסרה שנוצרות. |
className | string | — | המחלקה המוחלת על מכל התגיות שנוצר. |
slotProps | ComboboxTagsSlotProps | — | מאפיינים מובנים לתגיות ולכפתורי ההסרה שנוצרים, הממוענים לפי הערך הנבחר. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Combobox.Input
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
className | string | — | המחלקה המוחלת על קלט הטקסט. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Combobox.Trigger
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
children | ReactNode | — | תוכן הכפתור שפותח את הרשימה, למשל אייקון. |
className | string | — | המחלקה המוחלת על כפתור הפתיחה. |
render | ReactElement | ((props, state) => ReactElement) | — | החלפת האלמנט המוצג תוך שימור ההתנהגות וקישורי הנגישות. |
Combobox.Content
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
align | "start" | "center" | "end" | — | יישור הרשימה ביחס לקלט לאורך הצד שנבחר. |
children* | (filteredItems: ComboboxItemData[]) => ReactNode | — | פונקציית תצוגה הנקראת עם הפריטים שנותרו לאחר הסינון. |
className | string | — | המחלקה המוחלת על החלונית הקופצת של הרשימה. |
side | "top" | "right" | "bottom" | "left" | — | הצד של הקלט שבו ממוקמת הרשימה. |
Combobox.Item
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
className | string | — | המחלקה המוחלת על אלמנט האפשרות. |
item* | ComboboxItemData | — | הפריט שאפשרות זו מציגה, בדרך כלל מתוך פונקציית התוכן. |
Combobox.Empty
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
children | ReactNode | — | התוכן המוצג כשהסינון לא משאיר פריטים. |
className | string | — | המחלקה המוחלת על אלמנט המצב הריק. |
ComboboxItemData
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
disabled | boolean | — | מונע בחירה בפריט זה. |
label* | string | — | הטקסט המוצג לפריט ומשמש להתאמה במסנן. |
value* | string | — | הערך המדווח כשהפריט נבחר. |
מקלדת
| מקש | פעולה |
|---|---|
| ↓ / ↑ | פתיחה ומעבר בין אפשרויות |
| Enter | בחירה |
| Escape | סגירה |
| a–z | הקלדה לסינון אפשרויות |
נגישות
תיבת בחירה משולבת של ARIA עם listbox. האפשרות הפעילה מוכרזת באמצעות aria-activedescendant. התוכן חושף מצב פתוח או סגור; האפשרויות חושפות מצב מודגש, מושבת, ובמצב מרובה גם נבחר. השתמשו ב-Empty להסבר כשאין אפשרויות תואמות. הנפשת ההופעה של החלונית הקופצת מושבתת כאשר יש העדפה לתנועה מופחתת.
נקודות חיבור לעיצוב
data-statedata-highlighteddata-disableddata-selected