From 39ffdb800682c1686e91521bfc83a902e8356d73 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 10 Jun 2026 18:50:25 -0400 Subject: [PATCH 1/4] fix(ui): use 'true' for inert attribute value for React 18/19 compat Empty string '' is falsy in React 19's boolean attribute handler and does not set the inert attribute, leaving hidden content interactive. 'true' is truthy in both React 18 and 19. Also adds global.d.ts type augmentation to packages/headless and packages/ui, removes per-site @ts-ignore suppressions, and adds ESLint no-unknown-property ignore for inert. --- .changeset/fix-inert-react-19-compat.md | 6 +++++ eslint.config.mjs | 2 +- packages/headless/src/global.d.ts | 7 ++++++ .../src/primitives/tabs/tabs.test.tsx | 23 +++++++++++++++++++ .../PricingTable/PricingTableMatrix.tsx | 1 - .../devPrompts/KeylessPrompt/index.tsx | 2 +- packages/ui/src/elements/Collapsible.tsx | 3 +-- .../elements/__tests__/Collapsible.test.tsx | 5 ++-- packages/ui/src/global.d.ts | 8 +++++++ 9 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-inert-react-19-compat.md create mode 100644 packages/headless/src/global.d.ts diff --git a/.changeset/fix-inert-react-19-compat.md b/.changeset/fix-inert-react-19-compat.md new file mode 100644 index 00000000000..faa86ed040b --- /dev/null +++ b/.changeset/fix-inert-react-19-compat.md @@ -0,0 +1,6 @@ +--- +"@clerk/headless": patch +"@clerk/ui": patch +--- + +Fix `inert` attribute to work correctly in React 19. The previous value `''` (empty string) is falsy and not set by React 19's boolean attribute handler, leaving hidden panel and collapsible content interactive. Switches to `'true'` which is truthy in both React 18 and 19. diff --git a/eslint.config.mjs b/eslint.config.mjs index 0624c8f75fe..f8bc5cebd4d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -386,7 +386,7 @@ export default tseslint.config([ 'react/jsx-sort-props': 'warn', 'react/no-array-index-key': 'warn', 'react/no-unstable-nested-components': 'warn', - 'react/no-unknown-property': ['error', { ignore: ['css'] }], // Emotion + 'react/no-unknown-property': ['error', { ignore: ['css', 'inert'] }], // Emotion; inert not in React 18 types 'react/self-closing-comp': 'warn', 'react/prop-types': 'off', 'react/react-in-jsx-scope': 'off', diff --git a/packages/headless/src/global.d.ts b/packages/headless/src/global.d.ts new file mode 100644 index 00000000000..3ade00635a2 --- /dev/null +++ b/packages/headless/src/global.d.ts @@ -0,0 +1,7 @@ +declare module 'react' { + interface HTMLAttributes { + // `inert` landed in @types/react v19; augment for React 18 compatibility. + // Use 'true' (truthy string) — '' is falsy in React 19 and won't set the attribute. + inert?: 'true' | undefined; + } +} diff --git a/packages/headless/src/primitives/tabs/tabs.test.tsx b/packages/headless/src/primitives/tabs/tabs.test.tsx index 24a8ec960dc..ca344c0107c 100644 --- a/packages/headless/src/primitives/tabs/tabs.test.tsx +++ b/packages/headless/src/primitives/tabs/tabs.test.tsx @@ -506,6 +506,29 @@ describe('Tabs', () => { const panels = document.querySelectorAll('[data-cl-slot="tabs-panel"][data-cl-hidden]'); expect(panels).toHaveLength(2); }); + + it('non-selected panels have inert attribute, selected panel does not', () => { + renderTabs(); + const panels = document.querySelectorAll('[data-cl-slot="tabs-panel"]'); + const inert = Array.from(panels).filter(p => p.hasAttribute('inert')); + const notInert = Array.from(panels).filter(p => !p.hasAttribute('inert')); + // Presence check only — React 18 renders inert="true", React 19 normalises to inert="" + expect(inert).toHaveLength(2); + expect(notInert).toHaveLength(1); + }); + + it('inert updates when selection changes', async () => { + const user = userEvent.setup(); + renderTabs(); + + await user.click(screen.getByText('Settings')); + + const panels = document.querySelectorAll('[data-cl-slot="tabs-panel"]'); + const [account, settings, billing] = Array.from(panels); + expect(account).toHaveAttribute('inert'); + expect(settings).not.toHaveAttribute('inert'); + expect(billing).toHaveAttribute('inert'); + }); }); describe('roving tabindex', () => { diff --git a/packages/ui/src/components/PricingTable/PricingTableMatrix.tsx b/packages/ui/src/components/PricingTable/PricingTableMatrix.tsx index e6ab609343b..4c17b858aac 100644 --- a/packages/ui/src/components/PricingTable/PricingTableMatrix.tsx +++ b/packages/ui/src/components/PricingTable/PricingTableMatrix.tsx @@ -265,7 +265,6 @@ export function PricingTableMatrix({ }), feePeriodNoticeAnimation, ]} - // @ts-ignore - Needed until React 19 support inert={planPeriod !== 'annual' ? 'true' : undefined} >
{ }); describe('Inert Attribute', () => { - it('sets inert to empty string when open={false}', async () => { + it('sets inert when open={false}', async () => { const { wrapper } = await createFixtures(); const { container, rerender } = render(Content, { wrapper }); @@ -375,7 +375,8 @@ describe('Collapsible', () => { rerender(Content); const element = container.querySelector('.cl-collapsible') as HTMLElement; - expect(element).toHaveAttribute('inert', ''); + // Check presence only — React 18 renders inert="true", React 19 normalises to inert="" + expect(element).toHaveAttribute('inert'); }); it('does not set inert when open={true}', async () => { diff --git a/packages/ui/src/global.d.ts b/packages/ui/src/global.d.ts index 7bbffaeda66..19db755dffa 100644 --- a/packages/ui/src/global.d.ts +++ b/packages/ui/src/global.d.ts @@ -1,6 +1,14 @@ import type { Clerk } from '@clerk/shared/types'; import type { ClerkUIConstructor } from '@clerk/shared/ui'; +declare module 'react' { + interface HTMLAttributes { + // `inert` landed in @types/react v19; augment for React 18 compatibility. + // Use 'true' (truthy string) — '' is falsy in React 19 and won't set the attribute. + inert?: 'true' | undefined; + } +} + declare module '*.svg' { const value: React.FC>; export default value; From 437f8703ad34880f08c4bbd3c7bf23237613c9f8 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 10 Jun 2026 18:51:17 -0400 Subject: [PATCH 2/4] Update fix-inert-react-19-compat.md --- .changeset/fix-inert-react-19-compat.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/fix-inert-react-19-compat.md b/.changeset/fix-inert-react-19-compat.md index faa86ed040b..2068744f5a9 100644 --- a/.changeset/fix-inert-react-19-compat.md +++ b/.changeset/fix-inert-react-19-compat.md @@ -1,5 +1,4 @@ --- -"@clerk/headless": patch "@clerk/ui": patch --- From 08c899ff6290f8b69c27d4563211e9090429f3a6 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 10 Jun 2026 19:04:49 -0400 Subject: [PATCH 3/4] chore(repo): drop redundant inert eslint ignore inert is already a recognized DOM property in eslint-plugin-react, and no usage is on a literal DOM element, so the ignore entry had no effect. --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index f8bc5cebd4d..0624c8f75fe 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -386,7 +386,7 @@ export default tseslint.config([ 'react/jsx-sort-props': 'warn', 'react/no-array-index-key': 'warn', 'react/no-unstable-nested-components': 'warn', - 'react/no-unknown-property': ['error', { ignore: ['css', 'inert'] }], // Emotion; inert not in React 18 types + 'react/no-unknown-property': ['error', { ignore: ['css'] }], // Emotion 'react/self-closing-comp': 'warn', 'react/prop-types': 'off', 'react/react-in-jsx-scope': 'off', From 91077cd55793e8e9aa45109265ece20f293543e7 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 10 Jun 2026 19:30:28 -0400 Subject: [PATCH 4/4] fix(headless): make react inert augmentation a module to avoid shadowing react types --- packages/headless/src/global.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/headless/src/global.d.ts b/packages/headless/src/global.d.ts index 3ade00635a2..34c921452ae 100644 --- a/packages/headless/src/global.d.ts +++ b/packages/headless/src/global.d.ts @@ -1,4 +1,10 @@ +// `export {}` makes this file a module so `declare module 'react'` augments React's +// types instead of replacing them (an ambient declaration would shadow React, breaking +// type resolution package-wide). +export {}; + declare module 'react' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars interface HTMLAttributes { // `inert` landed in @types/react v19; augment for React 18 compatibility. // Use 'true' (truthy string) — '' is falsy in React 19 and won't set the attribute.