refactor(*): migrate 'radix-ui' primitives to 'base-ui' - #1123
Conversation
📝 WalkthroughWalkthroughThe pull request replaces Radix UI dialog, dropdown, and tooltip primitives with Base UI components. Shared APIs and consumers now use render props, Base UI event handlers, positioning components, and updated state attributes. ChangesBase UI migration
Estimated code review effort: 4 (Complex) | ~60 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
tanstack-com | 5257538 | Commit Preview URL Branch Preview URL |
Aug 08 2026, 02:40 AM |
… shared components root
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (5)
src/components/npm-stats/PackagePills.tsx (2)
243-254: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffA nested interactive button sits inside a
menuitem.The remove button is a child of
Menu.Item, which renders withrole="menuitem". Nested interactive controls inside a menu item are not reachable by keyboard in a menu composite, because the item itself owns focus. Screen reader users cannot invoke the remove action.Consider rendering the remove action as its own
Menu.Itemfor each sub-package.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/npm-stats/PackagePills.tsx` around lines 243 - 254, Update the sub-package menu structure around the remove button so the remove action is rendered as its own sibling Menu.Item rather than a nested button inside another menuitem. Preserve the existing onRemoveFromGroup arguments and stopPropagation behavior while ensuring the action is independently keyboard reachable.
197-204: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winRemove the cast from the Base UI
Menu.Itemhandler.
Menu.Item.onClickhas a Base UI-specific event shape, buthandleColorClickonly uses native DOM properties. TypeMenu.Item’s handler consistently by replacing the cast with the actual event parameter type and passingedirectly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/npm-stats/PackagePills.tsx` around lines 197 - 204, Update the Menu.Item onClick handler around onColorClick to use the actual Base UI event parameter type, removing the unknown React.MouseEvent cast and passing e directly. Preserve the existing preventDefault call and arguments to onColorClick.src/components/SearchModal.tsx (1)
3368-3370: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winThe outside-press guard depends on an implicit CSS-class contract.
isSearchModalPortalTargetmatches.dropdown-content. OnlyDropdownContentinsrc/components/Dropdown.tsxadds that class. The menus migrated in this PR that useMenu.Popupdirectly, for examplesrc/components/charts/ChartControls.tsxandsrc/components/npm-stats/NPMStatsChart.tsx, do not add it. If any such menu is later rendered insideSearchModal, a click inside it closes the modal.Consider matching the Base UI popup attribute instead of a project-specific class, for example
[role="menu"], which is whatsrc/components/LibrariesOverlay.tsxalready uses for the same purpose.♻️ Proposed change
function isSearchModalPortalTarget(target: EventTarget | null) { - return target instanceof Element && !!target.closest('.dropdown-content') + return ( + target instanceof Element && + !!target.closest('.dropdown-content, [role="menu"]') + ) }Also applies to: 3403-3418
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/SearchModal.tsx` around lines 3368 - 3370, Update isSearchModalPortalTarget to detect Base UI menu popups via their semantic selector, such as [role="menu"], instead of relying only on the project-specific .dropdown-content class. Preserve the existing Element/null safety and ensure migrated Menu.Popup instances are recognized so inside-menu presses do not close SearchModal.src/components/charts/ChartControls.tsx (1)
42-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSeveral migrated menu triggers omit
type="button". Moving each trigger into therenderprop dropped the button type on four buttons. The default type issubmit, so any of these inside a form submits it. Most other triggers in this PR settype="button".
src/components/charts/ChartControls.tsx#L42-L49: addtype="button"to the time-range trigger button.src/components/charts/ChartControls.tsx#L72-L85: addtype="button"to the bin-type trigger button.src/components/Breadcrumbs.tsx#L53-L61: addtype="button"to the table-of-contents trigger button.src/components/npm-stats/PackagePills.tsx#L132-L138: addtype="button"to the "More options" trigger button.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/charts/ChartControls.tsx` around lines 42 - 49, Set type="button" on the trigger buttons rendered by Menu.Trigger in ChartControls.tsx lines 42-49 and 72-85, Breadcrumbs.tsx lines 53-61, and PackagePills.tsx lines 132-138, covering the time-range, bin-type, table-of-contents, and “More options” triggers.src/components/Dropdown.tsx (1)
36-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
childrenandrendercan both be set, andchildrenis then dropped.
DropdownItemPropsmarks bothchildrenandrenderoptional. If a call site passes both, this code renders onlyrender. Consider a discriminated union so the type rejects that combination.Also applies to: 125-135
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Dropdown.tsx` around lines 36 - 41, Update DropdownItemProps to a discriminated union that allows either children or render, but not both, and ensure the render path at the referenced DropdownItem usage preserves that mutually exclusive contract. Keep onSelect and className available on both variants while rejecting call sites that provide both content props.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/Breadcrumbs.tsx`:
- Around line 80-82: Update the heading rendering in the Breadcrumbs component
to sanitize heading.text before passing it to dangerouslySetInnerHTML,
preserving supported inline markup while removing unsafe content. Keep the
existing heading output behavior and ensure the sanitized value is used in the
rendered span.
In `@src/components/Dropdown.tsx`:
- Around line 60-70: Update DropdownTrigger to derive nativeButton from render,
passing false whenever the trigger is not a native button while preserving
explicit nativeButton values such as BrandContextMenu’s false setting. Keep
native button triggers using the existing true/default behavior and continue
forwarding className and render to Menu.Trigger.
- Around line 110-136: Update DropdownItem to preserve explicit keyboard
Enter/Space activation through the supported Menu.Item selection mechanism
rather than relying solely on onClick. Keep onSelect wired to the documented
item-selection prop, and ensure the render and non-render branches both preserve
that keyboard behavior.
In `@src/components/ds/ui/index.tsx`:
- Around line 827-857: Sanitize heading content before Breadcrumbs renders it
through dangerouslySetInnerHTML. Update collectHeadingsFromBlocks or the
MarkdownHeading construction path to sanitize MarkdownHeading.text while
preserving the existing heading markup and navigation behavior; ensure
Breadcrumbs only receives sanitized HTML.
In `@src/components/npm-stats/NPMStatsChart.tsx`:
- Around line 1747-1751: Update the height input handling around the onChange
callback to preserve an empty raw value while the user edits, instead of
converting it to 0 and clamping it to 240. Keep the existing 240–1200 bounds for
valid numeric input, and apply the minimum or other normalization on blur so
clearing the controlled field remains possible.
- Around line 1737-1755: Update the iframe-height input and the read-only input
and textarea in the Menu.Popup content to add an onKeyDown handler that stops
propagation, matching the existing PackagePills pattern, so field keyboard
interactions—including arrow keys, Escape, and digits—are not captured by menu
typeahead.
In `@src/components/npm-stats/PackagePills.tsx`:
- Around line 180-271: Update the Menu.Item handlers in PackagePills, including
the visibility item, color item, sub-package items, and add-packages item, to
use Base UI’s closeOnClick behavior instead of preventDefault. Set
closeOnClick={false} on actions that must keep the menu open, especially
onToggleVisibility and repeated sub-package actions, and remove the now-inert
e.preventDefault() calls; preserve closing behavior for one-shot actions if
appropriate.
In `@src/components/Select.tsx`:
- Around line 66-74: Update the selected option label span in Select to include
min-w-0 and flex-1 so long labels can shrink, and add right padding to reserve
space for the CaretUpDownIcon. Keep the existing truncation styling and caret
positioning unchanged.
In `@src/routes/ds.dropdown.tsx`:
- Line 33: Update the description in the route metadata to reference
src/components/ds/ui/index.tsx instead of src/components/Dropdown.tsx, keeping
the rest of the description unchanged.
---
Nitpick comments:
In `@src/components/charts/ChartControls.tsx`:
- Around line 42-49: Set type="button" on the trigger buttons rendered by
Menu.Trigger in ChartControls.tsx lines 42-49 and 72-85, Breadcrumbs.tsx lines
53-61, and PackagePills.tsx lines 132-138, covering the time-range, bin-type,
table-of-contents, and “More options” triggers.
In `@src/components/Dropdown.tsx`:
- Around line 36-41: Update DropdownItemProps to a discriminated union that
allows either children or render, but not both, and ensure the render path at
the referenced DropdownItem usage preserves that mutually exclusive contract.
Keep onSelect and className available on both variants while rejecting call
sites that provide both content props.
In `@src/components/npm-stats/PackagePills.tsx`:
- Around line 243-254: Update the sub-package menu structure around the remove
button so the remove action is rendered as its own sibling Menu.Item rather than
a nested button inside another menuitem. Preserve the existing onRemoveFromGroup
arguments and stopPropagation behavior while ensuring the action is
independently keyboard reachable.
- Around line 197-204: Update the Menu.Item onClick handler around onColorClick
to use the actual Base UI event parameter type, removing the unknown
React.MouseEvent cast and passing e directly. Preserve the existing
preventDefault call and arguments to onColorClick.
In `@src/components/SearchModal.tsx`:
- Around line 3368-3370: Update isSearchModalPortalTarget to detect Base UI menu
popups via their semantic selector, such as [role="menu"], instead of relying
only on the project-specific .dropdown-content class. Preserve the existing
Element/null safety and ensure migrated Menu.Popup instances are recognized so
inside-menu presses do not close SearchModal.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 646d5086-9917-4679-8e27-613f02b78fea
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (32)
package.jsonsrc/components/AuthenticatedUserMenu.tsxsrc/components/AvatarCropModal.tsxsrc/components/BlogAuthorFilter.tsxsrc/components/BrandContextMenu.tsxsrc/components/Breadcrumbs.tsxsrc/components/CopyPageDropdown.tsxsrc/components/Dropdown.tsxsrc/components/LibrariesBrowser.tsxsrc/components/LibrariesOverlay.tsxsrc/components/LibraryLayout.tsxsrc/components/LoginModal.tsxsrc/components/Navbar.tsxsrc/components/SearchModal.tsxsrc/components/Select.tsxsrc/components/application-builder/parts.tsxsrc/components/charts/ChartControls.tsxsrc/components/ds/BrandAssets.tsxsrc/components/ds/ui/index.tsxsrc/components/npm-stats/BaselineSection.tsxsrc/components/npm-stats/ChartControls.tsxsrc/components/npm-stats/LatestBucketNavigator.tsxsrc/components/npm-stats/NPMStatsChart.tsxsrc/components/npm-stats/PackagePills.tsxsrc/components/shop/CartDrawer.tsxsrc/routes/ds.buttons.tsxsrc/routes/ds.cards.tsxsrc/routes/ds.dropdown.tsxsrc/routes/ds.iconography.lazy.tsxsrc/routes/stats/npm/index.tsxsrc/styles/app.csssrc/ui/Tooltip.tsx
| <span | ||
| dangerouslySetInnerHTML={{ __html: heading.text }} | ||
| /> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Trace where MarkdownHeading values are produced.
rg -n 'MarkdownHeading' --type=ts --type=tsx src | head -30
rg -nP -C 4 '\bheadings\s*[:=]' --type=ts --type=tsx src | head -60Repository: TanStack/tanstack.com
Length of output: 223
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect repository files and locate the Breadcrumbs component plus heading usage.
printf 'Tracked files matching Breadcrumbs: '; git ls-files | grep -E '(^|/)Breadcrumbs\.tsx$' || true
echo
printf 'Tracked TypeScript/TSX files containing MarkdownHeading/headings: '; git ls-files '*.{ts,tsx}' | xargs rg -n 'MarkdownHeading|headings' || true
echo
printf 'Breadcrumbs relevant section: '; wc -l src/components/Breadcrumbs.tsx
sed -n '1,140p' src/components/Breadcrumbs.tsxRepository: TanStack/tanstack.com
Length of output: 13039
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Collect MarkdownHeading producers:\n'
sed -n '1,320p' src/utils/markdown/processor.ts
printf '\nBreadcrumbs callers:\n'
rg -n -C 5 'Breadcrumbs|headings=\{markdown\.headings\}|headings=\w+' src/components src/routes | sed -n '1,220p'
printf '\nDoc and blog routes around markdown parsing:\n'
sed -n '1,90p' src/components/Doc.tsx
sed -n '60,180p' src/routes/blog.$.tsxRepository: TanStack/tanstack.com
Length of output: 23830
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Repo/package markdown dependencies:\n'
git ls-files | grep -E '(^|/)(package\.json)$' | xargs -r rg -n '"`@tanstack/markdown`"|markdown|allowHtml|inlineHtml' || true
printf '\nBlog loader and post source data:\n'
rg -n -C 8 'function\s*\(|export async function|fetchBlogPost|files|read.*blog|markdown|content|content:' src/routes/src/blog src/utils src/components src | sed -n '1,220p'
printf '\nDoc loader content source:\n'
rg -n -C 8 'function\s*\(|export async function|content:|markdown|fetch|read|filePath' src/routes src/components/Doc.tsx src/utils/markdown src/utils | sed -n '1,240p'Repository: TanStack/tanstack.com
Length of output: 14878
🌐 Web query:
TanStack Markdown 0.0.11 allowHtml inlineHtml parser heading
💡 Result:
In TanStack Markdown, raw HTML support is an opt-in feature controlled by the allowHtml option [1][2]. When allowHtml: true is configured in your ParseOptions or RenderOptions, the parser recognizes and includes raw block and inline HTML nodes in the output [3][4]. Regarding your specific areas of interest: 1. allowHtml: This is a boolean option [3][4]. By default, it is set to false, meaning raw HTML is escaped [3][5]. Enabling it requires an explicit trust boundary, as TanStack Markdown does not act as a general HTML sanitizer [5][2]. 2. inlineHtml: Inline HTML is supported only when allowHtml is enabled [1]. The parser produces an HtmlInlineNode type, which contains the raw inline HTML value [4]. 3. Parser and Headings: The parser handles ATX headings (levels 1-6) and automatically generates duplicate-safe IDs for them [6][1]. You can configure this behavior using the headingIds option [3][4]. Setting headingIds to false disables ID generation, or you can provide a custom function to implement your own ID logic [6]. For security, the library advises keeping allowHtml disabled for untrusted user-generated content [5]. When enabled, renderers use dangerouslySetInnerHTML for the HTML nodes [5]. If you need to render HTML, ensure you have an independent defense layer or sanitization policy in place if the source is not trusted [5].
Citations:
- 1: https://tanstack.com/markdown/latest/docs/core-concepts/syntax-profile
- 2: https://tanstack.com/markdown/latest
- 3: https://tanstack.com/markdown/latest/docs/reference/parser
- 4: https://tanstack.com/markdown/latest/docs/reference/types
- 5: https://tanstack.com/markdown/latest/docs/core-concepts/security
- 6: https://tanstack.com/markdown/latest/docs/core-concepts/parsing
Sanitize or avoid dangerouslySetInnerHTML for heading text.
allowHtml: true is enabled in parseSiteMarkdown, so parsed headings can include raw inline HTML. inlineText ignores HtmlInlineNode, but this should not be relied on once raw HTML support is opted in; render headings safely before passing them to dangerouslySetInnerHTML.
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 80-80: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation
(react-unsafe-html-injection)
🪛 React Doctor (0.9.3)
[error] 81-81: dangerouslySetInnerHTML is an XSS hole that runs attacker-controlled HTML in your users' browsers.
Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.
(no-danger)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Breadcrumbs.tsx` around lines 80 - 82, Update the heading
rendering in the Breadcrumbs component to sanitize heading.text before passing
it to dangerouslySetInnerHTML, preserving supported inline markup while removing
unsafe content. Keep the existing heading output behavior and ensure the
sanitized value is used in the rendered span.
Source: Linters/SAST tools
| export function DropdownTrigger({ | ||
| children, | ||
| render, | ||
| className, | ||
| asChild = true, | ||
| nativeButton, | ||
| }: DropdownTriggerProps) { | ||
| return ( | ||
| <DropdownMenu.Trigger asChild={asChild} className={className}> | ||
| {children} | ||
| </DropdownMenu.Trigger> | ||
| <Menu.Trigger | ||
| className={className} | ||
| render={render} | ||
| nativeButton={nativeButton} | ||
| /> |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# List every DropdownTrigger call site and the element passed to render.
rg -nP -A 6 '<DropdownTrigger\b' --type=tsx --type=ts srcRepository: TanStack/tanstack.com
Length of output: 191
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | rg '(^|/)Dropdown\.tsx$|Dropdown' || true
echo
echo "Call sites:"
rg -n -A 8 '<DropdownTrigger\b' -g '*.tsx' -g '*.ts' src || true
echo
echo "Trigger implementation:"
fd -a 'Dropdown\.tsx' src | while read -r f; do
echo "--- $f"
wc -l "$f"
sed -n '1,140p' "$f"
doneRepository: TanStack/tanstack.com
Length of output: 26493
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Dropdown imports:"
fd -a 'Dropdown\.tsx$|AuthenticatedUserMenu\.tsx$|BrandContextMenu\.tsx$' src | while read -r f; do
echo "--- $f"
sed -n '1,80p' "$f"
done
echo
echo "Non-button/non-explicit nativeButton call sites:"
rg -n -B 3 -A 10 '<DropdownTrigger\b' -g '*.tsx' -g '*.ts' src \
| rg -v 'nativeButton|type="button"|<button|<Button' \
|| true
echo
echo "Node package metadata for Base UI version:"
for f in package.json src/**/package.json; do
[ -f "$f" ] && { echo "--- $f"; rg -n '"`@base-ui/react`"|"`@base-ui`|`@base-ui/react/menu`' "$f" || true; }
doneRepository: TanStack/tanstack.com
Length of output: 32396
🌐 Web query:
Base UI React Menu Trigger nativeButton default false props
💡 Result:
In the Base UI React Menu component, the Menu.Trigger component has a nativeButton prop, which defaults to true, not false [1]. When nativeButton is set to true (the default), the Menu.Trigger renders as a native HTML element [1][2]. This provides standard button behavior, including focus management and appropriate ARIA attributes for accessibility. If you explicitly set nativeButton to false, the component will not automatically render as a element, allowing for greater customization of the underlying rendered element [1]. However, in most standard use cases, leaving it at its default value of true is recommended to ensure accessibility and expected interaction patterns [3][2].
Citations:
- 1: https://app.unpkg.com/@base-ui/react@1.6.0/files/menu/trigger/MenuTrigger.mjs
- 2: https://app.unpkg.com/@base-ui/react@1.6.0/files/menu/trigger/MenuTrigger.d.ts
- 3: https://mui-base-ui.mintlify.app/components/menu
Derive nativeButton from the trigger element.
Menu.Trigger defaults nativeButton to true, so AuthenticatedUserMenu’s <div> trigger gets button semantics. Preserve non-native triggers by setting nativeButton in DropdownTrigger whenever render is not a native button, and keep BrandContextMenu’s explicit nativeButton={false}.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Dropdown.tsx` around lines 60 - 70, Update DropdownTrigger to
derive nativeButton from render, passing false whenever the trigger is not a
native button while preserving explicit nativeButton values such as
BrandContextMenu’s false setting. Keep native button triggers using the existing
true/default behavior and continue forwarding className and render to
Menu.Trigger.
| export function DropdownItem({ | ||
| children, | ||
| className, | ||
| onSelect, | ||
| asChild, | ||
| render, | ||
| }: DropdownItemProps) { | ||
| const itemClassName = twMerge( | ||
| 'flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 outline-none', | ||
| 'text-sm text-gray-700 dark:text-gray-300', | ||
| 'hover:bg-gray-100 dark:hover:bg-gray-700/50', | ||
| 'data-highlighted:bg-gray-100 dark:data-highlighted:bg-gray-700/50', | ||
| 'transition-colors duration-150', | ||
| className, | ||
| ) | ||
|
|
||
| if (render) { | ||
| return ( | ||
| <Menu.Item onClick={onSelect} className={itemClassName} render={render} /> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <DropdownMenu.Item | ||
| asChild={asChild} | ||
| onSelect={onSelect} | ||
| className={twMerge( | ||
| 'flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 outline-none', | ||
| 'text-sm text-gray-700 dark:text-gray-300', | ||
| 'hover:bg-gray-100 dark:hover:bg-gray-700/50', | ||
| 'focus:bg-gray-100 dark:focus:bg-gray-700/50', | ||
| 'transition-colors duration-150', | ||
| className, | ||
| )} | ||
| > | ||
| <Menu.Item onClick={onSelect} className={itemClassName}> | ||
| {children} | ||
| </DropdownMenu.Item> | ||
| </Menu.Item> | ||
| ) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the installed Base UI Menu.Item typings for selection-related props.
fd -t d -H 'base-ui*' node_modules 2>/dev/null | head
fd -t f 'index.d.ts' node_modules/@base-ui 2>/dev/null | head -20
rg -n 'onClick|closeOnClick|nativeButton|MenuItemProps' node_modules/@base-ui --glob '*.d.ts' -m 40 2>/dev/null | head -40Repository: TanStack/tanstack.com
Length of output: 159
🌐 Web query:
Base UI Menu.Item keyboard Enter selection onClick prop documentation
💡 Result:
In Base UI, the Menu.Item component provides an onClick prop that serves as the primary click handler for the item [1][2]. When a Menu.Item is selected via keyboard interaction—specifically by pressing the Enter key—it is expected to trigger this onClick handler [1]. However, developers have reported inconsistencies where the onClick event may not fire as expected when using custom render overrides or specific underlying elements (such as links) [3][4]. Key technical considerations include: 1. Standard Behavior: By default, Menu.Item renders an element that manages interaction states [2]. When the item is focused and Enter is pressed, the component handles the activation, which should trigger the onClick event [1]. 2. Troubleshooting Custom Rendering: If you are using the render prop to replace the default element, you must ensure that the underlying component correctly propagates event handlers [3]. In some configurations, particularly when overriding the default element, you may need to explicitly ensure that the Enter key correctly triggers the component's internal activation logic, or verify that the native event propagation is not being blocked [3][4]. 3. Link Navigation: When rendering a link (e.g., an tag) as a Menu.Item, pressing Enter may sometimes close the menu without triggering the navigation [4]. This can occur if the internal activation logic suppresses default browser behaviors or if the event handling requires manual intervention to bridge the gap between Base UI's activation and the link's standard behavior [4][5]. 4. Disabled State: Disabled items are focusable via keyboard navigation but cannot be activated, meaning the onClick handler will not be called [6][7]. If your onClick handler is not firing on Enter, ensure that your custom component is correctly forwarding events and that you are not inadvertently intercepting the activation sequence [3]. For advanced cases involving complex navigation or custom components, you may need to use a ref to manually trigger actions or handle specific keydown events to ensure accessibility and expected behavior [5].
Citations:
- 1: https://base-ui.com/react/components/menu
- 2: https://cdn.jsdelivr.net/npm/@base-ui/react@1.3.0/menu/item/MenuItem.d.ts
- 3: [menu]
MenuItemwithrender:onClickis not called for keyboard users mui/base-ui#2281 - 4: [menu] Cannot select menu item via enter key when rendered as link mui/base-ui#1225
- 5: [menu] Should link menu items navigate to their
hrefwhen pressing Space? mui/base-ui#1746 - 6: https://github.com/mui/base-ui/blob/d81ec002/packages/react/src/menu/item/MenuItem.test.tsx
- 7: [menu] Implement WCAG-compliant focus management for disabled items mui/base-ui#2573
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package references =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
[ -e "$f" ] && { echo "--- $f"; rg -n '"`@base-ui/react`"|`@base-ui/react`' "$f" | head -20 || true; }
done
echo "== installed package manifests, if present =="
fd -t f 'package.json' node_modules/@base-ui 2>/dev/null | while read -r f; do
echo "--- $f"
sed -n '1,100p' "$f"
done
echo "== installed types/tests if present =="
rg -n 'onClick|onActivate|activate|keyboard|Enter|Space|onKeyDown' node_modules/@base-ui 2>/dev/null --glob '*.{js,jsx,ts,tsx,d.ts}' -m 80 || true
echo "== relevant source references in repo =="
fd -t f 'Dropdown.tsx|package.json|pnpm-lock.yaml' . | sed -n '1,120p'
rg -n 'DropdownItem|Menu\.Item|Dropdown' src --glob '*.{ts,tsx}' -m 50 || trueRepository: TanStack/tanstack.com
Length of output: 457
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== base-ui package manifests if present =="
fd -t f 'package.json' node_modules/@base-ui 2>/dev/null | while read -r f; do
echo "--- $f"
sed -n '1,120p' "$f"
done
echo "== package location and installed files =="
fd -a 'base-ui|`@base`+ui|*.d.ts' node_modules 2>/dev/null | sed -n '1,120p' || true
echo "== installed type/check tests mentioning click/key activation =="
rg -n 'onClick|onActivate|activate|keyboard|Enter|Space|onKeyDown|MenuItemProps' node_modules/@base-ui 2>/dev/null --glob '*.{js,jsx,ts,tsx,d.ts}' -m 120 || true
echo "== repo package references and Dropdown file =="
sed -n '1,80p' package.json
sed -n '90,145p' src/components/Dropdown.tsxRepository: TanStack/tanstack.com
Length of output: 202
🌐 Web query:
Base UI Menu.Item onClick fires Enter activation default behavior @base-ui/react``
💡 Result:
In @base-ui/react, the Menu.Item component is designed to trigger its onClick handler when activated via the Enter key (or Space, depending on the configuration and element type) [1][2]. However, users have reported issues where the onClick handler is not correctly fired for keyboard users in certain scenarios, particularly when using the render prop to customize the underlying element [3][4]. If you are encountering this issue, please consider the following points based on the current state of Base UI: 1. Native Element Behavior: By default, Menu.Item renders a div element. If you replace the default element with a button-like component using the render prop, you may need to ensure proper handling of native button behaviors [3]. Some issues related to Enter key activation have been traced back to event.preventDefault calls within internal hooks like useButton, which can inadvertently block the activation of custom elements or links [4]. 2. Known Issues: There are active GitHub issues tracking inconsistent behavior when rendering custom components (such as links) or when the internal button logic interacts poorly with specific rendered elements [3][4][1]. 3. Debugging: If your onClick handler is not firing, verify if your custom component requires additional props or if the default event propagation is being interrupted [3]. You can also test with the default element to confirm if the issue is specific to your custom rendering logic [3]. For the most reliable behavior, ensure that any custom component passed to the render prop correctly manages its own interactive state, and check the official Base UI repository for recent fixes or status updates on these specific keyboard navigation issues [3][4].
Citations:
- 1: [menu] Should link menu items navigate to their
hrefwhen pressing Space? mui/base-ui#1746 - 2: [menu] Disabled menu items get highlighted and can even get clicked (sort of) mui/base-ui#1733
- 3: [menu]
MenuItemwithrender:onClickis not called for keyboard users mui/base-ui#2281 - 4: [menu] Cannot select menu item via enter key when rendered as link mui/base-ui#1225
Keep keyboard item activation on onSelect.
@base-ui/react 1.7.0 does not expose a supported MenuItemProps.onSelect replacement, but relying on onClick makes keyboard selection depend on synthesized click behavior. Preserve keyboard Enter/Space activation explicitly rather than replacing Radix’s documented selection prop.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Dropdown.tsx` around lines 110 - 136, Update DropdownItem to
preserve explicit keyboard Enter/Space activation through the supported
Menu.Item selection mechanism rather than relying solely on onClick. Keep
onSelect wired to the documented item-selection prop, and ensure the render and
non-render branches both preserve that keyboard behavior.
| <DropdownTrigger | ||
| render={ | ||
| <button | ||
| className={twMerge( | ||
| hiddenClass, | ||
| 'inline-flex cursor-pointer items-center gap-1 whitespace-nowrap text-text-muted transition-colors hover:text-text-primary', | ||
| )} | ||
| > | ||
| <span>On this page</span> | ||
| <CaretDownIcon className="h-3.5 w-3.5" /> | ||
| </button> | ||
| } | ||
| /> | ||
| <DropdownContent align="end" sideOffset={8} className={hiddenClass}> | ||
| {headings.map((heading) => ( | ||
| <DropdownItem key={`breadcrumb-toc-${heading.id}`} asChild> | ||
| <Link | ||
| to="." | ||
| hash={heading.id} | ||
| style={{ | ||
| paddingLeft: `${(heading.level - 2) * 0.5 + 0.5}rem`, | ||
| }} | ||
| resetScroll={false} | ||
| hashScrollIntoView={{ behavior: 'smooth' }} | ||
| > | ||
| <span dangerouslySetInnerHTML={{ __html: heading.text }} /> | ||
| </Link> | ||
| </DropdownItem> | ||
| <DropdownItem | ||
| key={`breadcrumb-toc-${heading.id}`} | ||
| render={ | ||
| <Link | ||
| to="." | ||
| hash={heading.id} | ||
| style={{ | ||
| paddingLeft: `${(heading.level - 2) * 0.5 + 0.5}rem`, | ||
| }} | ||
| resetScroll={false} | ||
| hashScrollIntoView={{ behavior: 'smooth' }} | ||
| > | ||
| <span dangerouslySetInnerHTML={{ __html: heading.text }} /> | ||
| </Link> | ||
| } | ||
| /> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline src/components/ds/ui/index.tsx --match Breadcrumbs --view expanded
rg -n -C 4 --type ts --type tsx \
'\bMarkdownHeading\b|dangerouslySetInnerHTML|heading\.text' srcRepository: TanStack/tanstack.com
Length of output: 257
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect relevant components without relying on rg file-type syntax.
printf '--- Breadcrumbs outline ---\n'
ast-grep outline src/components/ds/ui/index.tsx --match Breadcrumbs --view expanded || true
printf '\n--- Relevant symbol matches ---\n'
grep -RInE --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' 'MarkdownHeading|dangerouslySetInnerHTML|heading\.text' src || true
printf '\n--- Candidate file sections ---\n'
python3 - <<'PY'
from pathlib import Path
for p in Path('src').rglob('*'):
if p.is_file() and p.suffix in {'.ts','.tsx','.js','.jsx'}:
try:
text = p.read_text(encoding='utf-8')
except UnicodeDecodeError:
continue
lines = text.splitlines()
hits=None
for i,l in enumerate(lines):
if 'MarkdownHeading' in l or 'dangerouslySetInnerHTML' in l or 'heading.text' in l:
if hits is None: hits=[]
hits.append(i)
if hits:
print(f'\n--- {p} ---')
for i,h in enumerate(hits):
a=max(0,h-6); b=min(len(lines),h+8)
print(f'@@ lines {a+1}-{b}')
for n in range(a,b):
print(f'{n+1}: {lines[n]}')
PYRepository: TanStack/tanstack.com
Length of output: 250
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect relevant components without relying on rg file-type syntax.
echo '--- Breadcrumbs outline ---'
ast-grep outline src/components/ds/ui/index.tsx --match Breadcrumbs --view expanded || true
echo
echo '--- Relevant symbol matches ---'
grep -RInE --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' 'MarkdownHeading|dangerouslySetInnerHTML|heading\.text' src || true
echo
echo '--- Candidate file sections ---'
python3 - <<'PY'
from pathlib import Path
for p in Path('src').rglob('*'):
if p.is_file() and p.suffix in {'.ts','.tsx','.js','.jsx'}:
try:
text = p.read_text(encoding='utf-8')
except UnicodeDecodeError:
continue
lines = text.splitlines()
hits = None
for i, l in enumerate(lines):
if 'MarkdownHeading' in l or 'dangerouslySetInnerHTML' in l or 'heading.text' in l:
if hits is None:
hits = []
hits.append(i)
if hits:
print(f'\n--- {p} ---')
for h in hits:
a = max(0, h - 6)
b = min(len(lines), h + 8)
print(f'@@ lines {a+1}-{b}')
for n in range(a, b):
print(f'{n+1}: {lines[n]}')
PYRepository: TanStack/tanstack.com
Length of output: 25917
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- package references for `@tanstack/markdown` ---'
grep -RInE '`@tanstack/markdown`' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true
echo
echo '--- markdown processor relevant sections ---'
sed -n '1,260p' src/utils/markdown/processor.ts
echo
echo '--- docs markdown extensions references ---'
find . -path './node_modules' -prune -o \( -type f \( -name '*.ts' -o -name '*.tsx' \) \) -print | xargs grep -lE 'docsMarkdownExtensions|sanitize|rehype|remark|html' 2>/dev/null | head -50 || trueRepository: TanStack/tanstack.com
Length of output: 8734
🌐 Web query:
@tanstack/markdown 0.0.11 parseMarkdown allowHtml extensions sanitize headings text
💡 Result:
In @tanstack/markdown (version 0.0.11), the handling of HTML, extensions, and heading-related text is governed by the core parser and renderer architecture as follows: Allowing HTML: By default, raw HTML is escaped for security. To enable raw block and inline HTML, you must set the allowHtml option to true in the ParseOptions passed to parseMarkdown or the RenderOptions passed to renderHtml [1][2][3]. Even when enabled, the library automatically strips executable URL protocols (such as javascript:) from links and images [4][1][3]. Extensions: Extensions are synchronous hooks used to implement profile-specific syntax [5][6]. You can define them as objects with optional hooks including parseBlock, transformDocument, transformInline, and renderHtml [5][7][6]. Extensions are passed as an array to the extensions option in ParseOptions [2][7]. The renderHtml hook allows you to provide custom HTML for nodes; note that HTML returned by these custom hooks is trusted and not automatically sanitized by the library [5]. Headings: The parser supports ATX headings (levels 1-6) [3]. By default, the parser generates lowercase, duplicate-safe IDs for headings [2][6]. You can customize this behavior via the headingIds option in ParseOptions, which accepts either a boolean (to disable) or a custom function that receives the heading text and the normalized line index to return a unique ID string [2][6]. When rendering, you can optionally enable anchor links by setting headingAnchors: true in the render options [8]. Sanitization: TanStack Markdown does not include a general-purpose HTML sanitizer [4][3]. Its security model relies on escaping raw HTML by default and stripping unsafe URL schemes [4][1]. Because the library prioritizes a lightweight, synchronous architecture, it assumes that any raw HTML or custom extension-generated HTML provided to the renderer is trusted [4][5]. [4][1][2][5][6][3]
Citations:
- 1: https://tanstack.com/markdown/latest/docs/overview
- 2: https://tanstack.com/markdown/latest/docs/reference/parser
- 3: https://tanstack.com/markdown/latest/docs/core-concepts/syntax-profile
- 4: https://tanstack.com/markdown/latest
- 5: https://tanstack.com/markdown/latest/docs/guides/extensions
- 6: https://tanstack.com/markdown/latest/docs/core-concepts/parsing
- 7: https://tanstack.com/markdown/latest/docs/reference/types
- 8: https://tanstack.com/markdown/latest/docs/reference/html
Sanitize MarkdownHeading.text before rendering it as HTML.
parseSiteMarkdown enables allowHtml, and collectHeadingsFromBlocks saves inline markdown as heading.text; Breadcrumbs later passes that value through dangerouslySetInnerHTML. Keep this rendering only if MarkdownHeading stores sanitized text.
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 853-853: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation
(react-unsafe-html-injection)
🪛 React Doctor (0.9.3)
[error] 854-854: dangerouslySetInnerHTML is an XSS hole that runs attacker-controlled HTML in your users' browsers.
Render trusted content as React children so attacker-controlled HTML cannot run in users' browsers.
(no-danger)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ds/ui/index.tsx` around lines 827 - 857, Sanitize heading
content before Breadcrumbs renders it through dangerouslySetInnerHTML. Update
collectHeadingsFromBlocks or the MarkdownHeading construction path to sanitize
MarkdownHeading.text while preserving the existing heading markup and navigation
behavior; ensure Breadcrumbs only receives sanitized HTML.
Source: Linters/SAST tools
| <Menu.Popup className="z-50 w-[min(420px,calc(100vw-2rem))] rounded-md bg-white p-3 text-gray-900 shadow-lg dark:bg-gray-800 dark:text-gray-100"> | ||
| <div className="space-y-3"> | ||
| <div className="flex items-center justify-between gap-3"> | ||
| <div className="text-xs font-medium">Embed chart</div> | ||
| <label className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-300"> | ||
| <span>Iframe height</span> | ||
| <input | ||
| className="w-16 rounded border border-gray-500/20 bg-gray-50 px-1.5 py-1 text-right font-mono text-[11px] outline-none focus:border-blue-500 dark:bg-gray-900" | ||
| max={1200} | ||
| min={240} | ||
| onChange={(event) => { | ||
| const nextHeight = Number(event.currentTarget.value) | ||
| if (!Number.isFinite(nextHeight)) return | ||
| setIframeHeight(Math.max(240, Math.min(1200, nextHeight))) | ||
| }} | ||
| type="number" | ||
| value={iframeHeight} | ||
| /> | ||
| </label> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Text entry inside the menu popup can be captured by menu typeahead.
Menu.Popup applies composite navigation and printable-character typeahead to its subtree. The iframe-height <input> here does not stop key events. The same pattern in src/components/npm-stats/PackagePills.tsx at lines 169-174 calls event.stopPropagation() on onKeyDown for exactly this reason. Apply the same guard to the number input, and to the read-only <input> and <textarea> below, so that arrow keys, Escape, and digits reach the fields.
🐛 Proposed fix
<input
className="w-16 rounded border border-gray-500/20 bg-gray-50 px-1.5 py-1 text-right font-mono text-[11px] outline-none focus:border-blue-500 dark:bg-gray-900"
max={1200}
min={240}
onChange={(event) => {
const nextHeight = Number(event.currentTarget.value)
if (!Number.isFinite(nextHeight)) return
setIframeHeight(Math.max(240, Math.min(1200, nextHeight)))
}}
+ onClick={(event) => event.stopPropagation()}
+ onKeyDown={(event) => event.stopPropagation()}
type="number"
value={iframeHeight}
/>🧰 Tools
🪛 React Doctor (0.9.3)
[error] 1748-1748: Coercing an input's value with this parse stores 0 for a cleared field and NaN for partial input, which then flows into state or a request body; guard the empty and NaN cases (for example value ? Number(value) : undefined) before using it.
Guard Number(e.target.value) / parseInt(e.target.value) against empty and NaN before storing it. Number('') is 0 and Number('abc') is NaN, both of which silently ship a wrong value.
(no-unguarded-numeric-input-parse)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/npm-stats/NPMStatsChart.tsx` around lines 1737 - 1755, Update
the iframe-height input and the read-only input and textarea in the Menu.Popup
content to add an onKeyDown handler that stops propagation, matching the
existing PackagePills pattern, so field keyboard interactions—including arrow
keys, Escape, and digits—are not captured by menu typeahead.
| onChange={(event) => { | ||
| const nextHeight = Number(event.currentTarget.value) | ||
| if (!Number.isFinite(nextHeight)) return | ||
| setIframeHeight(Math.max(240, Math.min(1200, nextHeight))) | ||
| }} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
A cleared height field snaps to 240 and blocks further editing.
Number('') is 0, and Number.isFinite(0) is true. The clamp then stores 240. The user cannot clear the field to type a new value, because the controlled value immediately becomes 240.
Keep the raw text in state, or clamp only on blur.
🐛 Proposed fix
onChange={(event) => {
- const nextHeight = Number(event.currentTarget.value)
- if (!Number.isFinite(nextHeight)) return
- setIframeHeight(Math.max(240, Math.min(1200, nextHeight)))
+ const raw = event.currentTarget.value
+ if (raw === '') return
+ const nextHeight = Number(raw)
+ if (!Number.isFinite(nextHeight)) return
+ setIframeHeight(nextHeight)
}}
+ onBlur={() =>
+ setIframeHeight((current) =>
+ Math.max(240, Math.min(1200, current)),
+ )
+ }🧰 Tools
🪛 React Doctor (0.9.3)
[error] 1748-1748: Coercing an input's value with this parse stores 0 for a cleared field and NaN for partial input, which then flows into state or a request body; guard the empty and NaN cases (for example value ? Number(value) : undefined) before using it.
Guard Number(e.target.value) / parseInt(e.target.value) against empty and NaN before storing it. Number('') is 0 and Number('abc') is NaN, both of which silently ship a wrong value.
(no-unguarded-numeric-input-parse)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/npm-stats/NPMStatsChart.tsx` around lines 1747 - 1751, Update
the height input handling around the onChange callback to preserve an empty raw
value while the user edits, instead of converting it to 0 and clamping it to
240. Keep the existing 240–1200 bounds for valid numeric input, and apply the
minimum or other normalization on blur so clearing the controlled field remains
possible.
Source: Linters/SAST tools
| <span className="truncate font-medium"> | ||
| {selectedOption.label} | ||
| </span> | ||
| ) : selectedOption.logo ? ( | ||
| <span className="flex items-center justify-center w-6 h-6 rounded border border-gray-500/20"> | ||
| <img | ||
| height={16} | ||
| width={16} | ||
| src={selectedOption.logo} | ||
| alt={`${selectedOption.label} logo`} | ||
| <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> | ||
| <CaretUpDownIcon | ||
| className="h-4 w-4 opacity-40" | ||
| aria-hidden="true" | ||
| /> | ||
| </span> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the selected label shrink and reserve caret space.
The label can overlap the caret or overflow the trigger for a long option label. Add min-w-0 flex-1 and right padding to the label.
Proposed fix
- <span className="truncate font-medium">
+ <span className="min-w-0 flex-1 truncate pr-6 font-medium">
{selectedOption.label}
</span>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span className="truncate font-medium"> | |
| {selectedOption.label} | |
| </span> | |
| ) : selectedOption.logo ? ( | |
| <span className="flex items-center justify-center w-6 h-6 rounded border border-gray-500/20"> | |
| <img | |
| height={16} | |
| width={16} | |
| src={selectedOption.logo} | |
| alt={`${selectedOption.label} logo`} | |
| <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> | |
| <CaretUpDownIcon | |
| className="h-4 w-4 opacity-40" | |
| aria-hidden="true" | |
| /> | |
| </span> | |
| <span className="min-w-0 flex-1 truncate pr-6 font-medium"> | |
| {selectedOption.label} | |
| </span> | |
| <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> | |
| <CaretUpDownIcon | |
| className="h-4 w-4 opacity-40" | |
| aria-hidden="true" | |
| /> | |
| </span> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/Select.tsx` around lines 66 - 74, Update the selected option
label span in Select to include min-w-0 and flex-1 so long labels can shrink,
and add right padding to reserve space for the CaretUpDownIcon. Keep the
existing truncation styling and caret positioning unchanged.
…nstead of 'preventDefault'
…th 'nativeButton={false}'
Replaces all
@radix-uiprimitives with Base UI and removes the three@radix-ui/react-{dialog,dropdown-menu,tooltip}dependencies.Component mapping
Dialog.OverlayDialog.BackdropDialog.ContentDialog.PopupDropdownMenu.ContentMenu.Positioner+Menu.PopupTooltip.ContentTooltip.Positioner+Tooltip.PopupTooltip.ProviderdelayDuration/skipDelayDurationdelay/timeoutDropdownMenu.ItemonSelectMenu.ItemonClickonSelect+e.preventDefault()closeOnClick={false}onInteractOutside+preventDefault()onOpenChange+eventDetails.cancel()--radix-*-transform-origin--transform-origin--radix-dropdown-menu-content-available-height--available-heightfocus:styling on itemsdata-highlighted:asChild→renderThe dropdown adapters drop Radix's
asChildboolean in favour of Base UI'srenderelement prop.On triggers
renderis required andchildrenis not in the type. That matters: 22 of 24<DropdownTrigger>call sites passed noasChildprop at all and silently relied on its= truedefault, so a boolean rename would have flipped them to wrapper mode with no compile error. Makingrenderrequired turns every one of those sites into a type error instead.On items
renderstays optional andchildrenis kept — their previous default was already falsy, so wrapper-mode call sites are unchanged.DropdownTriggeralso gains an optionalnativeButtonpassthrough. Base UI defaults it totrue, so a trigger whoserenderis not a native<button>would otherwise be given button semantics — Radix'sasChildhad no such notion. Two call sites neednativeButton={false}:BrandContextMenu, whose trigger is aposition: fixedvirtual anchor<span>, andAuthenticatedUserMenu, whose trigger is a<div>wrapping the avatar.Radix-only workarounds removed
SearchModalno longer needs the machinery that worked around Radix unmounting content during its exit animation — Base UI keeps popups mounted through it natively. Removed:forceMount, theshouldRenderSearchmount gate,searchModalTransitionMs, and thedocument.body.style.pointerEventssave/restore block.That last one was not merely redundant: Radix portaled content outside the body's pointer-events scope, Base UI does not. Left in place it rendered the search modal visible but unclickable.
Radix DOM contracts consumed outside the migrated files
Radix's
data-stateand--radix-*are a DOM contract, so anything reading them breaks when the primitive is swapped — with no compile error. Three places consumed them:src/styles/app.cssanimation selectors for.cart-panel,.cart-overlay,.search-modal-overlay,.search-modal-contentand.dropdown-contentmove from
[data-state='open'|'closed']to[data-open]/[data-closed]. Base UI documents two idioms —data-open/data-closedfor keyframe animations anddata-starting-style/data-ending-stylefor transitions — and these rules are keyframe-based, so they use the former.Tooltipand the builder tooltips animate with transitions and use the latter.PackagePillssized its menu with--radix-dropdown-menu-content-available-height, which no longer resolves;now
--available-height. Confirmed in the browser that Base UI sets it on the positioner and it inherits to the popup.LibraryLayoutkeeps the docs sidebar open while a dropdown launched from inside it is active, viaexpandedMenuRef.current?.querySelector('[data-state="open"]'). Base UI marks the open trigger withdata-popup-open; the popup itself is portaled tobodyand so is out of that subtree, which rules out[data-open]here. Verified against the live sidebar: the new selector matches and the old one does not.The last two were found while merging
main, not bytsc— which is the concrete form of the silent-failure risk this section describes.A fourth landed the same way, caught in review:
PackagePillskept fouronClickhandlers callinge.preventDefault(). Under Radix that cancelled the close; under Base UIpreventDefault()on a plain click handler does nothing, so its group-visibility toggle, colour picker, sub-package toggles and Add Packages would each have dismissed the menu on first use. They now usecloseOnClick={false}likeBaselineSection. The same edit removed anas unknown as React.MouseEventcast that the old signature had required.Other
Menufrom@base-ui/react/menudirectly rather than through a wrapper. An earlier revision of this branch kept aDropdownMenu*adapter so those call sites needed no edits, but three of its four parts only renamed Base UI's API (DropdownMenu→Menu.Root,DropdownMenuTrigger→Menu.Trigger,DropdownMenuItem→Menu.ItemwithonSelectin place ofonClick), which hid the real primitives for no benefit.Portal/Positioner/Popupis now written out at each of the 11 surfaces. The existingDropdown.tsxis untouched — it carries the site's default popup styling, which these call sites do not want./ds/dropdown,/ds/cardsand/ds/buttonscopy and code samples updated to reference Base UI.Verification
tsc, lint and the test suite pass.Exercised in the browser, locally and on the preview deploy: blog author filter (open → select → filter applied),
/ds/dropdown,/ds/buttonssplit button, navbar social menu,CopyPageDropdown, breadcrumb TOC,BrandContextMenu(anchors at cursor),LoginModal,CartDrawer, andSearchModal— including that its framework filter opens inside the modal without dismissing it. Console clean.The preview deploy reaches the npm API, so the stats dropdowns were exercised there: all four
ChartControlsmenus,BaselineSectionpresets,LatestBucketNavigatorplayback, andNPMStatsChartexport/embed.Three things that
tsccannot check were confirmed by reading computed style rather than by eye:FrameworkSelectand evaluating both selectors against that subtree —[data-popup-open]matches,[data-state="open"]does not.PackagePills'max-heightresolves to a real value again (1719px, matching--available-heighton the positioner). With the stale--radix-*variable it computed tonone.CartDrawerplayscart-panel-inon open, then takesdata-closedand playscart-panel-out/cart-overlay-outon close — the CSS attribute rename works in both directions. The search modal's four animations were confirmed the same way viagetAnimations().closeOnClick={false}was checked where it replaces Radix'sonSelect+preventDefault()idiom: clicking a baseline preset, and clicking the custom-ms input inside the playback menu, both leave the menu open.Keyboard activation was checked directly, since
Menu.ItemtakesonClickwhere Radix tookonSelect: focusing an item in the blog author menu and pressing Enter, and separately Space, both apply the filter and close the menu. Base UI handles that internally — no extra key handling was needed.Not exercised:
AuthenticatedUserMenuandAvatarCropModal(need auth) andcharts/ChartControls(admin only). Each is the same code path as something above —AvatarCropModaldiffers from the verifiedLoginModalonly inmax-w-mdvsmax-w-xs,AuthenticatedUserMenuuses the sameDropdownItem render={...}as the navbar social menu, andcharts/ChartControlshas the sameMenu.Positionerline as the verifiednpm-stats/ChartControls. The one thing that inference does not cover isAvatarCropModal'sreact-easy-cropwidget interacting with Base UI's focus trap; the closest evidence isSearchModal's nested filter menu working.Summary by CodeRabbit
Refactor
Documentation