diff --git a/.changeset/tooltip-above-modal.md b/.changeset/tooltip-above-modal.md
new file mode 100644
index 00000000000..11809b73cf0
--- /dev/null
+++ b/.changeset/tooltip-above-modal.md
@@ -0,0 +1,5 @@
+---
+'@clerk/ui': patch
+---
+
+Fix tooltips rendering behind modals (for example on the organization profile Security page). Tooltips now layer above modal content, and pressing Escape or clicking outside while a tooltip is open inside a modal closes only the tooltip instead of also dismissing the modal.
diff --git a/packages/ui/src/elements/Tooltip.tsx b/packages/ui/src/elements/Tooltip.tsx
index 1fcc207a298..7b0a4a76d4b 100644
--- a/packages/ui/src/elements/Tooltip.tsx
+++ b/packages/ui/src/elements/Tooltip.tsx
@@ -1,12 +1,15 @@
+import { usePortalRoot } from '@clerk/shared/react';
import type { Placement, UseFloatingReturn, UseInteractionsReturn } from '@floating-ui/react';
import {
autoUpdate,
flip,
+ FloatingNode,
FloatingPortal,
offset,
shift,
useDismiss,
useFloating,
+ useFloatingNodeId,
useFocus,
useHover,
useInteractions,
@@ -16,11 +19,10 @@ import {
} from '@floating-ui/react';
import * as React from 'react';
-import { usePortalRoot } from '@clerk/shared/react';
-
import { Box, descriptors, type LocalizationKey, Span, Text, useAppearance } from '../customizables';
import { usePrefersReducedMotion } from '../hooks';
import type { ThemableCssProp } from '../styledSystem';
+import { withFloatingTree } from './contexts';
interface TooltipOptions {
initialOpen?: boolean;
@@ -34,6 +36,7 @@ interface UseTooltipReturn extends UseFloatingReturn, UseInteractionsReturn {
setOpen: (open: boolean) => void;
isMounted: boolean;
transitionStyles: React.CSSProperties;
+ nodeId: string | undefined;
}
export function useTooltip({
@@ -51,7 +54,9 @@ export function useTooltip({
const { animations: layoutAnimations } = useAppearance().parsedOptions;
const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
+ const nodeId = useFloatingNodeId();
const data = useFloating({
+ nodeId,
placement,
open,
onOpenChange: setOpen,
@@ -104,11 +109,12 @@ export function useTooltip({
open,
setOpen,
isMounted,
+ nodeId,
...interactions,
...data,
transitionStyles,
}),
- [open, setOpen, interactions, data, isMounted, transitionStyles],
+ [open, setOpen, interactions, data, isMounted, transitionStyles, nodeId],
);
}
@@ -126,12 +132,12 @@ export const useTooltipContext = (): UseTooltipReturn => {
return context;
};
-function Root({ children, ...options }: { children: React.ReactNode } & TooltipOptions) {
+const Root = withFloatingTree(({ children, ...options }: { children: React.ReactNode } & TooltipOptions) => {
// This can accept any props as options, e.g. `placement`,
// or other positioning options.
const tooltip = useTooltip(options);
return {children};
-}
+});
type TriggerProps = React.HTMLProps & {
sx?: ThemableCssProp;
@@ -203,41 +209,44 @@ const Content = React.forwardRef<
}
return (
-
-
+
+
({
- paddingBlock: t.space.$1,
- paddingInline: t.space.$1x5,
- borderRadius: t.radii.$md,
- backgroundColor: t.colors.$black,
- color: t.colors.$white,
- maxWidth: t.sizes.$60,
- }),
- sx,
- ]}
+ ref={ref}
+ elementDescriptor={descriptors.tooltip}
+ style={{
+ ...context.floatingStyles,
+ ...style,
+ }}
+ {...context.getFloatingProps(props)}
+ sx={t => ({ zIndex: t.zIndices.$tooltip })}
>
-
+ ({
+ paddingBlock: t.space.$1,
+ paddingInline: t.space.$1x5,
+ borderRadius: t.radii.$md,
+ backgroundColor: t.colors.$black,
+ color: t.colors.$white,
+ maxWidth: t.sizes.$60,
+ }),
+ sx,
+ ]}
+ >
+
+
-
-
+
+
);
});
diff --git a/packages/ui/src/foundations/zIndices.ts b/packages/ui/src/foundations/zIndices.ts
index 950e76a86dd..8c08516547f 100644
--- a/packages/ui/src/foundations/zIndices.ts
+++ b/packages/ui/src/foundations/zIndices.ts
@@ -4,4 +4,5 @@ export const zIndices = Object.freeze({
fab: '9000',
modal: '10000',
dropdown: '11000',
+ tooltip: '12000',
} as const);