Dropdown menus paint under later glass panels - portal them out
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m6s
Deploy / deploy (push) Successful in 35s

The Signals > Setups filter dropdowns (Direction, Recommended Action)
opened UNDER the panels below: glass containers use backdrop-filter,
which creates a stacking context, so the menu's z-50 could never
compete with later sibling panels regardless of value.

The menu now portals to document.body with fixed positioning tracked
against the trigger (re-measured on scroll/resize), and click-outside
accounts for the portaled node. Applies to every Dropdown usage.
Menu background also updated from the old green-tinted #151911 to the
Horizon #14161f.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 23:20:04 +02:00
co-authored by Claude Fable 5
parent c3dd80e203
commit 6464fdd4af
+34 -6
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
export interface DropdownOption {
value: string;
@@ -17,8 +18,12 @@ interface DropdownProps {
/**
* Fully dark, custom dropdown — replaces native <select>, whose popup list
* can't be reliably styled (white background on Windows). Button + absolutely
* positioned menu, click-outside and Escape to close.
* can't be reliably styled (white background on Windows).
*
* The menu is PORTALED to document.body with fixed positioning: glass panels
* use backdrop-filter, which creates a stacking context, so an absolutely
* positioned menu inside one would paint UNDER any later sibling panel no
* matter its z-index.
*/
export function Dropdown({
value,
@@ -30,14 +35,34 @@ export function Dropdown({
placeholder = 'Select…',
}: DropdownProps) {
const [open, setOpen] = useState(false);
const [rect, setRect] = useState<{ top: number; left: number; width: number } | null>(null);
const rootRef = useRef<HTMLDivElement>(null);
const menuRef = useRef<HTMLUListElement>(null);
const selected = options.find((o) => o.value === value);
// Track the button's viewport position while open (scroll/resize).
useEffect(() => {
if (!open) return;
const measure = () => {
const r = rootRef.current?.getBoundingClientRect();
if (r) setRect({ top: r.bottom + 4, left: r.left, width: r.width });
};
measure();
window.addEventListener('scroll', measure, true);
window.addEventListener('resize', measure);
return () => {
window.removeEventListener('scroll', measure, true);
window.removeEventListener('resize', measure);
};
}, [open]);
useEffect(() => {
if (!open) return;
const onDocClick = (e: MouseEvent) => {
if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
const t = e.target as Node;
if (rootRef.current?.contains(t) || menuRef.current?.contains(t)) return;
setOpen(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
@@ -74,10 +99,12 @@ export function Dropdown({
</svg>
</button>
{open && (
{open && rect && createPortal(
<ul
ref={menuRef}
role="listbox"
className="absolute z-50 mt-1 max-h-64 w-full overflow-auto rounded-lg border border-white/[0.1] bg-[#151911] p-1 shadow-2xl shadow-black/50"
style={{ position: 'fixed', top: rect.top, left: rect.left, width: rect.width }}
className="z-50 max-h-64 overflow-auto rounded-lg border border-white/[0.1] bg-[#14161f] p-1 shadow-2xl shadow-black/50"
>
{options.map((opt) => {
const isSelected = opt.value === value;
@@ -109,7 +136,8 @@ export function Dropdown({
</li>
);
})}
</ul>
</ul>,
document.body,
)}
</div>
);