Components
Menu - New!
Menu is an ecosystem of components that provide a structured and accessible way to create menus.
Using the Downshift API
The Menu components extend the Downshift hook APIs, accepting these props as component props and passing them through to the hooks internally. By leveraging Downshift's hooks (useSelect
, useCombobox
, and useMultipleSelect
), the components automatically handle accessibility, keyboard navigation, and selection logic.
Downshift Hooks Used by Menu Components
The Menu components use different Downshift hooks depending on their features:
-
useSelect
Used by the standard Menu component to manage single selection, keyboard navigation, and menu visibility. This hook is responsible for handling ARIA attributes and accessibility for a typical dropdown menu.
Note: The ActionMenu usesuseSelect
, but it does not accept props related to selection state such asselectedItem
. -
useCombobox
Used by the Autocomplete variant of the Menu component. This hook enables input-based filtering and searching of menu items, combining input and menu behaviors.
Note: Only used when the menu is triggered by a form element (such as an input field); filter menus with the input embedded inside the menu itself do not useuseCombobox
. -
useMultipleSelect
Used in addition to eitheruseSelect
oruseCombobox
when the Menu component supports multiple selection. This hook augments the single-selection hooks to manage the state and accessibility for selecting multiple items from the menu, handling keyboard navigation and ARIA attributes for multi-select scenarios.
These hooks are chosen to provide robust accessibility, keyboard support, and consistent behavior across browsers and assistive technologies.
For more detailed information on these hooks and their usage, refer to the official Downshift documentation:
useSelect
documentation | GitHub README foruseSelect
useCombobox
documentation | GitHub README foruseCombobox
useMultipleSelect
documentation | GitHub README foruseMultipleSelect
Handling Overlapping Props When Using Multiple Downshift Hooks
Some menu components use both useSelect
and useMultipleSelect
hooks to support advanced selection patterns. Because these hooks have some overlapping prop names (such as stateReducer
), our components provide a mapping system to avoid conflicts and ensure the correct prop is passed to each hook.
Currently, there are three props with special mapped names for multiple selection:
multiSelectStateReducer
→ passed touseMultipleSelect
asstateReducer
onMultiSelectStateChange
→ passed touseMultipleSelect
asonStateChange
getA11yMultiStatusMessage
→ passed touseMultipleSelect
asgetA11yStatusMessage
If you need to customize multiple selection behavior, use these props. All other props should be passed as documented for each hook.
For usage examples and more details, see the documentation for each menu component:
Tracking state changes using Downshift
To track when the menu state changes (such as opening or closing), you can use the onStateChange
prop, individual state change callbacks like onIsOpenChange
, or provide a stateReducer
.
Using onStateChange
to track changes
The onStateChange
prop in Downshift is a flexible callback that allows you to respond to various state changes within the menu component.
This callback receives an object describing the changes and the current state, enabling you to track events such as when the menu opens or closes,
when an item is highlighted, or when the selected item changes.
You can use onStateChange
to perform side effects or update other parts of your application in response to these state transitions.
For example, to specifically watch for the menu opening or closing, you can check the isOpen
property in the changes object:
import { ActionMenu } from '@sproutsocial/seeds-react-menu'
<Box display="flex" justifyContent="center" alignItems="center" height="100%"><ActionMenumenuToggleElement={<MenuToggleButton>Open Menu</MenuToggleButton>}onStateChange={(changes) => {if (changes.hasOwnProperty('isOpen')) {// The menu open state has changedconsole.log('Menu is now', changes.isOpen ? 'open' : 'closed');}}}><MenuContent><MenuItem id="item-one">Item 1</MenuItem><MenuItem id="item-two">Item 2</MenuItem><MenuItem id="item-three">Item 3</MenuItem></MenuContent></ActionMenu></Box>
This approach allows you to react to a wide range of user interactions and state updates within the Downshift component. Each hook has a slightly different shape, so refer to the docs directly to see what is available for each:
Downshift Callbacks for Individual State Parameters
In addition to the general onStateChange
callback, Downshift provides specific "on change" callback props for individual state parameters. These include callbacks such as onIsOpenChange
, onHighlightedIndexChange
, onSelectedItemChange
, and others, depending on the hook you are using.
These callbacks are called only when their corresponding state value changes, making it easier to respond to specific events without having to inspect the changes object in onStateChange
. For example, you can use onIsOpenChange
to run code whenever the menu opens or closes:
import { ActionMenu } from '@sproutsocial/seeds-react-menu'
<Box display="flex" justifyContent="center" alignItems="center" height="100%"><ActionMenumenuToggleElement={<MenuToggleButton>Open Menu</MenuToggleButton>}onIsOpenChange={(changes) => {// changes.isOpen is guaranteed to be presentconsole.log('Menu is now', changes.isOpen ? 'open' : 'closed');}}><MenuContent><MenuItem id="item-one">Item 1</MenuItem><MenuItem id="item-two">Item 2</MenuItem><MenuItem id="item-three">Item 3</MenuItem></MenuContent></ActionMenu></Box>
Customizing State Transitions with stateReducer
Downshift provides a powerful stateReducer
prop that allows you to intercept and modify state changes before they are applied. This is useful when you want to customize or override the default behavior of the menu components, such as preventing the menu from closing under certain conditions, or implementing custom selection logic.
The stateReducer
function receives three arguments:
state
: The current state of the component.actionAndChanges
: An object containing the type of action and the proposed changes.actionAndChanges.type
: The type of state change (e.g., item selection, menu open/close).actionAndChanges.changes
: The changes that would be applied by default.
You return a new changes object, which will be used as the next state. This gives you full control over how the menu responds to user interactions.
Why use stateReducer
instead of onStateChange
?
onStateChange
is a side-effect callback: it lets you react to state changes after they happen, but does not let you prevent or alter them.stateReducer
lets you intercept and modify state transitions before they are applied, enabling advanced customizations.
See the documentation for more information on how to use stateReducer
:
Example: Preventing the menu from closing when an item is selected
Real-world usage:
In your app code, you would typically write yourstateReducer
like this:
import { useSelect } from "downshift";
const stateReducer = (state, actionAndChanges) => { const { type, changes } = actionAndChanges; // RECOMMENDATION: To track menu open/close state in a real app, // log when the isOpen property changes, as any action can cause the menu to open or close. // Note: This logs the proposed state from changes.isOpen, // which may differ from the actual state if you override isOpen in your reducer. if (Object.prototype.hasOwnProperty.call(changes, "isOpen")) { console.log("Menu is now", changes.isOpen ? "open" : "closed"); } switch (type) { // Prevent the menu from closing when an item is selected case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter: case useSelect.stateChangeTypes.ItemClick: return { ...changes, isOpen: state.isOpen, // keep the menu open highlightedIndex: state.highlightedIndex, // keep the current highlight }; default: return changes; // use default changes for all other actions }};
Live Demo (playground workaround):
The live playground cannot access static properties on hooks, so we define the state change types manually:
<Box display="flex" justifyContent="center" alignItems="center" height="100%"><ActionMenumenuToggleElement={<MenuToggleButton>Open Menu</MenuToggleButton>}stateReducer={(state, actionAndChanges) => {// NOTE: In a real app, use useSelect.stateChangeTypes.// This is only necessary here because the live playground cannot access static properties on hooks.const stateChangeTypes = {ToggleButtonKeyDownEnter: '__togglebutton_keydown_enter__',ItemClick: '__item_click__',};const { type, changes } = actionAndChanges;// Log when the menu open state changes (this logs the proposed state from changes.isOpen,// which may differ from the actual state if you override isOpen in your reducer)if (Object.prototype.hasOwnProperty.call(changes, "isOpen")) {console.log("Menu is now", changes.isOpen ? "open" : "closed");}switch (type) {case stateChangeTypes.ToggleButtonKeyDownEnter:case stateChangeTypes.ItemClick:return {...changes,isOpen: state.isOpen,highlightedIndex: state.highlightedIndex,};default:return changes;}}}><MenuContent><MenuItem id="item-one">Item 1</MenuItem><MenuItem id="item-two">Item 2</MenuItem><MenuItem id="item-three">Item 3</MenuItem></MenuContent></ActionMenu></Box>
Looking for more examples or common patterns?
See our documentation for specific menu components and their common use cases:
Each page includes practical examples and tips for the most common scenarios.