Repository: dequelabs/cauldron
Pull request #1816: feat(react, styles): combobox component multiselect
Author: login: chornonoh-vova
Created: 2025-02-28T09:34:22Z | Merged: 2025-03-13T15:58:06Z

Fixes: #1103 

Adds multi select to the combobox. Mode of the combobox (single or multi select) is controlled by `multiselect` prop. When `multiselect` prop if `false` or `undefined`, `value` and `defaultValue` must be `string`, when the `multiselect` prop is `true` `value` and `defaultValue` must be `string[]`. Additionally, in the multi select mode, `inputValue` and `defaultInputValue` props of type `string` are added to allow control of the text input.


Diff:
diff --git a/docs/pages/components/Combobox.mdx b/docs/pages/components/Combobox.mdx
index 9a9e892df..947dc32b2 100644
--- a/docs/pages/components/Combobox.mdx
+++ b/docs/pages/components/Combobox.mdx
@@ -4,19 +4,23 @@ description: An input component to display a list of suggested options.
 source: https://github.com/dequelabs/cauldron/tree/develop/packages/react/src/components/Combobox/Combobox.tsx
 ---
 
-import { useState } from 'react'
-import { Combobox, ComboboxOption, ComboboxGroup, FieldWrap } from '@deque/cauldron-react'
-
-```js
-import { 
+import { useState } from 'react';
+import {
   Combobox,
   ComboboxOption,
-  ComboboxGroup
-} from '@deque/cauldron-react'
+  ComboboxGroup,
+  FieldWrap
+} from '@deque/cauldron-react';
+
+```js
+import { Combobox, ComboboxOption, ComboboxGroup } from '@deque/cauldron-react';
 ```
 
 <Note>
-  Use [FieldWrap](./FieldWrap) to wrap all input components. The `Combobox` component has been designed specifically to only be used when rendered as a child of this component, and may not be completely accessible if rendered outside of `FieldWrap`.
+  Use [FieldWrap](./FieldWrap) to wrap all input components. The `Combobox`
+  component has been designed specifically to only be used when rendered as a
+  child of this component, and may not be completely accessible if rendered
+  outside of `FieldWrap`.
 </Note>
 
 ## Examples
@@ -33,12 +37,12 @@ function ComboboxWithOptionsPropExample() {
     { label: 'Yellow', value: 'Yellow' },
     { label: 'Green', value: 'Green' },
     { label: 'Blue', value: 'Blue' }
-  ]
+  ];
   return (
     <FieldWrap>
       <Combobox label="Favorite Color" options={options} />
     </FieldWrap>
-  )
+  );
 }
 ```
 
@@ -66,32 +70,32 @@ Combobox options can optionally include a description, which can be set via a pr
 ```jsx example
 <FieldWrap>
   <Combobox label="Select a Drink">
-    <ComboboxOption 
+    <ComboboxOption
       description="A monster of a barleywine, with a footprint of caramel and toffee."
       value="Bigfoot's Barleywine"
     >
       Bigfoot's Barleywine
     </ComboboxOption>
-    <ComboboxOption 
+    <ComboboxOption
       description="A hoppy explosion of citrus and pine, a true hopocalypse."
       value="Hopocalypse IPA"
     >
       Hopocalypse IPA
     </ComboboxOption>
-    <ComboboxOption 
+    <ComboboxOption
       description="A pot of liquid gold at the end of the rainbow."
       value="Leprechaun's Gold Lager"
     >
       Leprechaun's Gold Lager
     </ComboboxOption>
 
-    <ComboboxOption 
+    <ComboboxOption
       description="Delicate, effervescent, with a hint of mythical magic in every sip."
       value="Unicorn Tears Saison"
     >
       Unicorn Tears Saison
     </ComboboxOption>
-    <ComboboxOption 
+    <ComboboxOption
       description="Revive your taste buds with this undead burst of citrusy hops."
       value="Zombie Zest Pale Ale"
     >
@@ -117,7 +121,7 @@ Combobox options can optionally include a description, which can be set via a pr
 
 ```jsx example
 <FieldWrap>
-  <Combobox 
+  <Combobox
     label="Does this combobox have an error?"
     error="You must select an option."
     required
@@ -181,7 +185,11 @@ To set the initial value for uncontrolled Comboboxes, use `defaultValue`.
 Comboboxes can also be controlled, by setting the `value` prop and updating the value from event callbacks.
 
 <Note>
-  `onChange` and `onSelectionChange` have different behaviors, and special consideration needs to be made when `Combobox` is controlled. `onChange` gets called when the value of the input field changes from user input or selections while `onSelectionChange` is only called when a selection changes from the listbox.
+  `onChange` and `onSelectionChange` have different behaviors, and special
+  consideration needs to be made when `Combobox` is controlled. `onChange` gets
+  called when the value of the input field changes from user input or selections
+  while `onSelectionChange` is only called when a selection changes from the
+  listbox.
 </Note>
 
 ```jsx example
@@ -195,9 +203,9 @@ function ComboboxControlledExample() {
   };
   return (
     <FieldWrap>
-      <Combobox 
-        label="Fruit" 
-        value={value} 
+      <Combobox
+        label="Fruit"
+        value={value}
         onChange={handleChange}
         onSelectionChange={handleSelectionChange}
       >
@@ -235,12 +243,24 @@ Alternatively, a custom `formValue` prop can be set for each combobox option to
 ```jsx example
 <FieldWrap>
   <Combobox name="fruit" label="Fruit" defaultValue="Peach">
-    <ComboboxOption value="Apple" formValue="1">🍎 Apple</ComboboxOption>
-    <ComboboxOption value="Banana" formValue="2">🍌 Banana</ComboboxOption>
-    <ComboboxOption value="Cucumber" formValue="3">🥒 Cucumber</ComboboxOption>
-    <ComboboxOption value="Orange" formValue="4">🍊 Orange</ComboboxOption>
-    <ComboboxOption value="Peach" formValue="5">🍑 Peach</ComboboxOption>
-    <ComboboxOption value="Pear" formValue="6">🍐 Pear</ComboboxOption>
+    <ComboboxOption value="Apple" formValue="1">
+      🍎 Apple
+    </ComboboxOption>
+    <ComboboxOption value="Banana" formValue="2">
+      🍌 Banana
+    </ComboboxOption>
+    <ComboboxOption value="Cucumber" formValue="3">
+      🥒 Cucumber
+    </ComboboxOption>
+    <ComboboxOption value="Orange" formValue="4">
+      🍊 Orange
+    </ComboboxOption>
+    <ComboboxOption value="Peach" formValue="5">
+      🍑 Peach
+    </ComboboxOption>
+    <ComboboxOption value="Pear" formValue="6">
+      🍐 Pear
+    </ComboboxOption>
   </Combobox>
 </FieldWrap>
 ```
@@ -277,118 +297,254 @@ When autocomplete is set to "automatic" the listbox will provide a filtered list
 </FieldWrap>
 ```
 
+## Multiselect
+
+Combobox can also support multiple selection of options.
+
+### Multiselect Uncontrolled
+
+```jsx example
+<FieldWrap>
+  <Combobox label="Multiselect Combobox" name="multiselect" multiselect>
+    <ComboboxOption value="Apple">🍎 Apple</ComboboxOption>
+    <ComboboxOption value="Banana">🍌 Banana</ComboboxOption>
+    <ComboboxOption value="Cucumber">🥒 Cucumber</ComboboxOption>
+    <ComboboxOption value="Orange">🍊 Orange</ComboboxOption>
+    <ComboboxOption value="Peach">🍑 Peach</ComboboxOption>
+    <ComboboxOption value="Pear">🍐 Pear</ComboboxOption>
+  </Combobox>
+</FieldWrap>
+```
+
+### Multiselect Disabled
+
+```jsx example
+<FieldWrap>
+  <Combobox
+    label="Multiselect Disabled Combobox"
+    name="multiselect-disabled"
+    multiselect
+    disabled
+    defaultValue={['Apple', 'Banana']}
+  >
+    <ComboboxOption value="Apple">🍎 Apple</ComboboxOption>
+    <ComboboxOption value="Banana">🍌 Banana</ComboboxOption>
+    <ComboboxOption value="Cucumber">🥒 Cucumber</ComboboxOption>
+    <ComboboxOption value="Orange">🍊 Orange</ComboboxOption>
+    <ComboboxOption value="Peach">🍑 Peach</ComboboxOption>
+    <ComboboxOption value="Pear">🍐 Pear</ComboboxOption>
+  </Combobox>
+</FieldWrap>
+```
+
+### Multiselect Controlled
+
+Multiselect Combobox can be controlled by passing `value` and `inputValue` props and updating them with `onSelectionChange` and `onChange`
+
+<Note>
+  Multiselect Combobox components will pass in array values for the selected
+  options in `onSelectionChange` and expect an array of values for `value` and
+  `defaultValue` props. Also, in the mutliselect state combobox has an
+  additional props `inputValue` and `defaultInputValue` to control text input
+  state.
+</Note>
+
+```jsx example
+function MultiselectComboboxControlledExample() {
+  const [inputValue, setInputValue] = useState('');
+  const [value, setValue] = useState(['Peach']);
+  const handleChange = ({ target }) => {
+    setInputValue(target.value);
+  };
+  const handleSelectionChange = ({ value }) => {
+    setValue(value);
+  };
+  return (
+    <FieldWrap>
+      <Combobox
+        label="Multi Fruit"
+        value={value}
+        inputValue={inputValue}
+        onChange={handleChange}
+        onSelectionChange={handleSelectionChange}
+        multiselect
+      >
+        <ComboboxOption value="Apple">🍎 Apple</ComboboxOption>
+        <ComboboxOption value="Banana">🍌 Banana</ComboboxOption>
+        <ComboboxOption value="Cucumber">🥒 Cucumber</ComboboxOption>
+        <ComboboxOption value="Orange">🍊 Orange</ComboboxOption>
+        <ComboboxOption value="Peach">🍑 Peach</ComboboxOption>
+        <ComboboxOption value="Pear">🍐 Pear</ComboboxOption>
+      </Combobox>
+    </FieldWrap>
+  );
+}
+```
+
 ## Props
 
 ### Combobox
 
-<ComponentProps className={true} refType="HTMLDivElement" props={[
-  {
-    name: 'label',
-    type: ['string', 'number', 'ReactElement', 'ReactFragment', 'ReactPortal'],
-    required: true,
-    description: 'Label for the Combobox input field.'
-  },
-  {
-    name: 'options',
-    type: 'object',
-    description: 'Array of objects mapping to Combobox options. Alternatively, "ComboboxOption" can be used as children instead of this prop.'
-  },
-  {
-    name: 'value',
-    type: 'string',
-    description: 'Initial value to be applied to the combobox. Optionally used for "uncontrolled" comboboxes.'
-  },
-  {
-    name: 'defaultValue',
-    type: 'string',
-    description: 'Value to be applied to the combobox. Optionally used for "controlled" comboboxes.'
-  },
-  {
-    name: 'autocomplete',
-    type: ['none', 'manual', 'automatic'],
-    defaultValue: 'manual',
-    description: 'Sets the autocompletion type of the Combobox listbox.'
-  },
-  {
-    name: 'onChange',
-    type: 'function',
-    description: 'Callback function that gets invoked when the combobox input value is changed.'
-  },
-  {
-    name: 'onSelectionChange',
-    type: 'function',
-    description: 'Callback function that gets invoked when the selected value is changed.'
-  },
-  {
-    name: 'onActiveChange',
-    type: 'function',
-    description: 'Callback function that gets invoked when the current active option was changed.'
-  },
-  { 
-    name: 'error',
-    type: 'React.ReactNode',
-    description: 'Error message for the field, when needed.'
-  },
-  {
-    name: 'requiredText',
-    type: 'string',
-    defaultValue: 'Required',
-    description: 'Custom "required" text. Useful for localization.'
-  },
-  {
-    name: 'renderNoResults',
-    type: ['() => React.JSX.Element', 'React.Element'],
-    description: 'Render prop to customize the output when there are no matching results. Useful for localization.'
-  },
-  {
-    name: 'portal',
-    type: ['React.Ref<HTMLElement>', 'HTMLElement'],
-    description: 'Alternative placement of combobox listbox. When not set, the listbox will be absolutely positioned inline.'
-  },
-  {
-    name: 'inputRef',
-    type: 'React.Ref<HTMLInputElement>',
-    description: 'Ref for the input.'
-  },
-]} />
+<ComponentProps
+  className={true}
+  refType="HTMLDivElement"
+  props={[
+    {
+      name: 'label',
+      type: [
+        'string',
+        'number',
+        'ReactElement',
+        'ReactFragment',
+        'ReactPortal'
+      ],
+      required: true,
+      description: 'Label for the Combobox input field.'
+    },
+    {
+      name: 'options',
+      type: 'object',
+      description:
+        'Array of objects mapping to Combobox options. Alternatively, "ComboboxOption" can be used as children instead of this prop.'
+    },
+    {
+      name: 'value',
+      type: ['string', 'string[]'],
+      description:
+        'Initial value to be applied to the combobox. Optionally used for "uncontrolled" comboboxes.'
+    },
+    {
+      name: 'defaultValue',
+      type: ['string', 'string[]'],
+      description:
+        'Value to be applied to the combobox. Optionally used for "controlled" comboboxes.'
+    },
+    {
+      name: 'autocomplete',
+      type: ['none', 'manual', 'automatic'],
+      defaultValue: 'manual',
+      description: 'Sets the autocompletion type of the Combobox listbox.'
+    },
+    {
+      name: 'multiselect',
+      type: 'boolean',
+      description: 'Sets the Combobox to multiselect'
+    },
+    {
+      name: 'onChange',
+      type: 'function',
+      description:
+        'Callback function that gets invoked when the combobox input value is changed.'
+    },
+    {
+      name: 'onSelectionChange',
+      type: 'function',
+      description:
+        'Callback function that gets invoked when the selected value is changed.'
+    },
+    {
+      name: 'onActiveChange',
+      type: 'function',
+      description:
+        'Callback function that gets invoked when the current active option was changed.'
+    },
+    {
+      name: 'error',
+      type: 'React.ReactNode',
+      description: 'Error message for the field, when needed.'
+    },
+    {
+      name: 'requiredText',
+      type: 'string',
+      defaultValue: 'Required',
+      description: 'Custom "required" text. Useful for localization.'
+    },
+    {
+      name: 'renderNoResults',
+      type: ['() => React.JSX.Element', 'React.Element'],
+      description:
+        'Render prop to customize the output when there are no matching results. Useful for localization.'
+    },
+    {
+      name: 'portal',
+      type: ['React.Ref<HTMLElement>', 'HTMLElement'],
+      description:
+        'Alternative placement of combobox listbox. When not set, the listbox will be absolutely positioned inline.'
+    },
+    {
+      name: 'inputRef',
+      type: 'React.Ref<HTMLInputElement>',
+      description: 'Ref for the input.'
+    }
+  ]}
+/>
 
 ### ComboboxOption
 
-<ComponentProps className={true} refType="HTMLLIElement" props={[
-  {
-    name: 'value',
-    type: 'string',
-    description: 'Value to be applied to the combobox option. When omitted, value will default to the text content of the option.'
-  },
-  {
-    name: 'formValue',
-    type: 'string',
-    description: 'An optional custom value to be used for form submissions when the "name" prop is provided for Combobox.'
-  },
-  {
-    name: 'disabled',
-    type: 'boolean',
-    description: 'When set, sets the combobox option as "aria-disabled="true" and removes the element from key navigation.'
-  },
-  {
-    name: 'description',
-    type: ['string', 'number', 'ReactElement', 'ReactFragment', 'ReactPortal'],
-    description: 'Additional text to display for the ComboboxOption.'
-  }
-]} />
+<ComponentProps
+  className={true}
+  refType="HTMLLIElement"
+  props={[
+    {
+      name: 'value',
+      type: 'string',
+      description:
+        'Value to be applied to the combobox option. When omitted, value will default to the text content of the option.'
+    },
+    {
+      name: 'formValue',
+      type: 'string',
+      description:
+        'An optional custom value to be used for form submissions when the "name" prop is provided for Combobox.'
+    },
+    {
+      name: 'disabled',
+      type: 'boolean',
+      description:
+        'When set, sets the combobox option as "aria-disabled="true" and removes the element from key navigation.'
+    },
+    {
+      name: 'description',
+      type: [
+        'string',
+        'number',
+        'ReactElement',
+        'ReactFragment',
+        'ReactPortal'
+      ],
+      description: 'Additional text to display for the ComboboxOption.'
+    },
+    {
+      name: 'removeOptionLabel',
+      type: 'string',
+      description:
+        'An optional custom value to be user for pills aria-label in multiselect mode'
+    }
+  ]}
+/>
 
 ### ComboboxGroup
 
-<ComponentProps className={true} refType="HTMLUListElement" props={[
-  {
-    name: 'label',
-    required: true,
-    type: ['string', 'number', 'ReactElement', 'ReactFragment', 'ReactPortal'],
-    description: 'Label for the group of options.'
-  }
-]} />
+<ComponentProps
+  className={true}
+  refType="HTMLUListElement"
+  props={[
+    {
+      name: 'label',
+      required: true,
+      type: [
+        'string',
+        'number',
+        'ReactElement',
+        'ReactFragment',
+        'ReactPortal'
+      ],
+      description: 'Label for the group of options.'
+    }
+  ]}
+/>
 
 ## Related Components
 
 - [Listbox](./Listbox)
-- [FieldWrap](./FieldWrap)
\ No newline at end of file
+- [FieldWrap](./FieldWrap)
diff --git a/e2e/screenshots/combobox-multiselect-.png b/e2e/screenshots/combobox-multiselect-.png
new file mode 100644
index 000000000..cac6d5f38
Binary files /dev/null and b/e2e/screenshots/combobox-multiselect-.png differ
diff --git a/e2e/screenshots/combobox.png b/e2e/screenshots/combobox.png
index ffc84df2b..2a976ff1a 100644
Binary files a/e2e/screenshots/combobox.png and b/e2e/screenshots/combobox.png differ
diff --git a/e2e/screenshots/dark--combobox-multiselect-.png b/e2e/screenshots/dark--combobox-multiselect-.png
new file mode 100644
index 000000000..e5191d00a
Binary files /dev/null and b/e2e/screenshots/dark--combobox-multiselect-.png differ
diff --git a/packages/react/src/components/Combobox/Combobox.test.tsx b/packages/react/src/components/Combobox/Combobox.test.tsx
index 4020dce78..05a1a4bdb 100644
--- a/packages/react/src/components/Combobox/Combobox.test.tsx
+++ b/packages/react/src/components/Combobox/Combobox.test.tsx
@@ -55,6 +55,18 @@ const assertOptionIsSelected = (index: number) => {
   );
 };
 
+const assertOptionsAreSelected = (...indexes: number[]) => {
+  const options = screen.queryAllByRole('option');
+  for (let i = 0; i < options.length; ++i) {
+    const selected = JSON.parse(options[i]?.getAttribute('aria-selected') + '');
+    if (indexes.includes(i)) {
+      expect(selected).toBeTruthy();
+    } else {
+      expect(selected).toBeFalsy();
+    }
+  }
+};
+
 test('should render combobox with options', () => {
   const options = [
     { value: 'Apple', label: 'Apple' },
@@ -396,6 +408,62 @@ test('should close combobox listbox when selecting option via click', async () =
   assertListboxIsOpen(false);
 });
 
+test('should select multiple combobox options via clicks', () => {
+  render(
+    <Combobox label="label" multiselect>
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+
+  const combobox = screen.getByRole('combobox');
+
+  fireEvent.focus(combobox);
+  fireEvent.click(screen.getAllByRole('option')[0]);
+  fireEvent.blur(combobox);
+
+  fireEvent.focus(combobox);
+  fireEvent.click(screen.getAllByRole('option')[1]);
+  fireEvent.blur(combobox);
+
+  const pills = screen.getAllByRole('button');
+  expect(pills).toHaveLength(2);
+  expect(pills[0]).toHaveAccessibleName('remove Apple');
+  expect(pills[1]).toHaveAccessibleName('remove Banana');
+});
+
+test('should select multiple combobox options via clicks, custom remove value aria label', () => {
+  render(
+    <Combobox label="label" multiselect>
+      <ComboboxOption removeOptionLabel="Remove and forget Apple">
+        Apple
+      </ComboboxOption>
+      <ComboboxOption removeOptionLabel="Remove and forget Banana">
+        Banana
+      </ComboboxOption>
+      <ComboboxOption removeOptionLabel="Remove and forget Cantaloupe">
+        Cantaloupe
+      </ComboboxOption>
+    </Combobox>
+  );
+
+  const combobox = screen.getByRole('combobox');
+
+  fireEvent.focus(combobox);
+  fireEvent.click(screen.getAllByRole('option')[0]);
+  fireEvent.blur(combobox);
+
+  fireEvent.focus(combobox);
+  fireEvent.click(screen.getAllByRole('option')[1]);
+  fireEvent.blur(combobox);
+
+  const pills = screen.getAllByRole('button');
+  expect(pills).toHaveLength(2);
+  expect(pills[0]).toHaveAccessibleName('Remove and forget Apple');
+  expect(pills[1]).toHaveAccessibleName('Remove and forget Banana');
+});
+
 test('should prevent default on combobox listbox option via mousedown', () => {
   const preventDefault = spy();
   render(
@@ -424,6 +492,33 @@ test('should prevent default on combobox listbox option via mousedown', () => {
 });
 
 test('should close combobox listbox when selecting option via keypress', () => {
+  render(
+    <Combobox label="label" multiselect>
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+
+  const combobox = screen.getByRole('combobox');
+
+  fireEvent.focus(combobox);
+  fireEvent.keyDown(combobox, { key: 'ArrowDown' });
+  fireEvent.keyDown(combobox, { key: 'Enter' });
+  fireEvent.blur(combobox);
+
+  fireEvent.focus(combobox);
+  fireEvent.keyDown(combobox, { key: 'ArrowDown' });
+  fireEvent.keyDown(combobox, { key: 'Enter' });
+  fireEvent.blur(combobox);
+
+  const pills = screen.getAllByRole('button');
+  expect(pills).toHaveLength(2);
+  expect(pills[0]).toHaveAccessibleName('remove Apple');
+  expect(pills[1]).toHaveAccessibleName('remove Banana');
+});
+
+test('should select multiple combobox options via keypresses', () => {
   render(
     <Combobox label="label">
       <ComboboxOption>Apple</ComboboxOption>
@@ -456,6 +551,27 @@ test('should set combobox value when selecting option via mousedown when passed
   expect(screen.getByRole('combobox')).toHaveDisplayValue('apple');
 });
 
+test('should not set multiselect combobox value when selecting second option via mousedown when passed children', () => {
+  render(
+    <Combobox label="label" multiselect>
+      <ComboboxOption value="apple">Apple</ComboboxOption>
+      <ComboboxOption value="banana">Banana</ComboboxOption>
+      <ComboboxOption value="cantaloupe">Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+
+  const combobox = screen.getByRole('combobox');
+
+  fireEvent.focus(combobox);
+  fireEvent.click(screen.getAllByRole('option')[0]);
+  expect(screen.getByRole('combobox')).toHaveDisplayValue('');
+  fireEvent.blur(combobox);
+
+  fireEvent.focus(combobox);
+  fireEvent.click(screen.getAllByRole('option')[1]);
+  expect(screen.getByRole('combobox')).toHaveDisplayValue('');
+});
+
 test('should set combobox value when selecting option via keypress when passed children', () => {
   render(
     <Combobox label="label">
@@ -472,6 +588,29 @@ test('should set combobox value when selecting option via keypress when passed c
   expect(screen.getByRole('combobox')).toHaveDisplayValue('apple');
 });
 
+test('should set multiselect combobox value when selecting second option via keypress when passed children', () => {
+  render(
+    <Combobox label="label">
+      <ComboboxOption value="apple">Apple</ComboboxOption>
+      <ComboboxOption value="banana">Banana</ComboboxOption>
+      <ComboboxOption value="cantaloupe">Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+
+  const combobox = screen.getByRole('combobox');
+
+  fireEvent.focus(combobox);
+  fireEvent.keyDown(combobox, { key: 'ArrowDown' });
+  fireEvent.keyDown(combobox, { key: 'Enter' });
+  expect(screen.getByRole('combobox')).toHaveDisplayValue('apple');
+  fireEvent.blur(combobox);
+
+  fireEvent.focus(combobox);
+  fireEvent.keyDown(combobox, { key: 'ArrowDown' });
+  fireEvent.keyDown(combobox, { key: 'Enter' });
+  expect(screen.getByRole('combobox')).toHaveDisplayValue('banana');
+});
+
 test('should set combobox value when selecting option via mousedown when passed options', () => {
   const options = [
     { value: 'apple', label: 'Apple' },
@@ -486,6 +625,26 @@ test('should set combobox value when selecting option via mousedown when passed
   expect(screen.getByRole('combobox')).toHaveDisplayValue('apple');
 });
 
+test('should not set multiselect combobox value when selecting second option via mousedown when passed options', () => {
+  const options = [
+    { value: 'apple', label: 'Apple' },
+    { value: 'banana', label: 'Banana' },
+    { value: 'cantaloupe', label: 'Cantaloupe' }
+  ];
+  render(<Combobox label="label" multiselect options={options} />);
+
+  const combobox = screen.getByRole('combobox');
+
+  fireEvent.focus(combobox);
+  fireEvent.click(screen.getAllByRole('option')[0]);
+  expect(screen.getByRole('combobox')).toHaveDisplayValue('');
+  fireEvent.blur(combobox);
+
+  fireEvent.focus(combobox);
+  fireEvent.click(screen.getAllByRole('option')[1]);
+  expect(screen.getByRole('combobox')).toHaveDisplayValue('');
+});
+
 test('should set combobox value when selecting option via keypress when passed options', () => {
   const options = [
     { value: 'apple', label: 'Apple' },
@@ -501,6 +660,28 @@ test('should set combobox value when selecting option via keypress when passed o
   expect(screen.getByRole('combobox')).toHaveDisplayValue('apple');
 });
 
+test('should not set multiselect combobox value when selecting  second option via keypress when passed options', () => {
+  const options = [
+    { value: 'apple', label: 'Apple' },
+    { value: 'banana', label: 'Banana' },
+    { value: 'cantaloupe', label: 'Cantaloupe' }
+  ];
+  render(<Combobox label="label" multiselect options={options} />);
+
+  const combobox = screen.getByRole('combobox');
+
+  fireEvent.focus(combobox);
+  fireEvent.keyDown(combobox, { key: 'ArrowDown' });
+  fireEvent.keyDown(combobox, { key: 'Enter' });
+  expect(screen.getByRole('combobox')).toHaveDisplayValue('');
+  fireEvent.blur(combobox);
+
+  fireEvent.focus(combobox);
+  fireEvent.keyDown(combobox, { key: 'ArrowDown' });
+  fireEvent.keyDown(combobox, { key: 'Enter' });
+  expect(screen.getByRole('combobox')).toHaveDisplayValue('');
+});
+
 test('should prevent default with enter keypress and open listbox', () => {
   const preventDefault = spy();
   render(
@@ -739,6 +920,87 @@ test('should handle selection with "click" event', () => {
   assertOptionIsSelected(0);
 });
 
+test('should handle multiple selections with "click" event', () => {
+  const onSelectionChange = spy();
+  render(
+    <Combobox label="label" multiselect onSelectionChange={onSelectionChange}>
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+  const combobox = screen.getByRole('combobox');
+
+  fireEvent.focus(combobox);
+  assertListboxIsOpen(true);
+  expect(onSelectionChange.notCalled).toBeTruthy();
+  fireEvent.click(screen.getAllByRole('option')[1]);
+  expect(onSelectionChange.callCount).toEqual(1);
+  expect(onSelectionChange.firstCall.firstArg.value).toEqual(['Banana']);
+  fireEvent.click(combobox);
+  fireEvent.click(screen.getAllByRole('option')[2]);
+  expect(onSelectionChange.secondCall.firstArg.value).toEqual([
+    'Banana',
+    'Cantaloupe'
+  ]);
+});
+
+test('should remove selected option upon clicking on pill', () => {
+  const onSelectionChange = spy();
+  render(
+    <Combobox label="label" multiselect onSelectionChange={onSelectionChange}>
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+  const combobox = screen.getByRole('combobox');
+
+  fireEvent.focus(combobox);
+  assertListboxIsOpen(true);
+  expect(onSelectionChange.notCalled).toBeTruthy();
+  fireEvent.click(screen.getAllByRole('option')[1]);
+  expect(onSelectionChange.callCount).toEqual(1);
+  expect(onSelectionChange.firstCall.firstArg.value).toEqual(['Banana']);
+  fireEvent.click(combobox);
+  fireEvent.click(screen.getAllByRole('option')[2]);
+  expect(onSelectionChange.secondCall.firstArg.value).toEqual([
+    'Banana',
+    'Cantaloupe'
+  ]);
+  fireEvent.click(screen.getByRole('button', { name: 'remove Banana' }));
+  expect(onSelectionChange.thirdCall.firstArg.value).toEqual(['Cantaloupe']);
+});
+
+test('should remove all selected options when clicking on pills', () => {
+  const onSelectionChange = spy();
+  render(
+    <Combobox label="label" multiselect onSelectionChange={onSelectionChange}>
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+  const combobox = screen.getByRole('combobox');
+
+  fireEvent.focus(combobox);
+  assertListboxIsOpen(true);
+  expect(onSelectionChange.notCalled).toBeTruthy();
+  fireEvent.click(screen.getAllByRole('option')[1]);
+  expect(onSelectionChange.callCount).toEqual(1);
+  expect(onSelectionChange.firstCall.firstArg.value).toEqual(['Banana']);
+  fireEvent.click(combobox);
+  fireEvent.click(screen.getAllByRole('option')[2]);
+  expect(onSelectionChange.secondCall.firstArg.value).toEqual([
+    'Banana',
+    'Cantaloupe'
+  ]);
+  fireEvent.click(screen.getByRole('button', { name: 'remove Banana' }));
+  expect(onSelectionChange.thirdCall.firstArg.value).toEqual(['Cantaloupe']);
+  fireEvent.click(screen.getByRole('button', { name: 'remove Cantaloupe' }));
+  expect(onSelectionChange.getCalls()[3].firstArg.value).toEqual([]);
+});
+
 test('should handle selection with "enter" keydown event', () => {
   const onSelectionChange = spy();
   render(
@@ -775,6 +1037,43 @@ test('should handle selection with "enter" keydown event', () => {
   fireEnterKeyPress();
 });
 
+test('should handle multiple selections with "enter" keydown event', () => {
+  const onSelectionChange = spy();
+  render(
+    <Combobox label="label" multiselect onSelectionChange={onSelectionChange}>
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+  const combobox = screen.getByRole('combobox');
+
+  // Note: Combobox forwards events to Listbox via dispatchEvent, but this doesn't
+  // work correctly within jsdom so we fire the events directly on listbox
+  const fireArrowDownKeyPress = () =>
+    fireEvent.keyDown(screen.getByRole('listbox'), { key: 'ArrowDown' });
+  const fireEnterKeyPress = () =>
+    fireEvent.keyDown(screen.getByRole('listbox'), { key: 'Enter' });
+
+  fireEvent.focus(combobox);
+  assertListboxIsOpen(true);
+  expect(onSelectionChange.notCalled).toBeTruthy();
+  fireArrowDownKeyPress();
+  fireEnterKeyPress();
+  expect(onSelectionChange.callCount).toEqual(1);
+  expect(onSelectionChange.firstCall.firstArg.value).toEqual(['Apple']);
+  fireEvent.click(combobox);
+  fireArrowDownKeyPress();
+  fireEnterKeyPress();
+  expect(onSelectionChange.secondCall.firstArg.value).toEqual([
+    'Apple',
+    'Banana'
+  ]);
+  fireEvent.click(combobox);
+  fireArrowDownKeyPress();
+  fireEnterKeyPress();
+});
+
 test('should handle selection when autocomplete="automatic" and combobox input is blurred', () => {
   const onSelectionChange = spy();
   render(
@@ -805,6 +1104,43 @@ test('should handle selection when autocomplete="automatic" and combobox input i
   expect(onSelectionChange.firstCall.firstArg.value).toEqual('Banana');
 });
 
+test('should handle multiple selections when autocomplete="automatic" and combobox input is blurred', () => {
+  const onSelectionChange = spy();
+  render(
+    <Combobox
+      label="label"
+      onSelectionChange={onSelectionChange}
+      multiselect
+      autocomplete="automatic"
+    >
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+  const combobox = screen.getByRole('combobox');
+
+  // Note: Combobox forwards events to Listbox via dispatchEvent, but this doesn't
+  // work correctly within jsdom so we fire the events directly on listbox
+  const fireArrowDownKeyPress = () =>
+    fireEvent.keyDown(screen.getByRole('listbox'), { key: 'ArrowDown' });
+
+  fireEvent.focus(combobox);
+  assertListboxIsOpen(true);
+  expect(onSelectionChange.notCalled).toBeTruthy();
+  fireEvent.blur(combobox);
+  expect(onSelectionChange.callCount).toEqual(1);
+  expect(onSelectionChange.firstCall.firstArg.value).toEqual(['Apple']);
+
+  fireEvent.click(combobox);
+  fireArrowDownKeyPress();
+  fireEvent.blur(combobox);
+  expect(onSelectionChange.secondCall.firstArg.value).toEqual([
+    'Apple',
+    'Banana'
+  ]);
+});
+
 test('should always render all options when autocomplete="none"', () => {
   render(
     <Combobox label="label" autocomplete="none">
@@ -995,6 +1331,23 @@ test('should set selected value with "defaultValue" prop', () => {
   assertOptionIsSelected(1);
 });
 
+test('should not set multiple selected values with "defaultValue" prop', () => {
+  render(
+    <Combobox label="label" multiselect defaultValue={['Apple', 'Banana']}>
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+  const combobox = screen.getByRole('combobox');
+
+  expect(screen.getByRole('combobox')).toHaveDisplayValue('');
+
+  fireEvent.focus(combobox);
+  assertListboxIsOpen(true);
+  assertOptionsAreSelected(0, 1);
+});
+
 test('should set selected value with "value" prop', () => {
   render(
     <Combobox label="label" value="Banana">
@@ -1012,6 +1365,23 @@ test('should set selected value with "value" prop', () => {
   assertOptionIsSelected(1);
 });
 
+test('should not set multiple selected values with "value" prop', () => {
+  render(
+    <Combobox label="label" multiselect value={['Apple', 'Banana']}>
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+  const combobox = screen.getByRole('combobox');
+
+  expect(screen.getByRole('combobox')).toHaveDisplayValue('');
+
+  fireEvent.focus(combobox);
+  assertListboxIsOpen(true);
+  assertOptionsAreSelected(0, 1);
+});
+
 test('should not render hidden input when name is not provided', () => {
   render(
     <Combobox label="label" value="Banana">
@@ -1038,6 +1408,21 @@ test('should render hidden input with value from text contents of ComboboxOption
   ).toEqual('Banana');
 });
 
+test('should render multiple hidden inputs with value from text contents of ComboboxOption', () => {
+  const value = ['Apple', 'Banana'];
+  render(
+    <Combobox label="label" name="fruit" multiselect value={value}>
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+
+  document.querySelectorAll('input[type="hidden"]').forEach((input, idx) => {
+    expect(input.getAttribute('value')).toEqual(value[idx]);
+  });
+});
+
 test('should render hidden input with value from value from ComboboxOption', () => {
   render(
     <Combobox label="label" name="fruit" value="Banana">
@@ -1052,6 +1437,21 @@ test('should render hidden input with value from value from ComboboxOption', ()
   ).toEqual('Banana');
 });
 
+test('should render multiple hidden inputs with value from value from ComboboxOption', () => {
+  const value = ['Apple', 'Banana'];
+  render(
+    <Combobox label="label" name="fruit" multiselect value={value}>
+      <ComboboxOption value="Apple">🍎</ComboboxOption>
+      <ComboboxOption value="Banana">🍌</ComboboxOption>
+      <ComboboxOption value="Cantaloupe">🍈</ComboboxOption>
+    </Combobox>
+  );
+
+  document.querySelectorAll('input[type="hidden"]').forEach((input, idx) => {
+    expect(input.getAttribute('value')).toEqual(value[idx]);
+  });
+});
+
 test('should render hidden input with value from formValue from ComboboxOption', () => {
   render(
     <Combobox label="label" name="fruit" value="Banana">
@@ -1066,6 +1466,22 @@ test('should render hidden input with value from formValue from ComboboxOption',
   ).toEqual('2');
 });
 
+test('should render multiple hidden inputs with value from formValue from ComboboxOption', () => {
+  const value = ['Apple', 'Banana'];
+  const formValue = ['1', '2'];
+  render(
+    <Combobox label="label" name="fruit" multiselect value={value}>
+      <ComboboxOption formValue="1">Apple</ComboboxOption>
+      <ComboboxOption formValue="2">Banana</ComboboxOption>
+      <ComboboxOption formValue="3">Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+
+  document.querySelectorAll('input[type="hidden"]').forEach((input, idx) => {
+    expect(input.getAttribute('value')).toEqual(formValue[idx]);
+  });
+});
+
 test('should support portal element for combobox listbox', () => {
   const element = document.createElement('div');
   render(
@@ -1159,6 +1575,50 @@ test('should have no axe violations with value and expanded', async () => {
   expect(results).toHaveNoViolations();
 });
 
+test('should have no axe violations with multiple values and expanded', async () => {
+  const comboboxRef = createRef<HTMLDivElement>();
+  render(
+    <Combobox
+      label="label"
+      ref={comboboxRef}
+      multiselect
+      value={['Apple', 'Banana']}
+    >
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+
+  expect(comboboxRef.current).toBeTruthy();
+  fireEvent.focus(screen.getByRole('combobox'));
+
+  const results = await axe(comboboxRef.current!);
+  expect(results).toHaveNoViolations();
+});
+
+test('should have no axe violations with multiple values, disabled, and expanded', async () => {
+  const comboboxRef = createRef<HTMLDivElement>();
+  render(
+    <Combobox
+      label="label"
+      ref={comboboxRef}
+      multiselect
+      value={['Apple', 'Banana']}
+      disabled
+    >
+      <ComboboxOption>Apple</ComboboxOption>
+      <ComboboxOption>Banana</ComboboxOption>
+      <ComboboxOption>Cantaloupe</ComboboxOption>
+    </Combobox>
+  );
+
+  expect(comboboxRef.current).toBeTruthy();
+  fireEvent.focus(screen.getByRole('combobox'));
+  const results = await axe(comboboxRef.current!);
+  expect(results).toHaveNoViolations();
+});
+
 test('should have no axe violations with no matching results', async () => {
   const comboboxRef = createRef<HTMLDivElement>();
   render(
diff --git a/packages/react/src/components/Combobox/Combobox.tsx b/packages/react/src/components/Combobox/Combobox.tsx
index 1f99dc0ab..de4284de1 100644
--- a/packages/react/src/components/Combobox/Combobox.tsx
+++ b/packages/react/src/components/Combobox/Combobox.tsx
@@ -3,24 +3,43 @@ import React, {
   useState,
   useRef,
   useCallback,
-  useEffect
+  useEffect,
+  useMemo
 } from 'react';
 import { createPortal } from 'react-dom';
-import classnames from 'classnames';
 import { useId } from 'react-id-generator';
-import { ContentNode } from '../../types';
+import classnames from 'classnames';
 import Listbox from '../Listbox';
 import ComboboxOption from './ComboboxOption';
 import { ComboboxProvider } from './ComboboxContext';
+import type { ContentNode } from '../../types';
 import type { ComboboxOptionState } from './ComboboxContext';
 import type { ComboboxValue } from './ComboboxOption';
 import type { ListboxOption } from '../Listbox/ListboxContext';
 import useSharedRef from '../../utils/useSharedRef';
 import { addIdRef } from '../../utils/idRefs';
 import TextFieldWrapper from '../internal/TextFieldWrapper';
+import { ListboxValue } from '../Listbox/ListboxOption';
+import ComboboxPill from './ComboboxPill';
 
 // Event Keys
-const [Enter, Escape, Home, End] = ['Enter', 'Escape', 'Home', 'End'];
+const [Enter, Escape, Home, End, Backspace, Delete] = [
+  'Enter',
+  'Escape',
+  'Home',
+  'End',
+  'Backspace',
+  'Delete'
+];
+
+const [ArrowDown, ArrowUp, ArrowLeft, ArrowRight] = [
+  'ArrowDown',
+  'ArrowUp',
+  'ArrowLeft',
+  'ArrowRight'
+];
+
+const PillKeys = [Backspace, Delete, ArrowLeft, ArrowRight];
 
 interface ComboboxOption {
   key?: string;
@@ -28,34 +47,61 @@ interface ComboboxOption {
   value?: ComboboxValue;
   formValue?: ComboboxValue;
   description?: string;
+  removeOptionLabel?: string;
 }
 
-interface ComboboxProps
-  extends React.InputHTMLAttributes<
-    Omit<HTMLInputElement, 'value' | 'defaultValue'>
+interface BaseComboboxProps
+  extends Omit<
+    React.InputHTMLAttributes<HTMLInputElement>,
+    'value' | 'defaultValue'
   > {
   label: ContentNode;
   options?: ComboboxOption[];
-  value?: ComboboxValue;
-  defaultValue?: ComboboxValue;
   requiredText?: React.ReactNode;
   error?: React.ReactNode;
   autocomplete?: 'none' | 'manual' | 'automatic';
-  onSelectionChange?: <T extends HTMLElement = HTMLElement>({
-    target,
-    value,
-    previousValue
-  }: {
-    target: T;
-    value: ComboboxValue;
-    previousValue: ComboboxValue;
-  }) => void;
   onActiveChange?: (option: ListboxOption) => void;
   renderNoResults?: (() => React.JSX.Element) | React.ReactElement;
   portal?: React.RefObject<HTMLElement> | HTMLElement;
   inputRef?: React.Ref<HTMLInputElement>;
 }
 
+interface SingleSelectComboboxProps extends BaseComboboxProps {
+  value?: ComboboxValue;
+  defaultValue?: ComboboxValue;
+  inputValue?: never;
+  defaultInputValue?: never;
+  onSelectionChange?: <T extends HTMLElement = HTMLElement>(props: {
+    target: T;
+    value: ComboboxValue;
+    previousValue: ComboboxValue;
+  }) => void;
+  multiselect?: false;
+}
+
+interface MultiSelectComboboxProps extends BaseComboboxProps {
+  value?: ComboboxValue[];
+  defaultValue?: ComboboxValue[];
+  inputValue?: ComboboxValue;
+  defaultInputValue?: ComboboxValue;
+  onSelectionChange?: <T extends HTMLElement = HTMLElement>(props: {
+    target: T;
+    value: ComboboxValue[];
+    previousValue: ComboboxValue[];
+  }) => void;
+  multiselect: true;
+}
+
+type ListboxOnSelectionChange = Parameters<
+  Exclude<
+    React.ComponentPropsWithRef<typeof Listbox>['onSelectionChange'],
+    undefined
+  >
+>[0];
+
+type OnSingleSelectionChange = SingleSelectComboboxProps['onSelectionChange'];
+type OnMultiSelectionChange = MultiSelectComboboxProps['onSelectionChange'];
+
 const defaultAutoCompleteMatches = (inputValue: string, value: string) => {
   // istanbul ignore if
   if (!value) {
@@ -76,7 +122,10 @@ const ComboboxNoResults = ({
   );
 };
 
-const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
+const Combobox = forwardRef<
+  HTMLDivElement,
+  SingleSelectComboboxProps | MultiSelectComboboxProps
+>(
   (
     {
       id: propId,
@@ -86,6 +135,9 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
       options = [],
       value: propValue,
       defaultValue,
+      inputValue: propInputValue,
+      defaultInputValue,
+      multiselect = false,
       requiredText = 'Required',
       error,
       autocomplete = 'manual',
@@ -100,16 +152,28 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
       portal,
       inputRef: propInputRef = null,
       'aria-describedby': ariaDescribedby,
+      disabled = false,
       ...props
     },
     ref
   ): React.JSX.Element => {
-    const [value, setValue] = useState<string>(defaultValue || propValue || '');
     const [matchingOptions, setMatchingOptions] = useState<
       Map<HTMLElement, ComboboxOptionState>
     >(new Map());
-    const [selectedValue, setSelectedValue] = useState<string>(value || '');
-    const [formValue, setFormValue] = useState<string | undefined>('');
+    const [inputValue, setInputValue] = useState(() => {
+      const value = defaultValue || propValue;
+      const inputVal = defaultInputValue || propInputValue;
+      return (Array.isArray(value) ? inputVal : value) || '';
+    });
+    const [selectedValues, setSelectedValues] = useState(() => {
+      const value = defaultValue || propValue;
+      if (!value) {
+        return [];
+      }
+      return Array.isArray(value) ? value : [value];
+    });
+    const [removeOptionLabels, setRemoveOptionLabels] = useState<string[]>([]);
+    const [formValues, setFormValues] = useState<ComboboxValue[]>([]);
     const [open, setOpen] = useState(false);
     const [activeDescendant, setActiveDescendant] =
       useState<ListboxOption | null>(null);
@@ -117,7 +181,10 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
     const comboboxRef = useSharedRef<HTMLDivElement>(ref);
     const inputRef = useSharedRef<HTMLInputElement>(propInputRef);
     const listboxRef = useRef<HTMLUListElement>(null);
-    const isControlled = typeof propValue !== 'undefined';
+    const pillsRef = useRef<HTMLButtonElement[]>([]);
+    const isControlled =
+      typeof propValue !== 'undefined' &&
+      (multiselect ? typeof propInputValue !== 'undefined' : true);
     const isRequired = !!props.required;
     const isAutoComplete = autocomplete !== 'none';
     const hasError = !!error;
@@ -130,12 +197,14 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
           id={`${id}-option-${index + 1}`}
           description={option.description}
           value={option.value}
+          formValue={option.formValue}
+          removeOptionLabel={option.removeOptionLabel}
         >
           {option.label}
         </ComboboxOption>
       ));
 
-    const triggerListboxKeyDown = React.useCallback(
+    const triggerListboxKeyDown = useCallback(
       (key: string) => {
         listboxRef.current?.dispatchEvent(
           new KeyboardEvent('keydown', {
@@ -153,38 +222,47 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         return;
       }
 
-      if (!open && selectedValue && value !== selectedValue) {
-        setValue(selectedValue);
+      if (
+        !open &&
+        !multiselect &&
+        selectedValues.length &&
+        !selectedValues.includes(inputValue)
+      ) {
+        setInputValue(selectedValues[selectedValues.length - 1] || '');
       }
 
       if (!open) {
         setActiveDescendant(null);
       }
 
-      if (open && autocomplete === 'automatic' && !selectedValue) {
+      if (open && autocomplete === 'automatic' && !selectedValues.length) {
         // Fire a Home keydown event on listbox to ensure the first item is selected
         triggerListboxKeyDown(Home);
       }
-    }, [open]);
+    }, [open, multiselect, autocomplete, selectedValues]);
 
     useEffect(() => {
+      const lastSelectedValue =
+        selectedValues.length !== 0
+          ? selectedValues[selectedValues.length - 1]
+          : undefined;
       const [element, option] =
         Array.from(matchingOptions.entries()).find(
-          ([, { selected }]) => selected
+          ([, { value }]) => value === lastSelectedValue
         ) || [];
       if (autocomplete === 'manual') {
         setActiveDescendant(!element ? null : { element, ...option });
       } else if (
         autocomplete === 'automatic' &&
         matchingOptions.size &&
-        !selectedValue
+        !selectedValues.length
       ) {
         // Fire a home keydown event on listbox to ensure the first item is selected
         requestAnimationFrame(() => {
           triggerListboxKeyDown(Home);
         });
       }
-    }, [matchingOptions]);
+    }, [selectedValues, matchingOptions]);
 
     const handleFocus = useCallback(
       (event: React.FocusEvent<HTMLInputElement>) => {
@@ -192,19 +270,29 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         // istanbul ignore else
         if (!event.defaultPrevented) {
           setOpen(true);
-          if (selectedValue && value === selectedValue && isAutoComplete) {
-            setValue('');
+          if (
+            selectedValues.length &&
+            selectedValues.includes(inputValue) &&
+            isAutoComplete
+          ) {
+            setInputValue('');
           }
         }
       },
-      [onFocus, value, selectedValue]
+      [onFocus, inputValue, selectedValues]
     );
 
     const handleInputClick = useCallback(
       (event: React.MouseEvent<HTMLDivElement>) => {
-        setOpen(true);
-        if (selectedValue && value === selectedValue && isAutoComplete) {
-          setValue('');
+        if (!disabled) {
+          setOpen(true);
+        }
+        if (
+          selectedValues.length &&
+          selectedValues.includes(inputValue) &&
+          isAutoComplete
+        ) {
+          setInputValue('');
         }
 
         if (event.target !== inputRef.current) {
@@ -212,7 +300,7 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
           inputRef.current?.focus();
         }
       },
-      [value, selectedValue]
+      [disabled, inputValue, selectedValues]
     );
 
     const handleComboboxOptionMouseDown = useCallback(
@@ -235,19 +323,42 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         onBlur?.(event);
         setOpen(false);
         if (autocomplete === 'automatic' && activeDescendant) {
-          const stringValue =
+          const activeValue =
             activeDescendant.value?.toString() ||
             /* istanbul ignore next: default value */ '';
-          setValue(stringValue);
-          setSelectedValue(stringValue);
-          onSelectionChange?.({
-            target: activeDescendant.element,
-            value: stringValue,
-            previousValue: value
-          });
+          setInputValue(activeValue);
+
+          if (!multiselect) {
+            setSelectedValues([activeValue]);
+            (onSelectionChange as OnSingleSelectionChange)?.({
+              target: activeDescendant.element,
+              value: activeValue,
+              previousValue: selectedValues[0]
+            });
+          } else {
+            const activeValueIndex = selectedValues.indexOf(activeValue);
+            const newSelectedValues = selectedValues.filter(
+              (v) => v !== activeValue
+            );
+            if (activeValueIndex === -1) {
+              newSelectedValues.push(activeValue);
+            }
+            setSelectedValues(newSelectedValues);
+            (onSelectionChange as OnMultiSelectionChange)?.({
+              target: activeDescendant.element,
+              value: newSelectedValues,
+              previousValue: selectedValues
+            });
+          }
         }
       },
-      [autocomplete, activeDescendant, onBlur]
+      [
+        autocomplete,
+        activeDescendant,
+        onBlur,
+        selectedValues,
+        onSelectionChange
+      ]
     );
 
     const handleKeyDown = useCallback(
@@ -256,7 +367,9 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
 
         const enterKeypress = event.key === Enter;
         const escKeypress = event.key === Escape;
-        const arrowKeypress = ['ArrowDown', 'ArrowUp'].includes(event.key);
+        const deleteKeypress = event.key === Delete;
+        const backspaceKeypress = event.key === Backspace;
+        const listboxArrowKeypress = [ArrowUp, ArrowDown].includes(event.key);
 
         if (
           // prevent the page from scrolling and allow start/end option activation
@@ -280,10 +393,23 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
 
         setOpen(true);
 
-        if (!open && arrowKeypress && selectedValue && isAutoComplete) {
+        if (
+          inputValue === '' &&
+          (backspaceKeypress || deleteKeypress || event.key === ArrowLeft) &&
+          pillsRef.current.length !== 0
+        ) {
+          pillsRef.current[pillsRef.current.length - 1]?.focus();
+        }
+
+        if (
+          !open &&
+          listboxArrowKeypress &&
+          selectedValues.length &&
+          isAutoComplete
+        ) {
           // If the user opens the combobox again with a selected value
           // just clear out the field to restore filtering capabilities
-          setValue('');
+          setInputValue('');
         }
 
         // Space should not trigger selection since the user could be typing
@@ -301,60 +427,165 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         triggerListboxKeyDown(event.key);
 
         // Close combobox with keyboard selections
-        if (enterKeypress && activeDescendant) {
+        if (enterKeypress && activeDescendant && !multiselect) {
           setOpen(false);
         }
       },
-      [onKeyDown, isAutoComplete, open, selectedValue, activeDescendant]
+      [
+        onKeyDown,
+        isAutoComplete,
+        open,
+        multiselect,
+        selectedValues,
+        activeDescendant
+      ]
     );
 
     useEffect(() => {
-      if (typeof propValue !== 'undefined') {
-        setValue(propValue);
+      if (isControlled) {
+        if (!multiselect) {
+          setInputValue((propValue as ComboboxValue) || '');
+        } else {
+          setInputValue((propInputValue as ComboboxValue) || '');
+        }
       }
-    }, [propValue]);
+    }, [multiselect, propValue, propInputValue]);
 
     const handleChange = useCallback(
       (event: React.ChangeEvent<HTMLInputElement>) => {
+        if (disabled) {
+          return;
+        }
         onChange?.(event);
         // istanbul ignore else
         if (!isControlled) {
-          setValue(event.target.value);
+          setInputValue(event.target.value);
         }
       },
       [isControlled, onChange]
     );
 
-    const handleSelectionChange = useCallback(
-      ({
-        target,
-        value: listboxValue,
-        previousValue
-      }: Parameters<
-        Exclude<
-          React.ComponentProps<typeof Listbox>['onSelectionChange'],
-          undefined
-        >
-      >[0]) => {
-        const stringValue =
-          listboxValue?.toString() || /* istanbul ignore next */ '';
-
-        // istanbul ignore else
-        if (!isControlled) {
-          setValue(stringValue);
+    const handleRemovePill = useCallback(
+      (target: HTMLElement, value: ComboboxValue) => {
+        if (disabled) {
+          return;
         }
 
-        setSelectedValue(stringValue);
+        const newSelectedValues = selectedValues.filter((v) => v !== value);
 
-        onSelectionChange?.({
+        setSelectedValues(newSelectedValues);
+        (onSelectionChange as OnMultiSelectionChange)?.({
           target,
-          value: stringValue,
-          previousValue: previousValue?.toString()
+          value: newSelectedValues,
+          previousValue: selectedValues
         });
+      },
+      [disabled, selectedValues, onSelectionChange]
+    );
 
-        setOpen(false);
+    const handlePillKeyDown = useCallback(
+      (event: React.KeyboardEvent<HTMLButtonElement>) => {
+        if (!PillKeys.includes(event.key)) {
+          return;
+        }
+
+        event.preventDefault();
+
+        const isDelete = event.key === Delete;
+        const isBackspace = event.key === Backspace;
+        const isArrowLeft = event.key === ArrowLeft;
+        const isArrowRight = event.key === ArrowRight;
+
+        const focusedIndex = pillsRef.current.findIndex(
+          (p) => document.activeElement === p
+        );
+
+        const pillsLength = pillsRef.current.length;
+
+        if (isDelete || isBackspace) {
+          if (disabled) {
+            return;
+          }
+
+          handleRemovePill(
+            pillsRef.current[focusedIndex],
+            selectedValues[focusedIndex]
+          );
+
+          const nextIndex = focusedIndex + 1;
+
+          if (nextIndex == pillsLength) {
+            inputRef.current.focus();
+          } else {
+            pillsRef.current[nextIndex].focus();
+          }
+        } else if (isArrowLeft || isArrowRight) {
+          const nextIndex = Math.max(focusedIndex + (isArrowLeft ? -1 : 1), 0);
+
+          if (isArrowRight && nextIndex === pillsLength) {
+            inputRef.current.focus();
+          } else {
+            pillsRef.current[nextIndex].focus();
+          }
+        }
+      },
+      [disabled, pillsRef, handleRemovePill]
+    );
+
+    const handleSelectionChange = useCallback(
+      ({ target, value, previousValue }: ListboxOnSelectionChange) => {
+        if (!multiselect) {
+          const listboxValue = (value as ListboxValue)?.toString() || '';
+          const listboxPreviousValue =
+            (previousValue as ListboxValue)?.toString() || '';
+
+          // istanbul ignore else
+          if (!isControlled) {
+            setInputValue(listboxValue);
+          }
+
+          setSelectedValues([listboxValue]);
+          (onSelectionChange as OnSingleSelectionChange)?.({
+            target,
+            value: listboxValue,
+            previousValue: listboxPreviousValue
+          });
+        } else {
+          const listboxPreviousValue = previousValue as ComboboxValue[];
+          const listboxValue = value as ComboboxValue[];
+
+          const previousValueSet = new Set(listboxPreviousValue);
+          const valueSet = new Set(listboxValue);
+
+          const removedValues = new Set(
+            listboxPreviousValue.filter((v) => !valueSet.has(v))
+          );
+          const addedValues = listboxValue.filter(
+            (v) => !previousValueSet.has(v)
+          );
+
+          const newSelectedValues = selectedValues
+            .filter((v) => !removedValues.has(v))
+            .concat(addedValues);
+
+          // istanbul ignore else
+          if (!isControlled) {
+            setInputValue('');
+          }
+
+          setSelectedValues(newSelectedValues);
+          (onSelectionChange as OnMultiSelectionChange)?.({
+            target,
+            value: newSelectedValues,
+            previousValue: selectedValues
+          });
+        }
+
+        if (!multiselect) {
+          setOpen(false);
+        }
       },
-      [isControlled, onSelectionChange]
+      [isControlled, multiselect, selectedValues, onSelectionChange]
     );
 
     const handleActiveChange = useCallback((option: ListboxOption) => {
@@ -366,7 +597,7 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
       onActiveChange?.(option);
     }, []);
 
-    const NoMatchingOptions = React.useMemo(
+    const NoMatchingOptions = useMemo(
       () =>
         React.isValidElement(renderNoResults)
           ? () => <ComboboxNoResults>{renderNoResults}</ComboboxNoResults>
@@ -376,11 +607,15 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
       [renderNoResults]
     );
 
-    const noMatchingOptions = !!value?.length && !matchingOptions.size && (
+    const noMatchingOptions = !!inputValue && !matchingOptions.size && (
       <NoMatchingOptions />
     );
 
     const comboboxListbox = (
+      // eslint-disable-next-line
+      // @ts-expect-error
+      // multiselect & value props are passed to Listbox, but TS is unable to infer that
+      // it's a correct mapping from Combobox's multiselect & value props
       <Listbox
         className={classnames('Combobox__listbox', {
           'Combobox__listbox--open': open
@@ -388,7 +623,7 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         role={noMatchingOptions ? 'presentation' : 'listbox'}
         aria-labelledby={noMatchingOptions ? undefined : `${id}-label`}
         id={`${id}-listbox`}
-        value={selectedValue}
+        value={multiselect ? selectedValues : selectedValues[0]}
         onMouseDown={handleComboboxOptionMouseDown}
         onClick={handleComboboxOptionClick}
         onSelectionChange={handleSelectionChange}
@@ -396,6 +631,8 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         ref={listboxRef}
         tabIndex={undefined}
         aria-activedescendant={undefined}
+        multiselect={multiselect}
+        disabled={disabled}
       >
         {comboboxOptions}
         {noMatchingOptions}
@@ -413,10 +650,17 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
     return (
       <div
         id={id}
-        className={classnames('Combobox', className)}
+        className={classnames(
+          'Combobox',
+          { 'Combobox--multiselect': multiselect },
+          className
+        )}
         ref={comboboxRef}
       >
-        {name && <input type="hidden" name={name} value={formValue} />}
+        {name &&
+          formValues.map((formValue, index) => (
+            <input type="hidden" key={index} name={name} value={formValue} />
+          ))}
         <label
           className={classnames('Field__label', {
             'Field__label--is-required': isRequired,
@@ -439,12 +683,38 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
           // there's already keyboard handlers to open the listbox on the input element
           onClick={handleInputClick}
         >
+          {multiselect &&
+            selectedValues.map((value, index) => {
+              const refCallback = (elem: HTMLButtonElement | null) => {
+                if (elem) {
+                  pillsRef.current[index] = elem;
+                } else {
+                  pillsRef.current.splice(index, 1);
+                }
+              };
+
+              const handleClick = () =>
+                handleRemovePill(pillsRef.current[index], value);
+
+              return (
+                <ComboboxPill
+                  ref={refCallback}
+                  key={value}
+                  value={value}
+                  removeOptionLabel={removeOptionLabels[index]}
+                  disabled={disabled}
+                  onClick={handleClick}
+                  onKeyDown={handlePillKeyDown}
+                />
+              );
+            })}
           <input
             type="text"
             id={`${id}-input`}
             ref={inputRef}
-            value={value}
+            value={inputValue}
             role="combobox"
+            disabled={disabled}
             aria-autocomplete={!isAutoComplete ? 'none' : 'list'}
             aria-controls={`${id}-listbox`}
             aria-expanded={open}
@@ -462,13 +732,15 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         </TextFieldWrapper>
         <ComboboxProvider
           autocomplete={autocomplete}
-          inputValue={value}
-          formValue={formValue}
-          selectedValue={selectedValue}
+          inputValue={inputValue}
+          formValues={formValues}
+          selectedValues={selectedValues}
+          removeOptionLabels={removeOptionLabels}
+          setRemoveOptionLabels={setRemoveOptionLabels}
           matches={!isAutoComplete || defaultAutoCompleteMatches}
           matchingOptions={matchingOptions}
           setMatchingOptions={setMatchingOptions}
-          setFormValue={setFormValue}
+          setFormValues={setFormValues}
         >
           {portal && typeof document !== 'undefined'
             ? (createPortal(
diff --git a/packages/react/src/components/Combobox/ComboboxContext.tsx b/packages/react/src/components/Combobox/ComboboxContext.tsx
index 96fc6f4d0..86ce398d0 100644
--- a/packages/react/src/components/Combobox/ComboboxContext.tsx
+++ b/packages/react/src/components/Combobox/ComboboxContext.tsx
@@ -4,19 +4,22 @@ import { ComboboxValue } from './ComboboxOption';
 type ComboboxContext = {
   autocomplete: 'none' | 'manual' | 'automatic';
   inputValue: ComboboxValue;
-  formValue: ComboboxValue;
-  selectedValue: ComboboxValue;
+  formValues: ComboboxValue[];
+  selectedValues: ComboboxValue[];
+  removeOptionLabels: string[];
+  setRemoveOptionLabels: React.Dispatch<React.SetStateAction<string[]>>;
   matchingOptions: Map<HTMLElement, ComboboxOptionState>;
   setMatchingOptions: React.Dispatch<
     React.SetStateAction<Map<HTMLElement, ComboboxOptionState>>
   >;
-  setFormValue: React.Dispatch<React.SetStateAction<ComboboxValue>>;
+  setFormValues: React.Dispatch<React.SetStateAction<ComboboxValue[]>>;
   matches: (<T extends string = string>(value: T) => boolean) | boolean;
 };
 
 export type ComboboxOptionState = {
   selected: boolean;
   value: ComboboxValue;
+  displayValue: ComboboxValue;
 };
 
 type ComboboxProvider = {
@@ -28,23 +31,27 @@ type ComboboxProvider = {
 const ComboboxContext = createContext<ComboboxContext>({
   autocomplete: 'manual',
   inputValue: undefined,
-  formValue: undefined,
-  selectedValue: undefined,
+  formValues: [],
+  selectedValues: [],
+  removeOptionLabels: [],
+  setRemoveOptionLabels: () => null,
   matches: true,
   matchingOptions: new Map(),
   setMatchingOptions: () => null,
-  setFormValue: () => null
+  setFormValues: () => null
 });
 
 function ComboboxProvider({
   autocomplete,
   inputValue,
-  formValue,
-  selectedValue,
+  formValues,
+  selectedValues,
+  removeOptionLabels,
+  setRemoveOptionLabels,
   matches,
   matchingOptions,
   setMatchingOptions,
-  setFormValue,
+  setFormValues,
   children
 }: ComboboxProvider): React.JSX.Element {
   const { Provider } = ComboboxContext as React.Context<ComboboxContext>;
@@ -52,25 +59,29 @@ function ComboboxProvider({
     () => ({
       autocomplete,
       inputValue,
-      formValue,
-      selectedValue,
+      formValues,
+      selectedValues,
+      removeOptionLabels,
+      setRemoveOptionLabels,
       matches:
         typeof matches === 'function' && !!inputValue
           ? (value) => matches(inputValue, value)
           : true,
       matchingOptions,
       setMatchingOptions,
-      setFormValue
+      setFormValues
     }),
     [
       autocomplete,
       inputValue,
-      formValue,
-      selectedValue,
+      formValues,
+      selectedValues,
+      removeOptionLabels,
+      setRemoveOptionLabels,
       matches,
       matchingOptions,
       setMatchingOptions,
-      setFormValue
+      setFormValues
     ]
   );
 
diff --git a/packages/react/src/components/Combobox/ComboboxOption.tsx b/packages/react/src/components/Combobox/ComboboxOption.tsx
index ddd01c19b..43c4697da 100644
--- a/packages/react/src/components/Combobox/ComboboxOption.tsx
+++ b/packages/react/src/components/Combobox/ComboboxOption.tsx
@@ -16,6 +16,7 @@ interface ComboboxOptionProps extends React.HTMLAttributes<HTMLLIElement> {
   value?: ComboboxValue;
   formValue?: ComboboxValue;
   description?: ContentNode;
+  removeOptionLabel?: string;
   children: string;
 }
 
@@ -64,14 +65,20 @@ const ComboboxOption = forwardRef<HTMLLIElement, ComboboxOptionProps>(
       description,
       value: propValue,
       formValue,
+      removeOptionLabel,
       ...props
     },
     ref
   ): React.JSX.Element | null => {
     const [id] = propId ? [propId] : useId(1, 'combobox-option');
     const { selected, active } = useListboxContext();
-    const { selectedValue, matches, setMatchingOptions, setFormValue } =
-      useComboboxContext();
+    const {
+      selectedValues,
+      setRemoveOptionLabels,
+      matches,
+      setMatchingOptions,
+      setFormValues
+    } = useComboboxContext();
     const comboboxOptionRef = useSharedRef<HTMLElement>(ref);
     const intersectionRef = useIntersectionRef<HTMLElement>(comboboxOptionRef, {
       root: null,
@@ -81,8 +88,9 @@ const ComboboxOption = forwardRef<HTMLLIElement, ComboboxOptionProps>(
       !!active?.element && active.element === comboboxOptionRef.current;
     const isSelected = !!(
       selected &&
-      !!selected[0]?.element &&
-      selected[0].element === comboboxOptionRef.current
+      selected.findIndex(
+        ({ element }) => element === comboboxOptionRef.current
+      ) !== -1
     );
     const isMatching =
       (typeof matches === 'boolean' && matches) ||
@@ -115,20 +123,38 @@ const ComboboxOption = forwardRef<HTMLLIElement, ComboboxOptionProps>(
         typeof propValue !== 'undefined'
           ? propValue
           : comboboxOptionRef.current?.innerText;
-
-      if (selectedValue === comboboxValue) {
-        setFormValue(
-          typeof formValue === 'undefined' ? comboboxValue : formValue
+      const value =
+        typeof formValue === 'undefined' ? comboboxValue : formValue;
+
+      setFormValues((prev) => {
+        const formValues = prev.filter((fv) => fv !== value);
+        if (selectedValues.includes(comboboxValue)) {
+          formValues.push(value);
+        }
+        return formValues;
+      });
+
+      setRemoveOptionLabels((prev) => {
+        if (!removeOptionLabel) {
+          return prev;
+        }
+        return selectedValues.map((value, index) =>
+          value === comboboxValue ? removeOptionLabel : prev[index]
         );
-      }
-    }, [selectedValue, formValue]);
+      });
+    }, [selectedValues, formValue]);
 
     useEffect(() => {
       if (isMatching) {
+        const comboboxValue =
+          typeof propValue !== 'undefined'
+            ? propValue
+            : comboboxOptionRef.current?.innerText;
         setMatchingOptions((options) => {
           return new Map(
             options.set(comboboxOptionRef.current, {
-              value: children as string,
+              value: comboboxValue,
+              displayValue: children as string,
               selected: isSelected
             })
           );
diff --git a/packages/react/src/components/Combobox/ComboboxPill.tsx b/packages/react/src/components/Combobox/ComboboxPill.tsx
new file mode 100644
index 000000000..24d1f587c
--- /dev/null
+++ b/packages/react/src/components/Combobox/ComboboxPill.tsx
@@ -0,0 +1,40 @@
+import React, { forwardRef } from 'react';
+import { ComboboxValue } from './ComboboxOption';
+import TagButton from '../TagButton';
+import Button from '../Button';
+
+interface ComboboxPillProps extends React.HTMLAttributes<HTMLButtonElement> {
+  value: ComboboxValue;
+  removeOptionLabel?: string;
+  disabled?: boolean;
+  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
+}
+
+const ComboboxPill = forwardRef<HTMLButtonElement, ComboboxPillProps>(
+  ({ value, removeOptionLabel, disabled = false, ...rest }, ref) => {
+    const commonProps = {
+      ref,
+      'aria-label': removeOptionLabel ? removeOptionLabel : `remove ${value}`,
+      className: 'ComboboxPill',
+      tabIndex: -1
+    };
+
+    return !disabled ? (
+      <TagButton
+        label={value || ''}
+        value=""
+        icon="close"
+        {...commonProps}
+        {...rest}
+      />
+    ) : (
+      <Button variant="tag" disabled={disabled} {...commonProps} {...rest}>
+        {value}
+      </Button>
+    );
+  }
+);
+
+ComboboxPill.displayName = 'ComboboxPill';
+
+export default ComboboxPill;
diff --git a/packages/react/src/components/Combobox/screenshots.e2e.tsx b/packages/react/src/components/Combobox/screenshots.e2e.tsx
index 64189aec7..0f5095b02 100644
--- a/packages/react/src/components/Combobox/screenshots.e2e.tsx
+++ b/packages/react/src/components/Combobox/screenshots.e2e.tsx
@@ -36,6 +36,52 @@ test('should have screenshot for Combobox', async ({ mount, page }) => {
   await expect(component).toHaveScreenshot('dark--combobox');
 });
 
+test('should have screenshot for Combobox[multiselect]', async ({
+  mount,
+  page
+}) => {
+  await page.addStyleTag({
+    // ensure screenshots are able to capture the boundaries of an expanded combobox
+    content: `.Combobox__listbox--open.Combobox__listbox { position: relative !important; }`
+  });
+  const component = await mount(
+    <FieldWrap>
+      <Combobox label="Combobox" multiselect>
+        <ComboboxOption>Apple</ComboboxOption>
+        <ComboboxOption>Banana</ComboboxOption>
+        <ComboboxOption>Cucumber</ComboboxOption>
+      </Combobox>
+      <Combobox label="Hover" multiselect>
+        <ComboboxOption>Apple</ComboboxOption>
+        <ComboboxOption>Banana</ComboboxOption>
+        <ComboboxOption>Cucumber</ComboboxOption>
+      </Combobox>
+      <Combobox
+        label="Disabled"
+        disabled
+        multiselect
+        value={['Apple', 'Banana']}
+      >
+        <ComboboxOption>Apple</ComboboxOption>
+        <ComboboxOption>Banana</ComboboxOption>
+        <ComboboxOption>Cucumber</ComboboxOption>
+      </Combobox>
+      <Combobox label="Focus" multiselect value={['Apple', 'Banana']}>
+        <ComboboxOption>Apple</ComboboxOption>
+        <ComboboxOption>Banana</ComboboxOption>
+        <ComboboxOption>Cucumber</ComboboxOption>
+      </Combobox>
+    </FieldWrap>
+  );
+
+  await component.getByRole('combobox', { name: 'Focus' }).focus();
+  await component.getByRole('combobox', { name: 'Hover' }).hover();
+
+  await expect(component).toHaveScreenshot('combobox[multiselect]');
+  await setTheme(page, 'dark');
+  await expect(component).toHaveScreenshot('dark--combobox[multiselect]');
+});
+
 test('should have screenshot for Combobox[error]', async ({ mount, page }) => {
   await page.addStyleTag({
     // ensure screenshots are able to capture the boundaries of an expanded combobox
diff --git a/packages/react/src/components/Listbox/Listbox.tsx b/packages/react/src/components/Listbox/Listbox.tsx
index 2ee88e4d3..267c4c55a 100644
--- a/packages/react/src/components/Listbox/Listbox.tsx
+++ b/packages/react/src/components/Listbox/Listbox.tsx
@@ -22,6 +22,7 @@ interface BaseListboxProps
   > {
   navigation?: 'cycle' | 'bound';
   onActiveChange?: (option: ListboxOption) => void;
+  disabled?: boolean;
 }
 
 interface SingleSelectListboxProps extends BaseListboxProps {
@@ -75,6 +76,7 @@ const Listbox = forwardRef<
       onFocus,
       onSelectionChange,
       onActiveChange,
+      disabled = false,
       ...props
     },
     ref
@@ -127,6 +129,10 @@ const Listbox = forwardRef<
     const handleSelect = useCallback(
       (option: ListboxOption) => {
         setActiveOption(option);
+        if (disabled) {
+          return;
+        }
+
         const optionIsSelected = selectedOptions.some(
           (selected) => selected.element === option.element
         );
diff --git a/packages/styles/combobox.css b/packages/styles/combobox.css
index 0eaa73306..51abc6605 100644
--- a/packages/styles/combobox.css
+++ b/packages/styles/combobox.css
@@ -9,6 +9,8 @@
   --combobox-group-label-background-color: var(--gray-20);
   --combobox-group-label-font-size: var(--text-size-smaller);
   --combobox-option-selected-icon-color: var(--accent-primary);
+  --combobox-pill-height: 1.65rem;
+  --combobox-pill-icon-size: 18px;
 }
 
 .cauldron--theme-dark {
@@ -115,6 +117,10 @@
   margin-right: 1.25rem;
 }
 
+.Combobox .TextFieldWrapper input:is([disabled]) {
+  background-color: var(--field-background-color-disabled);
+}
+
 .Combobox__arrow {
   width: 2rem;
   display: flex;
@@ -254,3 +260,23 @@
   padding: var(--space-smaller);
   text-align: center;
 }
+
+.Combobox--multiselect .TextFieldWrapper {
+  flex-wrap: wrap;
+  gap: var(--space-half);
+  padding-left: var(--space-half);
+  padding-right: var(--space-large);
+}
+
+.Combobox--multiselect .TextFieldWrapper input {
+  width: auto;
+}
+
+.ComboboxPill {
+  --tag-height: var(--combobox-pill-height);
+  --icon-size: var(--combobox-pill-icon-size);
+}
+
+.ComboboxPill + .ComboboxPill {
+  margin: 0;
+}


Reviews:
user: login: anastasialanz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T21:00:43Z
user: login: scurker
body: I haven't been able to do a full in-depth review but wanted to outline a couple of quick things I noticed along with some general observations:

When a value is selected, the input value of the field is still present per the following requirement:
> When a selection is made to select an active selection, the combobox inner text field should be reset to blank.

I'm also not able to easily navigate between the pills via arrow keys. It doesn't look like I can navigate (via left/right arrow keys) when there's nothing in the input field and I can only navigate through when I press <kbd>backspace</kbd>. The interaction pattern we're expecting should be somewhat similar to this: https://atlassian.design/components/select/examples#multi-select

This is shaping up pretty good though, thank you for putting time into this! This has been a much wanted component for a while.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T22:56:27Z
user: login: scurker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T22:59:59Z
user: login: scurker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T23:02:55Z
user: login: scurker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T23:07:05Z
user: login: scurker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T23:12:22Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:04:50Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:09:12Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:13:31Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:13:42Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:13:49Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:13:59Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:14:11Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:14:18Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:16:25Z
user: login: scurker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T20:20:32Z
user: login: scurker
body: A couple of issues I'm still seeing:

The text is still present in the Combobox once I select a value:

![](https://github.com/user-attachments/assets/22b7107b-3ab4-42fe-bc7f-e062961c38e4)

Somehow, I was able to get into a scenario where there were multiples of the same value selected:

![](https://github.com/user-attachments/assets/d2538548-f91a-4456-af34-433df701b227)

When I have a list of selected options, type in an additional option (to search/limit what's available) and select an option, it clears out all of the previous values.

When I have a pill "focused" it's stuck in a focus trap that I can't escape out of unless I navigate via right arrow back to the input field.

state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T20:41:48Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:45:22Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:50:29Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T08:05:34Z
user: login: scurker
body: Please don't be discouraged by the amount of feedback, I think this is starting to shape up really nicely and we're getting very close. Comboboxes are pretty complex and I think you've done a great job at getting a lot of the basics right.

But just a couple of more feedback things:

I'm able to select items when a multiselect combobox is disabled. This isn't the correct behavior.

Additionally, when I type in "pea" and select "peach", "pea" remains in the input field. This value should be cleared out once an option is selected.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T17:57:26Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T09:23:32Z
user: login: scurker
body: A couple of observations from testing:

When the combobox is multiselect, I think we need to keep the listbox open so the user can easily select other options. I also suspect this may provide a better AT experience since right now selecting an item focuses back on an empty input field and doesn't announce that the selected option was selected.

We should also disable the editable inner text field when the combobox is set to disabled. It's a little bit of a strange experience to be able to navigate the listbox when you can't actually select any items.

Aside from that, I'm still working to get a design review from @dequelabs/cauldron-design.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:59:44Z
user: login: chornonoh-vova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T06:22:18Z
user: login: scurker
body: Code wise, things are looking good - nice work! There's still an a11y behavior we need to address but this is really close.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T20:14:37Z
user: login: shawnsharpDQ
body: Looks good!
state: DISMISSED
author_association: NONE
submitted_at: 2025-03-12T12:56:41Z
user: login: scurker
body: Last small thing to address, great work here!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T18:45:55Z
user: login: scurker
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T15:51:28Z

Review comments:
diff_hunk: @@ -0,0 +1,59 @@
+type GraphElement = {
+  element: HTMLElement;
+  x: number;
+  y: number;
+  height: number;
+  dx: number;
+  dy: number;
+};
+
+function elementsGraph(elements: HTMLElement[], idx: number): GraphElement[] {
+  const { x, y } = elements[idx].getBoundingClientRect();
+  return elements.map((element) => {
+    const { x: eX, y: eY, height: eHeight } = element.getBoundingClientRect();
+    return {
+      element,
+      x: eX,
+      y: eY,
+      height: eHeight,
+      dx: x - eX,
+      dy: y - eY
+    };
+  });
+}
+
+type Dir = 'up' | 'down';
+
+/**
+ * Perform search for the closest element in the array of elements relative to the currently selected element.
+ * Searches for the elements in the direction `dir`.
+ * If the element couldn't be found, returns currenly selected element.
path: packages/react/src/utils/closestElement.ts
user: login: anastasialanz
body: ```suggestion
 * If the element couldn't be found, returns currently selected element.
```
created_at: 2025-02-28T21:00:42Z
author_association: COLLABORATOR
diff_hunk: @@ -76,7 +116,39 @@ const ComboboxNoResults = ({
   );
 };
 
-const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
+function nextToFocus(
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: I'm... a little unclear what this is for. I'm going to leave some other comments related to this but maybe you could provide some additional clarification. It looks like this is trying to find some closest element based on positioning and adjacency and it's not clear why.
created_at: 2025-03-04T22:28:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,7 +116,39 @@ const ComboboxNoResults = ({
   );
 };
 
-const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
+function nextToFocus(
+  elements: HTMLElement[],
+  fallback: HTMLElement,
+  focusedIdx: number,
+  direction: number
+): HTMLElement | null {
+  const elems = elements.concat(fallback);
+  if (direction === -1) {
+    return null;
+  }
+
+  const style = getComputedStyle(elems[focusedIdx]);
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Generally speaking, we want to try to avoid calling things that may result in [layout or reflow](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) unless it's absolutely necessary. It might not be a problem here since it's only being invoked on `keydown` but it's not immediately clear why we need to know anything about layout/positioning.
created_at: 2025-03-04T22:31:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,24 +175,36 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
       portal,
       inputRef: propInputRef = null,
       'aria-describedby': ariaDescribedby,
+      disabled = false,
       ...props
     },
     ref
   ): React.JSX.Element => {
-    const [value, setValue] = useState<string>(defaultValue || propValue || '');
     const [matchingOptions, setMatchingOptions] = useState<
       Map<HTMLElement, ComboboxOptionState>
     >(new Map());
-    const [selectedValue, setSelectedValue] = useState<string>(value || '');
-    const [formValue, setFormValue] = useState<string | undefined>('');
+    const [inputValue, setInputValue] = useState(() => {
+      const value = defaultValue || propValue;
+      const inputVal = defaultInputValue || propInputValue;
+      return (Array.isArray(value) ? inputVal : value) || '';
+    });
+    const [selectedValues, setSelectedValues] = useState(() => {
+      const value = defaultValue || propValue;
+      if (!value) return [];
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Nit:

```suggestion
      if (!value) { 
        return [];
      }
```
created_at: 2025-03-04T22:32:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -235,16 +335,33 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         onBlur?.(event);
         setOpen(false);
         if (autocomplete === 'automatic' && activeDescendant) {
-          const stringValue =
+          const activeValue =
             activeDescendant.value?.toString() ||
             /* istanbul ignore next: default value */ '';
-          setValue(stringValue);
-          setSelectedValue(stringValue);
-          onSelectionChange?.({
-            target: activeDescendant.element,
-            value: stringValue,
-            previousValue: value
-          });
+          setInputValue(activeValue);
+
+          if (!multiselect) {
+            setSelectedValues([activeValue]);
+            (onSelectionChange as OnSingleSelectionChange)?.({
+              target: activeDescendant.element,
+              value: activeValue,
+              previousValue: selectedValues[0]
+            });
+          } else {
+            const idx = selectedValues.indexOf(activeValue);
+            const newSelectedValues = selectedValues.filter(
+              (v) => v !== activeValue
+            );
+            if (idx === -1) {
+              newSelectedValues.push(activeValue);
+            }
+            setSelectedValues(newSelectedValues);
+            (onSelectionChange as OnMultiSelectionChange)?.({
+              target: activeDescendant.element,
+              value: newSelectedValues,
+              previousValue: selectedValues
+            });
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Nit: We should avoid abbreviated names when possible. Also providing a descriptive value helps for readability since we know what index it is for.

```suggestion
            const activeValueIndex = selectedValues.indexOf(activeValue);
            const newSelectedValues = selectedValues.filter(
              (v) => v !== activeValue
            );
            if (activeValueIndex === -1) {
              newSelectedValues.push(activeValue);
            }
            setSelectedValues(newSelectedValues);
            (onSelectionChange as OnMultiSelectionChange)?.({
              target: activeDescendant.element,
              value: newSelectedValues,
              previousValue: selectedValues
            });
```
created_at: 2025-03-04T22:35:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -376,26 +565,31 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
       [renderNoResults]
     );
 
-    const noMatchingOptions = !!value?.length && !matchingOptions.size && (
+    const noMatchingOptions = !!inputValue && !matchingOptions.size && (
       <NoMatchingOptions />
     );
 
     const comboboxListbox = (
+      // eslint-disable-next-line
+      // @ts-ignore
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Nit: If needed, this is the better way to indicate we're expecting an issue rather than just outright ignoring it.

```suggestion
      // @ts-expect-error
```
created_at: 2025-03-04T22:42:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -434,16 +636,54 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         </label>
         {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
         <TextFieldWrapper
-          className={classnames({ 'TextFieldWrapper--error': hasError })}
+          className={classnames({
+            'Combobox__TextFieldWrapper--multiselect': multiselect,
+            'TextFieldWrapper--error': hasError
+          })}
           // We're handling click here to open the listbox when the wrapping element is clicked,
           // there's already keyboard handlers to open the listbox on the input element
           onClick={handleInputClick}
         >
+          {multiselect &&
+            selectedValues.map((value, idx) => {
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Nit: Same suggestion as above regarding index...

```suggestion
            selectedValues.map((value, index) => {
```
created_at: 2025-03-04T22:43:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,49 +28,49 @@ type ComboboxProvider = {
 const ComboboxContext = createContext<ComboboxContext>({
path: packages/react/src/components/Combobox/ComboboxContext.tsx
user: login: scurker
body: Would we not also want `multiselect` inside of the context? See https://github.com/dequelabs/cauldron/blob/158b965d91a283df2647c43d5f52ef89da6297d4/packages/react/src/components/Listbox/ListboxContext.tsx#L14
created_at: 2025-03-04T22:59:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,7 +116,39 @@ const ComboboxNoResults = ({
   );
 };
 
-const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
+function nextToFocus(
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: I'm guessing the requirement of "arrow keys" wasn't detailed enough. The intention was for there to be able to navigate via left/right arrow key navigation based on the selection order, not having to do anything with dom positioning if that helps clarify.
created_at: 2025-03-04T23:02:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -254,3 +254,24 @@
   padding: var(--space-smaller);
   text-align: center;
 }
+
+.Combobox__TextFieldWrapper--multiselect {
+  flex-wrap: wrap;
+  padding-right: var(--space-large);
+}
+
+.Combobox__TextFieldWrapper--multiselect input {
+  width: unset;
+}
path: packages/styles/combobox.css
user: login: scurker
body: I would probably recommend setting `width: auto` instead.

```suggestion
.Combobox__TextFieldWrapper--multiselect input {
  width: auto;
}
```
created_at: 2025-03-04T23:07:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -434,16 +636,54 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         </label>
         {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
         <TextFieldWrapper
-          className={classnames({ 'TextFieldWrapper--error': hasError })}
+          className={classnames({
+            'Combobox__TextFieldWrapper--multiselect': multiselect,
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: The naming convention on this class... is a little odd. It implies that TextFieldWrapper has a bem modifier of multiselect, which really belongs to Combobox.

My recommendation here would be instead to add the modifier to combobox and have a style cascade. Example:

```tsx
<div
  id={id}
  className={classnames('Combobox', className, { 'Combobox--multiselect': multiselect })}
  ref={comboboxRef}
>
  ...
</div>
```

Then in your styles you would just target the selector cascade:

```css
.Combobox--multiselect .TextFieldWrapper {
  ...
}
```
created_at: 2025-03-04T23:12:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,7 +116,39 @@ const ComboboxNoResults = ({
   );
 };
 
-const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
+function nextToFocus(
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: These calculations were done to support the following interactions with Up/Down errors when focused on the pills

https://github.com/user-attachments/assets/422b311c-8d44-41b5-b185-f52c073894cb

If it's not required, I'll remove it
created_at: 2025-03-05T09:04:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,49 +28,49 @@ type ComboboxProvider = {
 const ComboboxContext = createContext<ComboboxContext>({
path: packages/react/src/components/Combobox/ComboboxContext.tsx
user: login: chornonoh-vova
body: No, it's not needed
created_at: 2025-03-05T09:09:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,24 +175,36 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
       portal,
       inputRef: propInputRef = null,
       'aria-describedby': ariaDescribedby,
+      disabled = false,
       ...props
     },
     ref
   ): React.JSX.Element => {
-    const [value, setValue] = useState<string>(defaultValue || propValue || '');
     const [matchingOptions, setMatchingOptions] = useState<
       Map<HTMLElement, ComboboxOptionState>
     >(new Map());
-    const [selectedValue, setSelectedValue] = useState<string>(value || '');
-    const [formValue, setFormValue] = useState<string | undefined>('');
+    const [inputValue, setInputValue] = useState(() => {
+      const value = defaultValue || propValue;
+      const inputVal = defaultInputValue || propInputValue;
+      return (Array.isArray(value) ? inputVal : value) || '';
+    });
+    const [selectedValues, setSelectedValues] = useState(() => {
+      const value = defaultValue || propValue;
+      if (!value) return [];
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: Updated
created_at: 2025-03-05T09:13:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -235,16 +335,33 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         onBlur?.(event);
         setOpen(false);
         if (autocomplete === 'automatic' && activeDescendant) {
-          const stringValue =
+          const activeValue =
             activeDescendant.value?.toString() ||
             /* istanbul ignore next: default value */ '';
-          setValue(stringValue);
-          setSelectedValue(stringValue);
-          onSelectionChange?.({
-            target: activeDescendant.element,
-            value: stringValue,
-            previousValue: value
-          });
+          setInputValue(activeValue);
+
+          if (!multiselect) {
+            setSelectedValues([activeValue]);
+            (onSelectionChange as OnSingleSelectionChange)?.({
+              target: activeDescendant.element,
+              value: activeValue,
+              previousValue: selectedValues[0]
+            });
+          } else {
+            const idx = selectedValues.indexOf(activeValue);
+            const newSelectedValues = selectedValues.filter(
+              (v) => v !== activeValue
+            );
+            if (idx === -1) {
+              newSelectedValues.push(activeValue);
+            }
+            setSelectedValues(newSelectedValues);
+            (onSelectionChange as OnMultiSelectionChange)?.({
+              target: activeDescendant.element,
+              value: newSelectedValues,
+              previousValue: selectedValues
+            });
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: Updated
created_at: 2025-03-05T09:13:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -376,26 +565,31 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
       [renderNoResults]
     );
 
-    const noMatchingOptions = !!value?.length && !matchingOptions.size && (
+    const noMatchingOptions = !!inputValue && !matchingOptions.size && (
       <NoMatchingOptions />
     );
 
     const comboboxListbox = (
+      // eslint-disable-next-line
+      // @ts-ignore
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: Updated
created_at: 2025-03-05T09:13:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -434,16 +636,54 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         </label>
         {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
         <TextFieldWrapper
-          className={classnames({ 'TextFieldWrapper--error': hasError })}
+          className={classnames({
+            'Combobox__TextFieldWrapper--multiselect': multiselect,
+            'TextFieldWrapper--error': hasError
+          })}
           // We're handling click here to open the listbox when the wrapping element is clicked,
           // there's already keyboard handlers to open the listbox on the input element
           onClick={handleInputClick}
         >
+          {multiselect &&
+            selectedValues.map((value, idx) => {
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: Updated
created_at: 2025-03-05T09:13:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -254,3 +254,24 @@
   padding: var(--space-smaller);
   text-align: center;
 }
+
+.Combobox__TextFieldWrapper--multiselect {
+  flex-wrap: wrap;
+  padding-right: var(--space-large);
+}
+
+.Combobox__TextFieldWrapper--multiselect input {
+  width: unset;
+}
path: packages/styles/combobox.css
user: login: chornonoh-vova
body: Updated
created_at: 2025-03-05T09:14:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -434,16 +636,54 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         </label>
         {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
         <TextFieldWrapper
-          className={classnames({ 'TextFieldWrapper--error': hasError })}
+          className={classnames({
+            'Combobox__TextFieldWrapper--multiselect': multiselect,
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: Updated
created_at: 2025-03-05T09:14:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,7 +116,39 @@ const ComboboxNoResults = ({
   );
 };
 
-const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
+function nextToFocus(
+  elements: HTMLElement[],
+  fallback: HTMLElement,
+  focusedIdx: number,
+  direction: number
+): HTMLElement | null {
+  const elems = elements.concat(fallback);
+  if (direction === -1) {
+    return null;
+  }
+
+  const style = getComputedStyle(elems[focusedIdx]);
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: Please take a look at the prev comment, this style getting is needed to obtain margins for position calculations on Up/Down arrow keypresses
created_at: 2025-03-05T09:16:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,7 +116,39 @@ const ComboboxNoResults = ({
   );
 };
 
-const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
+function nextToFocus(
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Yup, as we discussed over slack we don't need to account for visual positioning, just left/right order based on insertion order when navigating through the pills.
created_at: 2025-03-05T20:20:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -413,10 +612,22 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
     return (
       <div
         id={id}
-        className={classnames('Combobox', className)}
+        className={classnames(
+          'Combobox',
+          { 'Combobox--multiselect': multiselect },
+          className
+        )}
         ref={comboboxRef}
       >
-        {name && <input type="hidden" name={name} value={formValue} />}
+        {name &&
+          formValues.map((formValue) => (
+            <input
+              type="hidden"
+              key={formValue}
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: While not likely, it's possible a form value could be duplicated. We should try to make sure this key is always unique regardless of the form value.
created_at: 2025-03-05T20:26:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -439,11 +650,46 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
           // there's already keyboard handlers to open the listbox on the input element
           onClick={handleInputClick}
         >
+          {multiselect &&
+            selectedValues.map((value, index) => {
+              const refCallback = (elem: HTMLButtonElement | null) => {
+                if (elem) {
+                  pillsRef.current[index] = elem;
+                } else {
+                  pillsRef.current.splice(index, 1);
+                }
+              };
+
+              const handleClick = () =>
+                handleRemove(pillsRef.current[index], value);
+
+              const commonProps = {
+                ref: refCallback,
+                key: value,
+                className: 'Combobox__pill',
+                tabIndex: -1,
+                onClick: handleClick,
+                onKeyDown: handlePillKeyDown
+              };
+
+              return !disabled ? (
+                <TagButton
+                  label=""
+                  value={value || ''}
+                  icon="close"
+                  {...commonProps}
+                />
+              ) : (
+                <Button disabled={disabled} {...commonProps}>
+                  {value}
+                </Button>
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: These 2 components do not have similar styles. You can hold off on changes here until I confirm with @dequelabs/cauldron-design how we want these pills to appear.
created_at: 2025-03-05T20:28:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -439,11 +650,46 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
           // there's already keyboard handlers to open the listbox on the input element
           onClick={handleInputClick}
         >
+          {multiselect &&
+            selectedValues.map((value, index) => {
+              const refCallback = (elem: HTMLButtonElement | null) => {
+                if (elem) {
+                  pillsRef.current[index] = elem;
+                } else {
+                  pillsRef.current.splice(index, 1);
+                }
+              };
+
+              const handleClick = () =>
+                handleRemove(pillsRef.current[index], value);
+
+              const commonProps = {
+                ref: refCallback,
+                key: value,
+                className: 'Combobox__pill',
+                tabIndex: -1,
+                onClick: handleClick,
+                onKeyDown: handlePillKeyDown
+              };
+
+              return !disabled ? (
+                <TagButton
+                  label=""
+                  value={value || ''}
+                  icon="close"
+                  {...commonProps}
+                />
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: One thing that isn't accounted for in the issue description, is what the accessible name for this control should be. We need some way to be able to pass in the accessible name for these pills, so they read something along the lines of "remove {label}" for users of assistive technology.

Otherwise, right now this would just read as "{label}, button" and not really providing a descriptive enough context for AT users.
created_at: 2025-03-05T20:30:23Z
author_association: CONTRIBUTOR
path: packages/react/src/utils/closestElement.ts
user: login: scurker
body: Based on the conversation we had in slack, I don't think this is needed.
created_at: 2025-03-05T20:31:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -280,10 +401,17 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
 
         setOpen(true);
 
-        if (!open && arrowKeypress && selectedValue && isAutoComplete) {
+        if (
+          inputValue === '' &&
+          (backspaceKeypress || deleteKeypress || event.key === 'ArrowLeft')
+        ) {
+          pillsRef.current[pillsRef.current.length - 1]?.focus();
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: If we're using an array ref, we need to guard the ref by either:

- Ensuring we can't get an out of range value
- Only invoking `focus` when there's actually selected items

It looks like it might be possible to go negative here.
created_at: 2025-03-05T20:33:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -280,10 +401,17 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
 
         setOpen(true);
 
-        if (!open && arrowKeypress && selectedValue && isAutoComplete) {
+        if (
+          inputValue === '' &&
+          (backspaceKeypress || deleteKeypress || event.key === 'ArrowLeft')
+        ) {
+          pillsRef.current[pillsRef.current.length - 1]?.focus();
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: even if it will be negative, result of 

```
const array = [];
console.log(array[-1]);
```

<img width="382" alt="image" src="https://github.com/user-attachments/assets/7ef04fb0-c84f-4c5d-9709-59a096161bbf" />

Will be undefined
created_at: 2025-03-06T09:45:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -439,11 +650,46 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
           // there's already keyboard handlers to open the listbox on the input element
           onClick={handleInputClick}
         >
+          {multiselect &&
+            selectedValues.map((value, index) => {
+              const refCallback = (elem: HTMLButtonElement | null) => {
+                if (elem) {
+                  pillsRef.current[index] = elem;
+                } else {
+                  pillsRef.current.splice(index, 1);
+                }
+              };
+
+              const handleClick = () =>
+                handleRemove(pillsRef.current[index], value);
+
+              const commonProps = {
+                ref: refCallback,
+                key: value,
+                className: 'Combobox__pill',
+                tabIndex: -1,
+                onClick: handleClick,
+                onKeyDown: handlePillKeyDown
+              };
+
+              return !disabled ? (
+                <TagButton
+                  label=""
+                  value={value || ''}
+                  icon="close"
+                  {...commonProps}
+                />
+              ) : (
+                <Button disabled={disabled} {...commonProps}>
+                  {value}
+                </Button>
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: It is not possible to have TagButton without icon, that's why I'we added a conditional here to render button instead
created_at: 2025-03-06T09:50:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -439,11 +650,46 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
           // there's already keyboard handlers to open the listbox on the input element
           onClick={handleInputClick}
         >
+          {multiselect &&
+            selectedValues.map((value, index) => {
+              const refCallback = (elem: HTMLButtonElement | null) => {
+                if (elem) {
+                  pillsRef.current[index] = elem;
+                } else {
+                  pillsRef.current.splice(index, 1);
+                }
+              };
+
+              const handleClick = () =>
+                handleRemove(pillsRef.current[index], value);
+
+              const commonProps = {
+                ref: refCallback,
+                key: value,
+                className: 'Combobox__pill',
+                tabIndex: -1,
+                onClick: handleClick,
+                onKeyDown: handlePillKeyDown
+              };
+
+              return !disabled ? (
+                <TagButton
+                  label=""
+                  value={value || ''}
+                  icon="close"
+                  {...commonProps}
+                />
+              ) : (
+                <Button disabled={disabled} {...commonProps}>
+                  {value}
+                </Button>
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: Added a screenshot on how it would look like
created_at: 2025-03-07T08:05:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,24 +3,38 @@ import React, {
   useState,
   useRef,
   useCallback,
-  useEffect
+  useEffect,
+  useMemo
 } from 'react';
 import { createPortal } from 'react-dom';
-import classnames from 'classnames';
 import { useId } from 'react-id-generator';
-import { ContentNode } from '../../types';
+import classnames from 'classnames';
 import Listbox from '../Listbox';
 import ComboboxOption from './ComboboxOption';
 import { ComboboxProvider } from './ComboboxContext';
+import type { ContentNode } from '../../types';
 import type { ComboboxOptionState } from './ComboboxContext';
 import type { ComboboxValue } from './ComboboxOption';
 import type { ListboxOption } from '../Listbox/ListboxContext';
 import useSharedRef from '../../utils/useSharedRef';
 import { addIdRef } from '../../utils/idRefs';
 import TextFieldWrapper from '../internal/TextFieldWrapper';
+import { ListboxValue } from '../Listbox/ListboxOption';
+import ComboboxPill from './ComboboxPill';
 
 // Event Keys
-const [Enter, Escape, Home, End] = ['Enter', 'Escape', 'Home', 'End'];
+const [Enter, Escape, Home, End, Backspace, Delete] = [
+  'Enter',
+  'Escape',
+  'Home',
+  'End',
+  'Backspace',
+  'Delete'
+];
+
+const ArrowKeys = ['ArrowDown', 'ArrowUp'];
+
+const PillKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight'];
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Why duplicate values here? We should include the string values as part of the "event keys" and if you want to have a collection of event keys, just include the destructured variables names as part of that collection.

```tsx
const PillKeys = [Backspace, Delete, ArrowLeft, ArrowRight]
``` 
created_at: 2025-03-07T17:17:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,24 +3,38 @@ import React, {
   useState,
   useRef,
   useCallback,
-  useEffect
+  useEffect,
+  useMemo
 } from 'react';
 import { createPortal } from 'react-dom';
-import classnames from 'classnames';
 import { useId } from 'react-id-generator';
-import { ContentNode } from '../../types';
+import classnames from 'classnames';
 import Listbox from '../Listbox';
 import ComboboxOption from './ComboboxOption';
 import { ComboboxProvider } from './ComboboxContext';
+import type { ContentNode } from '../../types';
 import type { ComboboxOptionState } from './ComboboxContext';
 import type { ComboboxValue } from './ComboboxOption';
 import type { ListboxOption } from '../Listbox/ListboxContext';
 import useSharedRef from '../../utils/useSharedRef';
 import { addIdRef } from '../../utils/idRefs';
 import TextFieldWrapper from '../internal/TextFieldWrapper';
+import { ListboxValue } from '../Listbox/ListboxOption';
+import ComboboxPill from './ComboboxPill';
 
 // Event Keys
-const [Enter, Escape, Home, End] = ['Enter', 'Escape', 'Home', 'End'];
+const [Enter, Escape, Home, End, Backspace, Delete] = [
+  'Enter',
+  'Escape',
+  'Home',
+  'End',
+  'Backspace',
+  'Delete'
+];
+
+const ArrowKeys = ['ArrowDown', 'ArrowUp'];
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: How are arrow down/up being used? It's not immediately clear.
created_at: 2025-03-07T17:18:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,7 +116,39 @@ const ComboboxNoResults = ({
   );
 };
 
-const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
+function nextToFocus(
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: IMO this abstraction is pretty unclear still. Why is direction `2` and `3`? I have no idea what those numbers mean out of context. What elements are being passed in? Why do we need a fallback?

Take a look at how directional keys have been implemented in listbox for example: https://github.com/dequelabs/cauldron/blob/158b965d91a283df2647c43d5f52ef89da6297d4/packages/react/src/components/Listbox/Listbox.tsx#L215-L232

I'm not saying you can't abstract this logic out into its own function, but it should be fairly easy to read the code and have an understanding of what it does. In this instance, my recommendation would be ensure we're using naming that is self documenting, for example `left` and `right` as the directions. Secondly, since this function is only used in one location right now I'm not sure having this logic be abstracted is useful. It would be immediately more readable if we knew inline this was logic for pill arrow key functionality.
created_at: 2025-03-07T17:27:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -280,10 +401,17 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
 
         setOpen(true);
 
-        if (!open && arrowKeypress && selectedValue && isAutoComplete) {
+        if (
+          inputValue === '' &&
+          (backspaceKeypress || deleteKeypress || event.key === 'ArrowLeft')
+        ) {
+          pillsRef.current[pillsRef.current.length - 1]?.focus();
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: I still think it would be good to avoid this logic block entirely if we knew there weren't any pills present.
created_at: 2025-03-07T17:28:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -319,42 +447,115 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         onChange?.(event);
         // istanbul ignore else
         if (!isControlled) {
-          setValue(event.target.value);
+          setInputValue(event.target.value);
         }
       },
       [isControlled, onChange]
     );
 
-    const handleSelectionChange = useCallback(
-      ({
-        target,
-        value: listboxValue,
-        previousValue
-      }: Parameters<
-        Exclude<
-          React.ComponentProps<typeof Listbox>['onSelectionChange'],
-          undefined
-        >
-      >[0]) => {
-        const stringValue =
-          listboxValue?.toString() || /* istanbul ignore next */ '';
-
-        // istanbul ignore else
-        if (!isControlled) {
-          setValue(stringValue);
-        }
+    const handleRemove = useCallback(
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Nit: Handle remove of what? We could benefit from a more specific name, something like:

```suggestion
    const handleRemovePill = useCallback(
```
created_at: 2025-03-07T17:30:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -319,42 +447,115 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         onChange?.(event);
         // istanbul ignore else
         if (!isControlled) {
-          setValue(event.target.value);
+          setInputValue(event.target.value);
         }
       },
       [isControlled, onChange]
     );
 
-    const handleSelectionChange = useCallback(
-      ({
-        target,
-        value: listboxValue,
-        previousValue
-      }: Parameters<
-        Exclude<
-          React.ComponentProps<typeof Listbox>['onSelectionChange'],
-          undefined
-        >
-      >[0]) => {
-        const stringValue =
-          listboxValue?.toString() || /* istanbul ignore next */ '';
-
-        // istanbul ignore else
-        if (!isControlled) {
-          setValue(stringValue);
-        }
+    const handleRemove = useCallback(
+      (target: HTMLElement, value: ComboboxValue) => {
+        if (disabled) return;
 
-        setSelectedValue(stringValue);
+        const newSelectedValues = selectedValues.filter((v) => v !== value);
 
-        onSelectionChange?.({
+        setSelectedValues(newSelectedValues);
+        (onSelectionChange as OnMultiSelectionChange)?.({
           target,
-          value: stringValue,
-          previousValue: previousValue?.toString()
+          value: newSelectedValues,
+          previousValue: selectedValues
         });
+      },
+      [disabled, selectedValues, onSelectionChange]
+    );
+
+    const handlePillKeyDown = useCallback(
+      (event: React.KeyboardEvent<HTMLButtonElement>) => {
+        if (!PillKeys.includes(event.key)) {
+          return;
+        }
+
+        event.preventDefault();
+
+        const deleteKeypress = event.key === Delete;
+        const backspaceKeypress = event.key === Backspace;
+
+        const focusedIndex = pillsRef.current.findIndex(
+          (p) => document.activeElement === p
+        );
+
+        const focusedPill = pillsRef.current[focusedIndex];
+
+        if (deleteKeypress || backspaceKeypress) {
+          if (disabled) return;
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Nit: we generally don't use one line if statements.

```suggestion
          if (disabled) {
            return;
          }
```
created_at: 2025-03-07T17:31:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -319,42 +447,115 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
         onChange?.(event);
         // istanbul ignore else
         if (!isControlled) {
-          setValue(event.target.value);
+          setInputValue(event.target.value);
         }
       },
       [isControlled, onChange]
     );
 
-    const handleSelectionChange = useCallback(
-      ({
-        target,
-        value: listboxValue,
-        previousValue
-      }: Parameters<
-        Exclude<
-          React.ComponentProps<typeof Listbox>['onSelectionChange'],
-          undefined
-        >
-      >[0]) => {
-        const stringValue =
-          listboxValue?.toString() || /* istanbul ignore next */ '';
-
-        // istanbul ignore else
-        if (!isControlled) {
-          setValue(stringValue);
-        }
+    const handleRemove = useCallback(
+      (target: HTMLElement, value: ComboboxValue) => {
+        if (disabled) return;
 
-        setSelectedValue(stringValue);
+        const newSelectedValues = selectedValues.filter((v) => v !== value);
 
-        onSelectionChange?.({
+        setSelectedValues(newSelectedValues);
+        (onSelectionChange as OnMultiSelectionChange)?.({
           target,
-          value: stringValue,
-          previousValue: previousValue?.toString()
+          value: newSelectedValues,
+          previousValue: selectedValues
         });
+      },
+      [disabled, selectedValues, onSelectionChange]
+    );
+
+    const handlePillKeyDown = useCallback(
+      (event: React.KeyboardEvent<HTMLButtonElement>) => {
+        if (!PillKeys.includes(event.key)) {
+          return;
+        }
+
+        event.preventDefault();
+
+        const deleteKeypress = event.key === Delete;
+        const backspaceKeypress = event.key === Backspace;
+
+        const focusedIndex = pillsRef.current.findIndex(
+          (p) => document.activeElement === p
+        );
+
+        const focusedPill = pillsRef.current[focusedIndex];
+
+        if (deleteKeypress || backspaceKeypress) {
+          if (disabled) return;
+          const nextIndex = focusedIndex + 1;
+          if (nextIndex >= pillsRef.current.length) {
+            inputRef.current?.focus();
+          } else {
+            pillsRef.current[nextIndex].focus();
+          }
+          handleRemove(focusedPill, focusedPill.innerText);
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: I think I know why you put `innerText` here, but is there some way we could not rely on the inner text of the element and use the value instead?
created_at: 2025-03-07T17:33:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -439,11 +652,36 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
           // there's already keyboard handlers to open the listbox on the input element
           onClick={handleInputClick}
         >
+          {multiselect &&
+            selectedValues.map((value, index) => {
+              const refCallback = (elem: HTMLButtonElement | null) => {
+                if (elem) {
+                  pillsRef.current[index] = elem;
+                } else {
+                  pillsRef.current.splice(index, 1);
+                }
+              };
+
+              const handleClick = () =>
+                handleRemove(pillsRef.current[index], value);
+
+              return (
+                <ComboboxPill
+                  ref={refCallback}
+                  key={value}
+                  value={value}
+                  removeValueAriaLabel={removeValueAriaLabel}
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: A couple of things with this property. We don't usually call things `xxxAriaLabel`. Secondly, the way this is written is assuming [subject-verb-object](https://en.wikipedia.org/wiki/Subject%E2%80%93verb%E2%80%93object_word_order) word order by concatenating the label with the string. This might make it impossible to translate correctly in some languages.

My recommendation here would be to set an optional property `removeOptionLabel` on `ComboboxOption` that would allow for the user to override the default, e.g. we would concatenate `remove {label}` if this property wasn't set, otherwise we'd use the value from that property. This would allow for things to be translated correctly in product land.
created_at: 2025-03-07T17:44:53Z
author_association: CONTRIBUTOR
path: e2e/screenshots/combobox-multiselect-.png
user: login: scurker
body: I'm trying to make sure we have updated designs in the ticket (since the original designs are from a long time ago). But in the meantime, the height of the input fields do not match the height of the requirements:
![screenshot of multiselect combobox with smaller pills](https://github.com/user-attachments/assets/31db5a4c-d171-4d94-8aed-586de549511d)

created_at: 2025-03-07T17:52:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,24 +3,38 @@ import React, {
   useState,
   useRef,
   useCallback,
-  useEffect
+  useEffect,
+  useMemo
 } from 'react';
 import { createPortal } from 'react-dom';
-import classnames from 'classnames';
 import { useId } from 'react-id-generator';
-import { ContentNode } from '../../types';
+import classnames from 'classnames';
 import Listbox from '../Listbox';
 import ComboboxOption from './ComboboxOption';
 import { ComboboxProvider } from './ComboboxContext';
+import type { ContentNode } from '../../types';
 import type { ComboboxOptionState } from './ComboboxContext';
 import type { ComboboxValue } from './ComboboxOption';
 import type { ListboxOption } from '../Listbox/ListboxContext';
 import useSharedRef from '../../utils/useSharedRef';
 import { addIdRef } from '../../utils/idRefs';
 import TextFieldWrapper from '../internal/TextFieldWrapper';
+import { ListboxValue } from '../Listbox/ListboxOption';
+import ComboboxPill from './ComboboxPill';
 
 // Event Keys
-const [Enter, Escape, Home, End] = ['Enter', 'Escape', 'Home', 'End'];
+const [Enter, Escape, Home, End, Backspace, Delete] = [
+  'Enter',
+  'Escape',
+  'Home',
+  'End',
+  'Backspace',
+  'Delete'
+];
+
+const ArrowKeys = ['ArrowDown', 'ArrowUp'];
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: chornonoh-vova
body: Arrow up/down keys are used in a listbox (to navigate between items)
created_at: 2025-03-10T09:23:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,23 +185,27 @@ To set the initial value for uncontrolled Comboboxes, use `defaultValue`.
 Comboboxes can also be controlled, by setting the `value` prop and updating the value from event callbacks.
 
 <Note>
-  `onChange` and `onSelectionChange` have different behaviors, and special consideration needs to be made when `Combobox` is controlled. `onChange` gets called when the value of the input field changes from user input or selections while `onSelectionChange` is only called when a selection changes from the listbox.
+  `onChange` and `onSelectionChange` have different behaviors, and special
+  consideration needs to be made when `Combobox` is controlled. `onChange` gets
+  called when the value of the input field changes from user input or selections
+  while `onSelectionChange` is only called when a selection changes from the
+  listbox.
 </Note>
 
 ```jsx example
 function ComboboxControlledExample() {
   const [value, setValue] = useState('Peach');
-  const handleChange = ({ target }) => {
-    setValue(target.value);
+  const handleChange = (value) => {
path: docs/pages/components/Combobox.mdx
user: login: scurker
body: Wait, this looks like a breaking change. Was this documented incorrectly or is it actually a breaking change?
created_at: 2025-03-10T16:52:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -439,11 +658,36 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
           // there's already keyboard handlers to open the listbox on the input element
           onClick={handleInputClick}
         >
+          {multiselect &&
+            selectedValues.map((value, index) => {
+              const refCallback = (elem: HTMLButtonElement | null) => {
+                if (elem) {
+                  pillsRef.current[index] = elem;
+                } else {
+                  pillsRef.current.splice(index, 1);
+                }
+              };
+
+              const handleClick = () =>
+                handleRemovePill(pillsRef.current[index], value);
+
+              return (
+                <ComboboxPill
+                  ref={refCallback}
+                  key={value}
+                  value={value}
+                  removeValueLabel={removeOptionLabels[index]}
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Can we keep the naming of these properties consistent? I was trying to follow the path between removeValue/removeOption. My vote would be to have this be `removeOptionLabel` to be consistent everywhere.
created_at: 2025-03-10T16:58:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,23 +185,27 @@ To set the initial value for uncontrolled Comboboxes, use `defaultValue`.
 Comboboxes can also be controlled, by setting the `value` prop and updating the value from event callbacks.
 
 <Note>
-  `onChange` and `onSelectionChange` have different behaviors, and special consideration needs to be made when `Combobox` is controlled. `onChange` gets called when the value of the input field changes from user input or selections while `onSelectionChange` is only called when a selection changes from the listbox.
+  `onChange` and `onSelectionChange` have different behaviors, and special
+  consideration needs to be made when `Combobox` is controlled. `onChange` gets
+  called when the value of the input field changes from user input or selections
+  while `onSelectionChange` is only called when a selection changes from the
+  listbox.
 </Note>
 
 ```jsx example
 function ComboboxControlledExample() {
   const [value, setValue] = useState('Peach');
-  const handleChange = ({ target }) => {
-    setValue(target.value);
+  const handleChange = (value) => {
path: docs/pages/components/Combobox.mdx
user: login: chornonoh-vova
body: No, this is a mistake that I accidentally did, thanks for catching it
created_at: 2025-03-11T06:22:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -100,24 +152,39 @@ const Combobox = forwardRef<HTMLDivElement, ComboboxProps>(
       portal,
       inputRef: propInputRef = null,
       'aria-describedby': ariaDescribedby,
+      disabled = false,
       ...props
     },
     ref
   ): React.JSX.Element => {
-    const [value, setValue] = useState<string>(defaultValue || propValue || '');
     const [matchingOptions, setMatchingOptions] = useState<
       Map<HTMLElement, ComboboxOptionState>
     >(new Map());
-    const [selectedValue, setSelectedValue] = useState<string>(value || '');
-    const [formValue, setFormValue] = useState<string | undefined>('');
+    const [inputValue, setInputValue] = useState(() => {
+      const value = defaultValue || propValue;
+      const inputVal = defaultInputValue || propInputValue;
+      return (Array.isArray(value) ? inputVal : value) || '';
+    });
+    const [selectedValues, setSelectedValues] = useState(() => {
+      const value = defaultValue || propValue;
+      if (!value) {
+        return [];
+      }
+      return Array.isArray(value) ? value : [value];
+    });
+    const [removeOptionLabels, setRemoveOptionLabels] = useState<string[]>([]);
+    const [formValues, setFormValues] = useState<ComboboxValue[]>([]);
     const [open, setOpen] = useState(false);
     const [activeDescendant, setActiveDescendant] =
path: packages/react/src/components/Combobox/Combobox.tsx
user: login: scurker
body: Something funky is going on with `setActiveDescendant` when the multiselect listbox stays open that I don't think we've addressed yet. This indicates that we still need a code change + a test to verify we have the correct active descendent when selecting multiple options.

The steps I'm following are:
- Select an option, the screen reader then should announce that option is selected
- Navigate to a different option then selected it, the screen reader should announce that new option is now selected

What I'm actually seeing is that `aria-activedescendant` is _always_ getting set to the first matching options causing only that option to ever be announced. I suspect this is occurring in the `useEffect` function around line `250` and `245` where we're pulling the element from the list of options. 
created_at: 2025-03-11T20:12:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -277,118 +297,254 @@ When autocomplete is set to "automatic" the listbox will provide a filtered list
 </FieldWrap>
 ```
 
+## Multiselect
+
+Combobox can also support multiple selection of options.
+
+### Multiselect Uncontrolled
+
+```jsx example
+<FieldWrap>
+  <Combobox label="Multiselect Combobox" name="multiselect" multiselect>
+    <ComboboxOption value="Apple">🍎 Apple</ComboboxOption>
+    <ComboboxOption value="Banana">🍌 Banana</ComboboxOption>
+    <ComboboxOption value="Cucumber">🥒 Cucumber</ComboboxOption>
+    <ComboboxOption value="Orange">🍊 Orange</ComboboxOption>
+    <ComboboxOption value="Peach">🍑 Peach</ComboboxOption>
+    <ComboboxOption value="Pear">🍐 Pear</ComboboxOption>
+  </Combobox>
+</FieldWrap>
+```
+
+### Multiselect disabled
path: docs/pages/components/Combobox.mdx
user: login: scurker
body: We should have consistent casing of our headings

```suggestion
### Multiselect Disabled
```
created_at: 2025-03-12T18:45:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: aws-amplify-us-east-1[bot]
created_at: 2025-02-28T09:38:27Z
body: This pull request is automatically being deployed by Amplify Hosting ([learn more](https://aws.amazon.com/amplify/hosting/)).<br/><br/>Access this pull request here: https://pr-1816.d15792l1n26ww3.amplifyapp.com
author_association: NONE
user: login: shawnsharpDQ
created_at: 2025-03-11T22:37:27Z
body: I am still able to interact with the caret icon to open the menu, but not close, when the component is disabled.
Otherwise, everything else is good to go.
author_association: NONE
user: login: scurker
created_at: 2025-03-13T15:52:13Z
body: Nice work @chornonoh-vova! We've needed this for a long time and this will help solve some long standing ux issues
author_association: CONTRIBUTOR