diff --git a/packages/eui/changelogs/upcoming/8916.md b/packages/eui/changelogs/upcoming/8916.md
new file mode 100644
index 00000000000..baf55826fba
--- /dev/null
+++ b/packages/eui/changelogs/upcoming/8916.md
@@ -0,0 +1,5 @@
+**Accessibility**
+
+- Added a new beta `EuiLiveAnnouncer` component which supports `aria-live` announcements on mount
+- Added `announceOnMount` prop on `EuiCallOut` to support announcing its content on mount
+
diff --git a/packages/eui/src/components/accessibility/index.ts b/packages/eui/src/components/accessibility/index.ts
index 12a82997e7b..2fc2ec54899 100644
--- a/packages/eui/src/components/accessibility/index.ts
+++ b/packages/eui/src/components/accessibility/index.ts
@@ -12,3 +12,4 @@ export { EuiScreenReaderOnly, euiScreenReaderOnly } from './screen_reader_only';
export type { EuiScreenReaderOnlyProps } from './screen_reader_only';
export { EuiSkipLink } from './skip_link';
export type { EuiSkipLinkProps } from './skip_link';
+export { type EuiLiveAnnouncerProps, EuiLiveAnnouncer } from './live_announcer';
diff --git a/packages/eui/src/components/accessibility/live_announcer/__snapshots__/live_announcer.test.tsx.snap b/packages/eui/src/components/accessibility/live_announcer/__snapshots__/live_announcer.test.tsx.snap
new file mode 100644
index 00000000000..4fa87f26e38
--- /dev/null
+++ b/packages/eui/src/components/accessibility/live_announcer/__snapshots__/live_announcer.test.tsx.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiLiveAnnouncer renders screen reader content when active 1`] = `
+
+ You have new notifications.
+
+`;
diff --git a/packages/eui/src/components/accessibility/live_announcer/index.ts b/packages/eui/src/components/accessibility/live_announcer/index.ts
new file mode 100644
index 00000000000..2704b9ff375
--- /dev/null
+++ b/packages/eui/src/components/accessibility/live_announcer/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export * from './live_announcer';
diff --git a/packages/eui/src/components/accessibility/live_announcer/live_announcer.stories.tsx b/packages/eui/src/components/accessibility/live_announcer/live_announcer.stories.tsx
new file mode 100644
index 00000000000..c24c1f60e04
--- /dev/null
+++ b/packages/eui/src/components/accessibility/live_announcer/live_announcer.stories.tsx
@@ -0,0 +1,170 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { ReactNode, useEffect, useState } from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { EuiLiveAnnouncer, EuiLiveAnnouncerProps } from './live_announcer';
+import { EuiFlexGroup, EuiFlexItem } from '../../flex';
+import { EuiButton } from '../../button';
+import { EuiCodeBlock } from '../../code';
+import { EuiSpacer } from '../../spacer';
+import { EuiCallOut } from '../../call_out';
+import { EuiFlyout, EuiFlyoutBody } from '../../flyout';
+
+const meta: Meta = {
+ title: 'Utilities/EuiLiveAnnouncer',
+ component: EuiLiveAnnouncer,
+ args: {
+ // Component defaults
+ role: 'status',
+ isActive: true,
+ },
+ parameters: {
+ loki: {
+ // There are no visual elements to test
+ skip: true,
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Playground: Story = {
+ parameters: {
+ codeSnippet: {
+ snippet: `
+ {message}
+ `,
+ },
+ },
+ args: {
+ children: 'You have new notifications',
+ },
+ render: function Render(args) {
+ const { children, ...rest } = args;
+ const [announcement, setAnnouncement] = useState(children);
+ const [isAnnouncementShown, setAnnouncementShown] = useState(false);
+
+ useEffect(() => {
+ setAnnouncement(children);
+ }, [children]);
+
+ const updateAnnouncement = () => {
+ setAnnouncement(
+ `You have ${Math.floor(Math.random() * 1000)} new notifications.`
+ );
+ };
+
+ return (
+
+
+ setAnnouncementShown((shown) => !shown)}>
+ Toggle announcement
+
+
+
+
+ {isAnnouncementShown && (
+ <>
+
+ Update announcement
+
+
+ {announcement}
+ {announcement}
+ >
+ )}
+
+
+ );
+ },
+};
+
+export const WithinFlyouts: Story = {
+ parameters: {
+ codeSnippet: {
+ skip: true,
+ },
+ },
+ args: {
+ children: 'You have new notifications',
+ },
+ render: function Render(args) {
+ const { children, ...rest } = args;
+ const [isFlyoutOpen, setFlyoutOpen] = useState(false);
+ const [announcement, setAnnouncement] = useState(children);
+ const [isShown, setShown] = useState(false);
+
+ useEffect(() => {
+ setAnnouncement(children);
+ }, [children]);
+
+ const updateAnnouncement = () => {
+ setAnnouncement(
+ `You have ${Math.floor(Math.random() * 1000)} new notifications.`
+ );
+ };
+
+ const content = (
+ <>
+
+
+ Update announcement
+
+
+ {announcement}
+ {announcement}
+
+
+ setShown((show) => !show)}>
+ Toggle CallOut
+
+ {isShown && (
+ <>
+
+ setShown(false)}
+ title="Important notification!"
+ >
+ {/* long text is for testing clearTimeout functionality */}
+
+ Lorem Ipsum is simply dummy text of the printing and typesetting
+ industry. Lorem Ipsum has been the industry's standard dummy
+ text ever since the 1500s, when an unknown printer took a galley
+ of type and scrambled it to make a type specimen book. It has
+ survived not only five centuries, but also the leap into
+ electronic typesetting, remaining essentially unchanged. It was
+ popularised in the 1960s with the release of Letraset sheets
+ containing Lorem Ipsum passages, and more recently with desktop
+ publishing software like Aldus PageMaker including versions of
+ Lorem Ipsum.
+
+
+ >
+ )}
+ >
+ );
+
+ return (
+ <>
+ setFlyoutOpen((open) => !open)}>
+ Toggle flyout
+
+
+ {isFlyoutOpen && (
+ setFlyoutOpen(false)}>
+ {content}
+
+ )}
+ >
+ );
+ },
+};
diff --git a/packages/eui/src/components/accessibility/live_announcer/live_announcer.test.tsx b/packages/eui/src/components/accessibility/live_announcer/live_announcer.test.tsx
new file mode 100644
index 00000000000..518dbb50531
--- /dev/null
+++ b/packages/eui/src/components/accessibility/live_announcer/live_announcer.test.tsx
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { ReactElement } from 'react';
+import { act } from '@testing-library/react';
+import { screen, render } from '../../../test/rtl';
+
+import { EuiLiveAnnouncer } from './live_announcer';
+
+const content = 'You have new notifications.';
+
+const renderComponent = (component: ReactElement) => {
+ const testArgs = render(component);
+
+ act(() => {
+ jest.advanceTimersByTime(50);
+ });
+
+ return testArgs;
+};
+
+describe('EuiLiveAnnouncer', () => {
+ jest.useFakeTimers();
+
+ afterEach(() => {
+ jest.clearAllTimers();
+ });
+
+ it('renders screen reader content when active', () => {
+ const { container } = renderComponent(
+ {content}
+ );
+
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it('renders `children` as string correctly', () => {
+ renderComponent({content});
+
+ const region = screen.getByRole('status');
+ expect(region).toHaveAttribute('aria-live', 'polite');
+ expect(region).toHaveAttribute('aria-atomic', 'true');
+ expect(region).toHaveTextContent('You have new notifications.');
+ });
+
+ it('renders `children` as ReactNode correctly', () => {
+ renderComponent({content});
+
+ const region = screen.getByRole('status').firstChild;
+ expect(region).toHaveTextContent('You have new notifications.');
+ });
+
+ it('clears the message after `clearAfterMs`', () => {
+ renderComponent(
+ {content}
+ );
+ const region = screen.getByRole('status');
+ expect(region).toHaveTextContent('You have new notifications.');
+ act(() => {
+ jest.advanceTimersByTime(1000);
+ });
+ expect(region).toHaveTextContent('');
+ });
+
+ it('does not clear the message if `clearAfterMs=false`', () => {
+ renderComponent(
+ {content}
+ );
+ const region = screen.getByRole('status');
+ act(() => {
+ jest.advanceTimersByTime(5000);
+ });
+ expect(region).toHaveTextContent('You have new notifications.');
+ });
+
+ it('sets `aria-live` to off when `isActive=false`', () => {
+ renderComponent(
+ {content}
+ );
+ const region = screen.getByRole('status');
+ expect(region).toHaveAttribute('aria-live', 'off');
+ });
+
+ it('sets custom `role` and `aria-live`', () => {
+ renderComponent(
+
+ {content}
+
+ );
+ const region = screen.getByRole('alert');
+ expect(region).toHaveAttribute('aria-live', 'assertive');
+ });
+
+ it('updates the message when `children` change', () => {
+ const { rerender } = renderComponent(
+ {content}
+ );
+ const region = screen.getByRole('status');
+ expect(region).toHaveTextContent('You have new notifications.');
+ rerender(
+ You have additional notifications.
+ );
+ expect(region).toHaveTextContent('You have additional notifications.');
+ });
+});
diff --git a/packages/eui/src/components/accessibility/live_announcer/live_announcer.tsx b/packages/eui/src/components/accessibility/live_announcer/live_announcer.tsx
new file mode 100644
index 00000000000..8337198a217
--- /dev/null
+++ b/packages/eui/src/components/accessibility/live_announcer/live_announcer.tsx
@@ -0,0 +1,90 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, {
+ FunctionComponent,
+ ReactNode,
+ useEffect,
+ useState,
+ isValidElement,
+} from 'react';
+
+import { EuiScreenReaderOnly } from '../screen_reader_only';
+import { EuiScreenReaderLiveProps } from '../screen_reader_live';
+
+export type EuiLiveAnnouncerProps = Omit<
+ EuiScreenReaderLiveProps,
+ 'focusRegionOnTextChange'
+> & {
+ /**
+ * Sets a delay in ms before the live region is cleared.
+ * The message will still be read by screen readers even if it's cleared in between.
+ *
+ * @default 2000
+ */
+ clearAfterMs?: number | false;
+};
+
+export const EuiLiveAnnouncer: FunctionComponent = ({
+ children,
+ clearAfterMs = 2000,
+ isActive = true,
+ role = 'status',
+ 'aria-live': ariaLive = 'polite',
+ ...rest
+}) => {
+ const [content, setContent] = useState('');
+ const [isMounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ if (!isMounted) {
+ setTimeout(() => {
+ // set isMounted with a small delay to trigger an render update after first render
+ setMounted(true);
+ }, 50);
+ }
+
+ return () => {
+ setMounted(false);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ let timeout: NodeJS.Timeout | undefined;
+
+ if (children) {
+ setContent(isValidElement(children) ? children : <>{children}>);
+ }
+
+ if (clearAfterMs) {
+ timeout = setTimeout(() => {
+ setContent('');
+ }, clearAfterMs);
+ }
+
+ return () => {
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+ };
+ }, [children, clearAfterMs]);
+
+ return (
+
+
+ {isMounted && content}
+
+
+ );
+};
diff --git a/packages/eui/src/components/accessibility/screen_reader_live/screen_reader_live.stories.tsx b/packages/eui/src/components/accessibility/screen_reader_live/screen_reader_live.stories.tsx
index f2232bd174b..af5e0ff223d 100644
--- a/packages/eui/src/components/accessibility/screen_reader_live/screen_reader_live.stories.tsx
+++ b/packages/eui/src/components/accessibility/screen_reader_live/screen_reader_live.stories.tsx
@@ -6,12 +6,18 @@
* Side Public License, v 1.
*/
+import React, { ReactNode, useEffect, useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import {
EuiScreenReaderLive,
EuiScreenReaderLiveProps,
} from './screen_reader_live';
+import { EuiFlexGroup, EuiFlexItem } from '../../flex';
+import { EuiButton } from '../../button';
+import { EuiCodeBlock } from '../../code';
+import { EuiSpacer } from '../../spacer';
+import { EuiFlyout, EuiFlyoutBody } from '../../flyout';
const meta: Meta = {
title: 'Utilities/EuiScreenReaderLive',
@@ -34,7 +40,107 @@ export default meta;
type Story = StoryObj;
export const Playground: Story = {
+ parameters: {
+ codeSnippet: {
+ snippet: `
+ {message}
+ `,
+ },
+ },
args: {
children: 'You have new notifications',
},
+ render: function Render(args) {
+ const { children, ...rest } = args;
+ const [announcement, setAnnouncement] = useState(children);
+ const [isAnnouncementShown, setAnnouncementShown] = useState(false);
+
+ useEffect(() => {
+ setAnnouncement(children);
+ }, [children]);
+
+ const updateAnnouncement = () => {
+ setAnnouncement(
+ `You have ${Math.floor(Math.random() * 1000)} new notifications.`
+ );
+ };
+
+ return (
+
+
+ setAnnouncementShown((shown) => !shown)}>
+ Toggle announcement
+
+
+
+
+ {isAnnouncementShown && (
+ <>
+
+ Update announcement
+
+
+ {announcement}
+
+ {announcement}
+
+ >
+ )}
+
+
+ );
+ },
+};
+
+export const WithinFlyouts: Story = {
+ parameters: {
+ codeSnippet: {
+ skip: true,
+ },
+ },
+ args: {
+ children: 'You have new notifications',
+ },
+ render: function Render(args) {
+ const { children, ...rest } = args;
+ const [isFlyoutOpen, setFlyoutOpen] = useState(false);
+ const [announcement, setAnnouncement] = useState(children);
+
+ useEffect(() => {
+ setAnnouncement(children);
+ }, [children]);
+
+ const updateAnnouncement = () => {
+ setAnnouncement(
+ `You have ${Math.floor(Math.random() * 1000)} new notifications.`
+ );
+ };
+
+ const content = (
+ <>
+
+
+ Update announcement
+
+
+ {announcement}
+ {announcement}
+
+ >
+ );
+
+ return (
+ <>
+ setFlyoutOpen((open) => !open)}>
+ Toggle flyout
+
+
+ {isFlyoutOpen && (
+ setFlyoutOpen(false)}>
+ {content}
+
+ )}
+ >
+ );
+ },
};
diff --git a/packages/eui/src/components/accessibility/screen_reader_live/screen_reader_live.tsx b/packages/eui/src/components/accessibility/screen_reader_live/screen_reader_live.tsx
index 6c4c8eee169..8b2cd86c679 100644
--- a/packages/eui/src/components/accessibility/screen_reader_live/screen_reader_live.tsx
+++ b/packages/eui/src/components/accessibility/screen_reader_live/screen_reader_live.tsx
@@ -31,10 +31,14 @@ export interface EuiScreenReaderLiveProps {
* `role` attribute for both live regions.
*
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions#roles_with_implicit_live_region_attributes
+ *
+ * @default 'status'
*/
role?: HTMLAttributes['role'];
/**
* `aria-live` attribute for both live regions
+ *
+ * @default 'polite'
*/
'aria-live'?: AriaAttributes['aria-live'];
/**
@@ -42,6 +46,8 @@ export interface EuiScreenReaderLiveProps {
* to automatically read out the text content. This prop should primarily be used for
* navigation or page changes, where programmatically resetting focus location back to
* a certain part of the page is desired.
+ *
+ * @default false
*/
focusRegionOnTextChange?: boolean;
}
@@ -86,6 +92,7 @@ export const EuiScreenReaderLive: FunctionComponent<
aria-atomic="true"
// Setting `aria-hidden` and setting `aria-live` to "off" prevents
// double announcements from VO when `focusRegionOnTextChange` is true
+ // WARNING: This solves the issue in VO/Chrome but results in VO/Safari not reading anything at all
aria-hidden={toggle ? undefined : 'true'}
aria-live={!toggle || focusRegionOnTextChange ? 'off' : ariaLive}
>
diff --git a/packages/eui/src/components/call_out/call_out.stories.tsx b/packages/eui/src/components/call_out/call_out.stories.tsx
index 7f40c86a655..3e4cbc4ea08 100644
--- a/packages/eui/src/components/call_out/call_out.stories.tsx
+++ b/packages/eui/src/components/call_out/call_out.stories.tsx
@@ -6,8 +6,10 @@
* Side Public License, v 1.
*/
+import React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
+import { EuiButton } from '../button';
import { EuiCallOut, EuiCallOutProps } from './call_out';
const meta: Meta = {
@@ -33,3 +35,33 @@ export const Playground: Story = {
children: 'Callout text',
},
};
+
+export const AnnounceOnMount: Story = {
+ parameters: {
+ controls: {
+ include: ['children', 'announceOnMount'],
+ },
+ loki: {
+ skip: true,
+ },
+ },
+ args: {
+ title: 'Callout title',
+ children: 'Callout text',
+ announceOnMount: true,
+ },
+ render: function Render() {
+ const [isShown, setShown] = useState(false);
+
+ return (
+ <>
+ setShown(!isShown)}>Toggle CallOut
+ {isShown && (
+
+ Callout text
+
+ )}
+ >
+ );
+ },
+};
diff --git a/packages/eui/src/components/call_out/call_out.test.tsx b/packages/eui/src/components/call_out/call_out.test.tsx
index 50c992e2319..3325b20de0f 100644
--- a/packages/eui/src/components/call_out/call_out.test.tsx
+++ b/packages/eui/src/components/call_out/call_out.test.tsx
@@ -7,9 +7,9 @@
*/
import React from 'react';
-import { fireEvent } from '@testing-library/react';
+import { act, fireEvent } from '@testing-library/react';
import { requiredProps } from '../../test/required_props';
-import { render } from '../../test/rtl';
+import { render, screen } from '../../test/rtl';
import { EuiCallOut, COLORS, HEADINGS } from './call_out';
@@ -63,12 +63,60 @@ describe('EuiCallOut', () => {
test('onDismiss', () => {
const onDismiss = jest.fn();
- const { getByTestSubject } = render(
- Content
- );
+ render(Content);
- fireEvent.click(getByTestSubject('euiDismissCalloutButton'));
+ fireEvent.click(screen.getByTestSubject('euiDismissCalloutButton'));
expect(onDismiss).toHaveBeenCalledTimes(1);
});
+
+ describe('announceOnMount', () => {
+ jest.useFakeTimers();
+
+ afterEach(() => {
+ jest.clearAllTimers();
+ });
+
+ it('announces the callout content in an aria-live region when announceOnMount is true', () => {
+ render(
+
+ Announcement content
+
+ );
+
+ act(() => {
+ jest.advanceTimersByTime(50);
+ });
+
+ // The live region should exist and contain the announcement
+ const liveRegion = screen.getByRole('status');
+ expect(liveRegion).toHaveTextContent(
+ 'Announcement title, Announcement content'
+ );
+ });
+
+ it('clears the announcement after 2000ms', () => {
+ render(
+
+ Announcement content
+
+ );
+
+ act(() => {
+ jest.advanceTimersByTime(50);
+ });
+
+ // The live region should exist and contain the announcement
+ const liveRegion = screen.getByRole('status');
+ expect(liveRegion).toHaveTextContent(
+ 'Announcement title, Announcement content'
+ );
+
+ act(() => {
+ jest.advanceTimersByTime(2000);
+ });
+
+ expect(liveRegion).toHaveTextContent('');
+ });
+ });
});
});
diff --git a/packages/eui/src/components/call_out/call_out.tsx b/packages/eui/src/components/call_out/call_out.tsx
index 5fb5c9a1806..ef5531912ba 100644
--- a/packages/eui/src/components/call_out/call_out.tsx
+++ b/packages/eui/src/components/call_out/call_out.tsx
@@ -19,6 +19,7 @@ import { EuiPanel } from '../panel';
import { EuiSpacer } from '../spacer';
import { EuiTitle } from '../title';
import { EuiI18n } from '../i18n';
+import { EuiLiveAnnouncer } from '../accessibility/live_announcer';
import { euiCallOutStyles, euiCallOutHeaderStyles } from './call_out.styles';
@@ -51,6 +52,13 @@ export type EuiCallOutProps = CommonProps &
* removing the callout or other actions.
*/
onDismiss?: () => void;
+ /**
+ * Enables the content to be read by screen readers on mount.
+ * Use this for callouts that are shown based on a user action.
+ *
+ * @default false
+ */
+ announceOnMount?: boolean;
};
export const EuiCallOut = forwardRef(
@@ -64,6 +72,7 @@ export const EuiCallOut = forwardRef(
className,
heading = 'p',
onDismiss,
+ announceOnMount = false,
...rest
},
ref
@@ -170,6 +179,12 @@ export const EuiCallOut = forwardRef(
>
)
}
+ {announceOnMount && (title || children) && (
+
+ {title && `${title}, `}
+ {children}
+
+ )}
);
}
diff --git a/packages/website/docs/components/display/callout.mdx b/packages/website/docs/components/display/callout.mdx
index 60f7413cb42..3f9ef85357f 100644
--- a/packages/website/docs/components/display/callout.mdx
+++ b/packages/website/docs/components/display/callout.mdx
@@ -241,6 +241,44 @@ export default () => {
};
```
+### Screen reader announcements
+
+`EuiCallOut` supports announcing its content to screen readers when it first appears using the `announceOnMount` prop.
+When enabled, the callout's title and content will be announced via an `aria-live` region. The announcement will be cleared after 2 seconds
+to prevent duplicate DOM content but screen readers will continue reading the message even after it's cleared.
+
+Generally you won't need this prop when a callout is rendered on initial page load, as screen readers will encounter the content during normal page navigation.
+Use `announceOnMount` when the callout appears as the result of a user action or contains information that needs immediate attention.
+
+
+```tsx interactive
+import React, { useState } from 'react';
+import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui';
+
+export default () => {
+ const [isShown, setIsShown] = useState(false);
+
+ return (
+ <>
+ setIsShown(!isShown)}>
+ {isShown ? 'Hide' : 'Show'} callout
+
+
+ {isShown && (
+
+ This content will be announced to screen readers when it appears.
+
+ )}
+ >
+ );
+};
+```
+
+
## Guidelines
**Keep these guidelines in mind:**
diff --git a/packages/website/docs/utilities/accessibility/index.mdx b/packages/website/docs/utilities/accessibility/index.mdx
index 9511dacb313..579cf26a6a6 100644
--- a/packages/website/docs/utilities/accessibility/index.mdx
+++ b/packages/website/docs/utilities/accessibility/index.mdx
@@ -190,6 +190,81 @@ This is primarily useful for announcing navigation or page changes, when program
```
+import { EuiBetaBadge } from '@elastic/eui';
+
+## Live announcer region
+
+:::warning `EuiLiveAnnouncer` is a new, experimental alternative to `EuiScreenReaderLive` for screen reader live region announcements.
+Its API and behavior may change based on feedback and testing.
+:::
+
+Both `EuiLiveAnnouncer` and `EuiScreenReaderLive` are designed for the **same use cases**: to announce dynamic content, such as nodifications or status changes based on user interaction.
+The main drawback for live regions is that they must be present on initial page load before reading content updates. `EuiLiveAnnouncer` addresses this
+by updating the content after the initial render to ensure an announcement is made when mounting a component.
+
+Same as for `EuiScreenReaderLive`, `EuiLiveAnnouncer` supports configurable `role` and `aria-live` props which default to `status` and `polite` respectively for non-intrusive but timely update announcements.
+If you're not using the default values, be sure to follow [ARIA guidelines](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) for `role` to `aria-live` mapping.
+
+### Main difference
+
+- **`EuiLiveAnnouncer`** will announce the message **when the component mounts** (e.g., when it first appears in the DOM), as well as when the message changes.
+- **`EuiScreenReaderLive`** will only announce when the message changes, and will not announce the initial message on mount.
+
+This means `EuiLiveAnnouncer` is especially useful for scenarios where you want to ensure an announcement is made immediately when a component is mounted, e.g. showing a notification or alert based on user interaction.
+
+An additional difference is that `EuiLiveAnnouncer` can clear its message after a configurable timeout via `clearAfterMs`, which prevents stale
+announcements and duplicated DOM content for screen reader users. Screen readers will continue reading the message even if it was already cleared.
+
+
+ ```tsx
+ import { useState } from 'react';
+ import { EuiButton, EuiLiveAnnouncer, EuiSpacer } from '@elastic/eui';
+
+ export default () => {
+ const [announcement, setAnnouncement] = useState('No notifications.');
+ const [isShown, setIsShown] = useState(false);
+
+ return (
+
+
+ setIsShown((shown) => !shown)}>
+ Toggle announcement
+
+
+
+
+ {isShown && (
+ <>
+ setAnnouncement(`You have ${Math.floor(Math.random() * 1000)} new notifications.`)}>
+ Update announcement
+
+
+ {announcement}
+
+ {announcement}
+
+ >
+ )}
+
+
+ );
+ };
+ ```
+
+
+### Key differences between `EuiLiveAnnouncer` and `EuiScreenReaderLive`
+
+| Feature | `EuiLiveAnnouncer` | `EuiScreenReaderLive` |
+|--------------------------------|:---------------------------------:|:-------------------------------:|
+| **Auto-clear message** | Yes, after configurable timeout (`clearAfterMs`) | No |
+| **Announce on mount** | Yes | No |
+| **Announce on message change** | Yes | Yes |
+| **Focus on message change** | No | Yes (`focusRegionOnTextChange` prop) |
+| **Handles rapid updates** | Yes | Yes |
+| **Output consistency** | High | Medium |
+| **API stability** | Beta/experimental | Stable |
+
+
## Skip link
The `EuiSkipLink` component allows users to bypass navigation, or ornamental elements, and quickly reach the main content of the page. It requires a `destinationId` which should match the `id` of your main content. If your ID does not correspond to a valid element, the skip link will fall back to focusing the `` tag on your page, if it exists.
@@ -288,4 +363,5 @@ import docgen from '@elastic/eui-docgen/dist/components';
+