diff --git a/src/Combobox/Combobox.js b/src/Combobox/Combobox.js
new file mode 100644
index 00000000..0311d680
--- /dev/null
+++ b/src/Combobox/Combobox.js
@@ -0,0 +1,247 @@
+import React, {useEffect, useRef, createContext} from 'react';
+import PropTypes from 'prop-types';
+import {useItemList} from 'use-item-list';
+
+import {KEY_CODES} from '../constants';
+import {mergeCallbacks} from '../utils';
+
+import useEventListener from '../useEventListener';
+import useOnClickOutside from '../useOnClickOutside';
+import usePopover from '../usePopover';
+import usePopoverState from '../usePopoverState';
+import useUniqueId from '../useUniqueId';
+
+import Portal from '../Portal';
+import Status from '../Status';
+import VisuallyHidden from '../VisuallyHidden';
+
+function defaultGetA11yStatusMessage({resultCount, inputValue}) {
+ if (inputValue?.length && !resultCount) {
+ return 'No suggestions available';
+ }
+
+ if (resultCount > 0) {
+ return [
+ resultCount,
+ 'suggestions available.',
+ 'Use up and down arrow keys to navigate. Press Enter key to select.',
+ ].join(' ');
+ }
+
+ return '';
+}
+
+const ComboboxContext = createContext();
+
+function Combobox({
+ id: idProp,
+ inputValue,
+ resultCount,
+ onSelect,
+ getStatusMessage,
+ shouldShowStatusMessage,
+ children,
+}) {
+ const inputRef = useRef();
+ const popoverRef = useRef();
+ const id = useUniqueId(idProp);
+ const statusMessage = getStatusMessage({resultCount, inputValue});
+
+ const popover = usePopover({
+ ref: popoverRef,
+ referenceRef: inputRef,
+ placement: 'bottom-start',
+ positionFixed: false,
+ adaptivePositioning: true,
+ matchReferenceWidth: true,
+ arrowSize: 0,
+ });
+
+ const {isOpen, open, close, toggle} = usePopoverState({
+ onOpen: onOpenMenu,
+ });
+
+ const canMenuBeOpen =
+ (shouldShowStatusMessage && Boolean(statusMessage)) ||
+ Boolean(resultCount);
+ const isMenuOpen = canMenuBeOpen && isOpen;
+
+ const itemList = useItemList({
+ id,
+ selected: inputValue,
+ initialHighlightedIndex: 0,
+ onSelect: handleSelect,
+ });
+
+ const shouldOpenOnValueChange = useRef(true);
+
+ function handleSelect(selectedItem) {
+ // Normally, the dropdown menu is opened whenever
+ // the input value changes. However we don't want that
+ // if the value was changed as the result of a selection
+ // as it would re-open the menu
+ shouldOpenOnValueChange.current = false;
+ onSelect(selectedItem);
+ close();
+ }
+
+ useEffect(() => {
+ if (inputValue.length && shouldOpenOnValueChange.current) {
+ open();
+ }
+ shouldOpenOnValueChange.current = true;
+ }, [inputValue]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ function onOpenMenu() {
+ // itemList.setHighlightedItem(0);
+ }
+
+ function handleGlobalMenuKeyEvents(event) {
+ if (event.keyCode === KEY_CODES.ESC) {
+ event.preventDefault();
+ close();
+ }
+ }
+
+ function handleInputKeyEvents(event) {
+ if (shouldShowStatusMessage) {
+ return;
+ }
+
+ if (event.keyCode === KEY_CODES.ARROW_UP) {
+ event.preventDefault();
+ if (isMenuOpen) {
+ itemList.moveHighlightedItem(-1);
+ } else {
+ open();
+ }
+ }
+ if (event.keyCode === KEY_CODES.ARROW_DOWN) {
+ event.preventDefault();
+ if (isMenuOpen) {
+ itemList.moveHighlightedItem(1);
+ } else {
+ open();
+ }
+ }
+ if (event.keyCode === KEY_CODES.TAB) {
+ if (isMenuOpen) {
+ itemList.selectHighlightedItem();
+ close();
+ }
+ }
+ if (event.keyCode === KEY_CODES.ENTER) {
+ event.preventDefault();
+ if (isMenuOpen) {
+ close();
+ itemList.selectHighlightedItem();
+ }
+ }
+ }
+
+ // Handle global keyboard events when the menu is open
+ useEventListener('keydown', handleGlobalMenuKeyEvents, {
+ isEnabled: isMenuOpen,
+ });
+
+ // Close the menu when clicking outside of the input
+ useOnClickOutside([inputRef, popoverRef], close, isMenuOpen);
+
+ const highlightedItemId = itemList.useHighlightedItemId();
+
+ const getInputProps = ({onFocus, onKeyDown}) => ({
+ id,
+ value: inputValue,
+ role: 'combobox',
+ 'aria-controls': itemList.listId,
+ 'aria-expanded': isMenuOpen ? 'true' : 'false',
+ 'aria-haspopup': 'true',
+ 'aria-activedescendant': isMenuOpen ? highlightedItemId : null,
+ 'aria-autocomplete': 'list',
+ autoComplete: 'off',
+ spellCheck: 'false',
+ onKeyDown: mergeCallbacks(onKeyDown, handleInputKeyEvents),
+ onFocus: mergeCallbacks(onFocus, () => {
+ if (!isMenuOpen) {
+ open();
+ }
+ }),
+ });
+
+ return (
+
+ {children}
+
+
+ {isMenuOpen && statusMessage}
+
+
+
+ );
+}
+
+Combobox.defaultProps = {
+ shouldShowStatusMessage: false,
+ getStatusMessage: defaultGetA11yStatusMessage,
+};
+
+Combobox.propTypes = {
+ /**
+ * Unique ID for the combobox that will be passed to the input element
+ * and used as the basis for the IDs of other associated elements.
+ * Will be automatically generated if not provided.
+ */
+ id: PropTypes.string,
+ /**
+ * The value of the input field. Will also be passed on to the wrapped
+ * input field automatically
+ */
+ inputValue: PropTypes.string.isRequired,
+ /**
+ * The number of rendered autocomplete suggestions, i.e. menu items
+ * that are rendered using the ComboboxMenuItem component
+ */
+ resultCount: PropTypes.number.isRequired,
+ /**
+ * Callback to provide custom status messages which will be used
+ * for screenreader announcements and in the UI when the `shouldShowStatusMessage`
+ * option is enabled.
+ * The passed function is called with an object containing `resultCount` and
+ * `inputValue`, and should return suitable strings to notify users of the number
+ * of results and how to navigate the combobox.
+ */
+ getStatusMessage: PropTypes.func,
+ /**
+ * Set this to true to make the status message returned from `getStatusMessage`
+ * visible in the dropdown menu, e.g. to display "No suggestions available" when
+ * there are no results. Never set this to `true` when there _are_ results, and always
+ * make it dependent on e.g. the number of results or the length of `inputValue`,
+ * as it will replace the options in the menu.
+ */
+ shouldShowStatusMessage: PropTypes.bool,
+ /**
+ * Function to be called when a menu option was selected by the user.
+ * Will be called with an object containing the option's `value` and `text`.
+ */
+ onSelect: PropTypes.func.isRequired,
+ children: PropTypes.node.isRequired,
+};
+
+export {ComboboxContext};
+
+// @component
+export default Combobox;
diff --git a/src/Combobox/ComboboxInput.js b/src/Combobox/ComboboxInput.js
new file mode 100644
index 00000000..560cbae9
--- /dev/null
+++ b/src/Combobox/ComboboxInput.js
@@ -0,0 +1,45 @@
+import React, {useContext, forwardRef} from 'react';
+import useMergedRefs from '../useMergedRefs';
+import {ComboboxContext} from './Combobox';
+
+const ComboboxInput = forwardRef(function ComboboxInput(props, outerRef) {
+ const {
+ as: Component = 'input',
+ forwardedAs,
+ refKey = 'ref',
+ onFocus,
+ onKeyDown,
+ ...otherProps
+ } = props;
+
+ const {
+ getInputProps,
+ popover: {setReferenceRef},
+ } = useContext(ComboboxContext);
+
+ // If there's a custom refKey, use that as the popover target
+ // and pass the regular ref on as usual
+ // Otherwise merge the refs and attach them to the default one
+ const mergedRefs = useMergedRefs([setReferenceRef, outerRef]);
+ const mergedCustomRefs = useMergedRefs([
+ setReferenceRef,
+ otherProps[refKey],
+ ]);
+ const refs = {
+ ref: refKey !== 'ref' ? outerRef : mergedRefs,
+ };
+ if (refKey !== 'ref') {
+ refs[refKey] = mergedCustomRefs;
+ }
+
+ return (
+
+ );
+});
+
+export default ComboboxInput;
diff --git a/src/Combobox/ComboboxMenu.js b/src/Combobox/ComboboxMenu.js
new file mode 100644
index 00000000..076b1b0a
--- /dev/null
+++ b/src/Combobox/ComboboxMenu.js
@@ -0,0 +1,53 @@
+import React, {useContext} from 'react';
+import PropTypes from 'prop-types';
+
+import * as MenuListUI from '../MenuList';
+import Box from '../Box';
+import PopoverCard from '../PopoverCard';
+
+import {ComboboxContext} from './Combobox';
+
+function ComboboxMenu({children, 'aria-label': ariaLabel}) {
+ const {
+ popover,
+ itemList,
+ statusMessage,
+ shouldShowStatusMessage,
+ } = useContext(ComboboxContext);
+
+ return (
+
+ {shouldShowStatusMessage ? (
+
+ {statusMessage}
+
+ ) : (
+
+ {children}
+
+ )}
+
+ );
+}
+
+ComboboxMenu.propTypes = {
+ /**
+ * A label describing the contents of the dropdown list
+ */
+ 'aria-label': PropTypes.string.isRequired,
+};
+
+export default ComboboxMenu;
diff --git a/src/Combobox/ComboboxMenuItem.js b/src/Combobox/ComboboxMenuItem.js
new file mode 100644
index 00000000..833d42af
--- /dev/null
+++ b/src/Combobox/ComboboxMenuItem.js
@@ -0,0 +1,48 @@
+import React, {useRef, useContext} from 'react';
+
+import * as MenuListUI from '../MenuList';
+
+import {ComboboxContext} from './Combobox';
+
+function ComboboxMenuItem({value, icon, isDisabled, children}) {
+ const itemRef = useRef();
+ const {itemList} = useContext(ComboboxContext);
+ const {
+ id,
+ select,
+ selected,
+ highlight,
+ useHighlighted,
+ clearHighlightedItem,
+ } = itemList.useItem({
+ ref: itemRef,
+ value: value ?? children,
+ text: children,
+ disabled: isDisabled,
+ });
+
+ return (
+
+
+ {icon && }
+ {children}
+
+
+ );
+}
+
+export default ComboboxMenuItem;
diff --git a/src/Combobox/README.mdx b/src/Combobox/README.mdx
new file mode 100644
index 00000000..35e46851
--- /dev/null
+++ b/src/Combobox/README.mdx
@@ -0,0 +1,97 @@
+---
+name: Combobox
+menu: Components
+---
+
+import {Playground, Props} from 'docz';
+import {Combobox, ComboboxInput, ComboboxMenu, ComboboxMenuItem} from './';
+
+# Combobox
+
+An accessible combobox component that can be used with any input element.
+
+Implemented based on the recommendations from the [WAI-ARIA Authoring Practices 1.2 working draft](https://www.w3.org/TR/wai-aria-practices-1.2/#combobox).
+
+## Examples
+
+
+ {() => {
+ const defaultOptions = [
+ 'Apple',
+ 'Apricot',
+ 'Banana',
+ 'Blueberry',
+ 'Cherry',
+ 'Fig',
+ 'Feijoa',
+ 'Gooseberry',
+ 'Jackfruit',
+ 'Jujube',
+ 'Kiwi',
+ 'Kumquat',
+ 'Lingonberry',
+ 'Lychee',
+ 'Mandarin',
+ 'Mango',
+ 'Melon',
+ 'Nectarine',
+ 'Orange',
+ 'Papaya',
+ 'Passion Fruit',
+ 'Pear',
+ 'Persimmon',
+ 'Physalis',
+ 'Pineapple',
+ 'Plum',
+ 'Pomegranate',
+ 'Pomelo',
+ 'Raspberry',
+ 'Starfruit',
+ 'Strawberry',
+ 'Watermelon',
+ 'Wumpa',
+ ];
+ const inputRef = React.useRef();
+ const [value, setValue] = React.useState('');
+ const [options, setOptions] = React.useState(defaultOptions);
+ React.useEffect(() => {
+ const filteredOptions = defaultOptions.filter(option =>
+ option.toLowerCase().includes(value.toLowerCase())
+ );
+ setOptions(filteredOptions);
+ }, [value]);
+ return (
+
+ );
+ }}
+
+
+## Props
+
+
diff --git a/src/Combobox/index.js b/src/Combobox/index.js
new file mode 100644
index 00000000..d5d51362
--- /dev/null
+++ b/src/Combobox/index.js
@@ -0,0 +1,4 @@
+export {default as Combobox, ComboboxContext} from './Combobox';
+export {default as ComboboxInput} from './ComboboxInput';
+export {default as ComboboxMenu} from './ComboboxMenu';
+export {default as ComboboxMenuItem} from './ComboboxMenuItem';