diff --git a/apps/www/src/content/docs/components/command/demo.ts b/apps/www/src/content/docs/components/command/demo.ts
index 9a54b8b07..4f16a96a5 100644
--- a/apps/www/src/content/docs/components/command/demo.ts
+++ b/apps/www/src/content/docs/components/command/demo.ts
@@ -233,6 +233,9 @@ export const itemsDemo = {
)}
+
+ {(count) => (count === 1 ? '1 result' : \`\${count} results\`)}
+
diff --git a/apps/www/src/content/docs/components/command/index.mdx b/apps/www/src/content/docs/components/command/index.mdx
index 89159adf4..5074fd22c 100644
--- a/apps/www/src/content/docs/components/command/index.mdx
+++ b/apps/www/src/content/docs/components/command/index.mdx
@@ -36,6 +36,7 @@ import { Command } from "@raystack/apsara";
+
@@ -110,10 +111,16 @@ Scrollable container for groups and items. Renders with `role="listbox"`.
### Empty
-Rendered when no items are visible (all filtered out, or the list is empty). Automatically hidden when the list contains at least one item.
+Rendered when no items are visible (all filtered out, or the list is empty). Automatically hidden when the list contains at least one item. Built on [Base UI Autocomplete.Empty](https://base-ui.com/react/components/autocomplete).
+### Status
+
+A visually hidden live region. Screen readers announce changes to its children. Place it anywhere inside `Command`. A function child receives the number of matching items. The count needs the `items` prop. Without it, the function receives `null`. Built on [Base UI Autocomplete.Status](https://base-ui.com/react/components/autocomplete).
+
+
+
### Item
A selectable entry. When `value` is omitted and `children` is a string, the string is used as the value. `onClick` fires on pointer click **and** on keyboard `Enter` when the item is highlighted. Pass a [`Kbd`](/docs/components/kbd) as `trailingIcon` to show a keyboard hint, and use the `ghost` variant so the keys sit on the item's own background.
@@ -161,6 +168,7 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test
| `command-input` | The `` element itself |
| `command-content` | The `Command.Content` list |
| `command-empty` | `Command.Empty` |
+| `command-status` | `Command.Status` |
| `command-group` | `Command.Group` wrapper |
| `command-label` | `Command.Label` |
| `command-item` | Each `Command.Item` |
@@ -177,5 +185,6 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test
- Follows the [WAI-ARIA Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
- Input uses `role="combobox"`, list uses `role="listbox"`, items use `role="option"`.
- Keyboard: `ArrowDown` / `ArrowUp` move the highlight, `Enter` activates the highlighted item, `Escape` closes the dialog.
+- `Command.Status` is a polite live region. Use it to announce the number of matching items.
- Disabled items expose `aria-disabled="true"`. The arrow keys still highlight them, and `Enter` does not activate them.
- When used inside `Command.Dialog`, focus is trapped in the dialog and returned to the trigger on close.
diff --git a/apps/www/src/content/docs/components/command/props.ts b/apps/www/src/content/docs/components/command/props.ts
index d98976187..dd8659dd6 100644
--- a/apps/www/src/content/docs/components/command/props.ts
+++ b/apps/www/src/content/docs/components/command/props.ts
@@ -83,6 +83,17 @@ export interface CommandContentProps {
}
export interface CommandEmptyProps {
+ /** Content to show when no items match. */
+ children?: React.ReactNode;
+
+ /** Additional CSS class names. */
+ className?: string;
+}
+
+export interface CommandStatusProps {
+ /** Text to announce. A function receives the number of matching items, or `null` without the `items` prop. */
+ children?: React.ReactNode | ((count: number | null) => React.ReactNode);
+
/** Additional CSS class names. */
className?: string;
}
diff --git a/packages/react/components/command/__tests__/command.test.tsx b/packages/react/components/command/__tests__/command.test.tsx
index 5c53793f3..51b97d7a6 100644
--- a/packages/react/components/command/__tests__/command.test.tsx
+++ b/packages/react/components/command/__tests__/command.test.tsx
@@ -133,6 +133,52 @@ describe('Command', () => {
});
});
+ describe('Status', () => {
+ it('announces the match count with the items prop', async () => {
+ const user = userEvent.setup();
+ const items = ['Calendar', 'Calculator', 'Profile'];
+ const { container } = render(
+
+
+
+ {(item: string) => (
+
+ {item}
+
+ )}
+
+ {count => `${count} results`}
+
+ );
+ const status = container.querySelector('[data-slot="command-status"]');
+ expect(status).toHaveTextContent('3 results');
+
+ await user.type(screen.getByRole('combobox'), 'cal');
+ await waitFor(() => expect(status).toHaveTextContent(/^2 results$/));
+ });
+
+ it('passes null to a function child without the items prop', () => {
+ const { container } = render(
+
+
+
+ {ITEMS.map(item => (
+
+ {item.label}
+
+ ))}
+
+
+ {count => (count === null ? 'No count' : `${count} results`)}
+
+
+ );
+ expect(
+ container.querySelector('[data-slot="command-status"]')
+ ).toHaveTextContent('No count');
+ });
+ });
+
describe('Auto-search disabled when items prop is passed', () => {
it('does not apply custom filtering to CommandItem when items is provided', async () => {
const user = userEvent.setup();
diff --git a/packages/react/components/command/__tests__/data-slots.test.tsx b/packages/react/components/command/__tests__/data-slots.test.tsx
index 5b2b1c4cf..004a0df1c 100644
--- a/packages/react/components/command/__tests__/data-slots.test.tsx
+++ b/packages/react/components/command/__tests__/data-slots.test.tsx
@@ -32,6 +32,7 @@ const BasicCommand = () => (
Disabled item
+
);
@@ -50,7 +51,8 @@ describe('Command data-slot contract', () => {
'command-item-leading-icon',
'command-item-label',
'command-item-trailing-icon',
- 'command-separator'
+ 'command-separator',
+ 'command-status'
]);
});
diff --git a/packages/react/components/command/command-empty.tsx b/packages/react/components/command/command-empty.tsx
index 4dc4e28f4..4e25564cf 100644
--- a/packages/react/components/command/command-empty.tsx
+++ b/packages/react/components/command/command-empty.tsx
@@ -1,19 +1,16 @@
'use client';
+import { Autocomplete as AutocompletePrimitive } from '@base-ui/react/autocomplete';
import { cx } from 'class-variance-authority';
-import { type ComponentProps } from 'react';
+import { type ReactNode } from 'react';
import styles from './command.module.css';
+import { useCommandContext } from './command-root';
-export interface CommandEmptyProps extends ComponentProps<'div'> {}
+export type CommandEmptyProps = AutocompletePrimitive.Empty.Props;
-export const CommandEmpty = ({
- ref,
- className,
- ...props
-}: CommandEmptyProps) => {
+export const CommandEmpty = ({ className, ...props }: CommandEmptyProps) => {
return (
-
+ typeof item === 'object' &&
+ item !== null &&
+ Array.isArray((item as { items?: unknown }).items);
+
+const countItems = (items: readonly unknown[]) =>
+ items.reduce(
+ (count, item) => count + (isGroup(item) ? item.items.length : 1),
+ 0
+ );
+
+export interface CommandStatusProps
+ extends Omit {
+ /** Text to announce. A function receives the number of matching items, or `null` without the `items` prop. */
+ children?: ReactNode | ((count: number | null) => ReactNode);
+}
+
+export const CommandStatus = ({
+ className,
+ children,
+ ...props
+}: CommandStatusProps) => {
+ const { hasItems } = useCommandContext();
+ const filteredItems = AutocompletePrimitive.useFilteredItems();
+
+ const content =
+ typeof children === 'function'
+ ? children(hasItems ? countItems(filteredItems) : null)
+ : children;
+
+ return (
+
+ {content}
+
+ );
+};
+
+CommandStatus.displayName = 'Command.Status';
diff --git a/packages/react/components/command/command.module.css b/packages/react/components/command/command.module.css
index 26f41997d..42cbc4079 100644
--- a/packages/react/components/command/command.module.css
+++ b/packages/react/components/command/command.module.css
@@ -117,6 +117,18 @@
display: none;
}
+.status {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+ border: 0;
+}
+
.group {
display: flex;
flex-direction: column;
diff --git a/packages/react/components/command/command.tsx b/packages/react/components/command/command.tsx
index a3e197db4..9c951f2cf 100644
--- a/packages/react/components/command/command.tsx
+++ b/packages/react/components/command/command.tsx
@@ -4,7 +4,7 @@ import {
CommandDialogContent,
CommandDialogTrigger
} from './command-dialog';
-import { CommandEmpty } from './command-empty';
+import { CommandEmpty, CommandStatus } from './command-empty';
import { CommandInput } from './command-input';
import { CommandItem } from './command-item';
import { CommandGroup, CommandLabel, CommandSeparator } from './command-misc';
@@ -15,6 +15,7 @@ export const Command = Object.assign(CommandRoot, {
Content: CommandContent,
Item: CommandItem,
Empty: CommandEmpty,
+ Status: CommandStatus,
Group: CommandGroup,
Label: CommandLabel,
Separator: CommandSeparator,
diff --git a/packages/react/components/command/index.tsx b/packages/react/components/command/index.tsx
index da668ec9b..e7d55454e 100644
--- a/packages/react/components/command/index.tsx
+++ b/packages/react/components/command/index.tsx
@@ -1,7 +1,10 @@
export { Command } from './command';
export type { CommandContentProps } from './command-content';
export type { CommandDialogContentProps } from './command-dialog';
-export type { CommandEmptyProps } from './command-empty';
+export type {
+ CommandEmptyProps,
+ CommandStatusProps
+} from './command-empty';
export type { CommandInputProps } from './command-input';
export type { CommandItemProps } from './command-item';
export type {
diff --git a/packages/react/index.tsx b/packages/react/index.tsx
index fe8d40934..f8102d8ec 100644
--- a/packages/react/index.tsx
+++ b/packages/react/index.tsx
@@ -145,7 +145,8 @@ export {
type CommandItemProps,
type CommandLabelProps,
type CommandProps,
- type CommandSeparatorProps
+ type CommandSeparatorProps,
+ type CommandStatusProps
} from './components/command';
export { Container, type ContainerProps } from './components/container';
export {