diff --git a/.changeset/fix-account-modal-accessible-name.md b/.changeset/fix-account-modal-accessible-name.md
new file mode 100644
index 0000000000..683703baf8
--- /dev/null
+++ b/.changeset/fix-account-modal-accessible-name.md
@@ -0,0 +1,15 @@
+---
+"@rainbow-me/rainbowkit": patch
+---
+
+fix: give the account modal an accessible name
+
+`AccountModal` set `aria-labelledby="rk_account_modal_title"` on its dialog,
+but `ProfileDetails` rendered its heading with its own hardcoded id,
+`rk_profile_title`, and put that same id on the balance heading too. The
+reference never resolved, so screen readers announced the modal as an
+unnamed dialog, and the duplicated id was invalid on its own.
+
+`ProfileDetails` now takes the `titleId` from `AccountModal` and renders it
+on the account name only, so the dialog is announced by the connected
+address or ENS name, the way the chain and connect modals already are.
diff --git a/packages/rainbowkit/src/components/AccountModal/AccountModal.test.tsx b/packages/rainbowkit/src/components/AccountModal/AccountModal.test.tsx
new file mode 100644
index 0000000000..5383f59abf
--- /dev/null
+++ b/packages/rainbowkit/src/components/AccountModal/AccountModal.test.tsx
@@ -0,0 +1,43 @@
+import user from '@testing-library/user-event';
+import React, { Fragment } from 'react';
+import { describe, expect, it } from 'vitest';
+import { useConnect } from 'wagmi';
+import { renderWithProviders } from '../../../test/';
+import { mockedAccounts } from '../../../test/mockWallet';
+import { formatAddress } from '../ConnectButton/formatAddress';
+import { AccountModal } from './AccountModal';
+
+const AccountModalWithConnectButton = () => {
+ const { connect, connectors } = useConnect();
+
+ return (
+
+ {}} open />
+
+
+ );
+};
+
+describe('', () => {
+ it('names the dialog after the connected account', async () => {
+ const modal = renderWithProviders();
+
+ await user.click(await modal.findByTestId('rk-connect-btn'));
+
+ const dialog = await modal.findByRole('dialog');
+ const titleId = dialog.getAttribute('aria-labelledby');
+ expect(titleId).toBeTruthy();
+
+ // The id the dialog points at must exist exactly once, on the account name
+ const titles = document.querySelectorAll(`[id="${titleId}"]`);
+ expect(titles).toHaveLength(1);
+ expect(titles[0]).toHaveTextContent(formatAddress(mockedAccounts[0]));
+ expect(dialog).toHaveAccessibleName(formatAddress(mockedAccounts[0]));
+ });
+});
diff --git a/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx b/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx
index fa0e3babf2..75d1e37e0d 100644
--- a/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx
+++ b/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx
@@ -36,6 +36,7 @@ export function AccountModal({ onClose, open }: AccountModalProps) {
balance={balance}
onClose={onClose}
onDisconnect={disconnect}
+ titleId={titleId}
/>
diff --git a/packages/rainbowkit/src/components/ProfileDetails/ProfileDetails.tsx b/packages/rainbowkit/src/components/ProfileDetails/ProfileDetails.tsx
index e908d77f73..4847a300a1 100644
--- a/packages/rainbowkit/src/components/ProfileDetails/ProfileDetails.tsx
+++ b/packages/rainbowkit/src/components/ProfileDetails/ProfileDetails.tsx
@@ -26,6 +26,7 @@ interface ProfileDetailsProps {
balance: ReturnType['balance'];
onClose: () => void;
onDisconnect: () => void;
+ titleId: string;
}
export function ProfileDetails({
@@ -35,6 +36,7 @@ export function ProfileDetails({
balance,
onClose,
onDisconnect,
+ titleId,
}: ProfileDetailsProps) {
const showRecentTransactions = useContext(ShowRecentTransactionsContext);
const { i18n } = useContext(I18nContext);
@@ -65,7 +67,6 @@ export function ProfileDetails({
const displayBalance = ethBalance
? abbreviateETHBalance(Number.parseFloat(ethBalance))
: undefined;
- const titleId = 'rk_profile_title';
const mobile = isMobile();
return (
@@ -120,7 +121,6 @@ export function ProfileDetails({