Định vị (Positioning)
Popover, Menu, Tooltip, HoverCard và listbox dùng hệ định vị nổi của Radix: chọn cạnh, căn lề, khoảng cách — và tự lật khi thiếu chỗ.
Các prop định vị
PopoverSurface, MenuPopover, MenubarContent, TooltipContent, HoverCardContent chuyển thẳng các prop này cho Radix Popper (ContextMenu mở tại con trỏ nên chỉ nhận nhóm prop va chạm).
| Prop · Kiểu | Mặc định | Mô tả |
|---|---|---|
| side"top" | "right" | "bottom" | "left" | Mặc định: "bottom" (Tooltip: "top") | Cạnh của trigger mà bề mặt bám vào. Là hướng vật lý: left / right không tự đảo khi RTL. |
| align"start" | "center" | "end" | Mặc định: PopoverSurface "center" · MenuPopover "start" | Căn theo trục còn lại. start / end theo hướng chữ — Yugen gắn dir của provider lên bề mặt nên RTL tự đảo. |
| sideOffsetnumber | Mặc định: Popover 6 · HoverCard 6 · Menu 4 · Tooltip 4 | Khoảng cách (px) giữa trigger và bề mặt. |
| alignOffsetnumber | Mặc định: 0 | Dịch theo trục căn lề (dương = về phía end). |
| avoidCollisionsboolean | Mặc định: true | Tự lật sang cạnh đối diện và trượt dọc trục để không tràn khỏi khung nhìn. |
| collisionPaddingnumber | Partial<Record<Side, number>> | Mặc định: 0 | Khoảng đệm với mép vùng va chạm (ví dụ chừa chỗ cho header sticky). |
| collisionBoundaryElement | Element[] | Mặc định: viewport | Vùng dùng để tính va chạm thay cho khung nhìn. |
| sticky"partial" | "always" | Mặc định: "partial" | Giữ bề mặt trong khung nhìn khi trigger cuộn ra một phần / hoàn toàn. |
| hideWhenDetachedboolean | Mặc định: false | Ẩn bề mặt khi trigger bị che hoàn toàn (ví dụ cuộn ra khỏi vùng cuộn). |
Sau khi định vị, Radix ghi data-side và data-align thực tế (kể cả khi đã lật) lên bề mặt, và cung cấp biến CSS --radix-popover-content-available-height, --radix-popover-trigger-width… (tiền tố đổi theo component: tooltip, dropdown-menu, select, hover-card).
Thử side & align
Chọn giá trị rồi mở popover. Thử side="right" trên màn hình hẹp để thấy bề mặt tự lật.
<Popover>
<PopoverTrigger asChild>
<Button appearance="primary">Mở popover</Button>
</PopoverTrigger>
<PopoverSurface side="right" align="start" sideOffset={16} withArrow>
Nội dung
</PopoverSurface>
</Popover>
{/* Tooltip mặc định side="top" */}
<SimpleTooltip content="Sao chép" side="bottom">
<Button icon={<Copy20Regular />} aria-label="Sao chép" />
</SimpleTooltip>Va chạm & tự lật
Mặc định bề mặt không bao giờ tràn khỏi khung nhìn: thiếu chỗ ở cạnh đã chọn thì lật sang cạnh đối diện, thiếu chỗ theo trục căn thì trượt vào trong.
{/* Chừa chỗ cho thanh trên cùng cao 74px và lề 16px ở các cạnh còn lại */}
<PopoverSurface
side="bottom"
align="start"
collisionPadding={{ top: 80, right: 16, bottom: 16, left: 16 }}
className="max-h-(--radix-popover-content-available-height) overflow-y-auto data-[side=top]:origin-bottom"
>
…
</PopoverSurface>
{/* Bám cố định theo phía đã chọn, chấp nhận bị cắt (hiếm khi cần) */}
<MenuPopover avoidCollisions={false}>…</MenuPopover>- Header sticky không được Radix biết tới — dùng
collisionPaddingở cạnh tương ứng để bề mặt không chui dưới header. - Danh sách dài: giới hạn chiều cao bằng biến
--radix-*-content-available-heightvà cho cuộn, thay vì để bề mặt cao quá khung nhìn (Dropdown đã làm sẵn). - Animation theo hướng xuất hiện: style bằng
data-[side=top]:…vì cạnh thực tế có thể khác cạnh bạn chọn.
Portal & theme
Mọi bề mặt nổi render qua portal vào <body> để thoát overflow: hidden và z-index của cha. Chúng vẫn tự nhận theme, brand và hướng chữ của YugenProvider lồng gần nhất.
Vùng này là YugenProvider theme="dark" brand="web".
<YugenProvider theme="dark" brand="web" className="p-4">
<Popover>
<PopoverTrigger asChild>
<Button appearance="primary">Mở popover</Button>
</PopoverTrigger>
{/* Tự nhận theme tối + brand Web của provider lồng, dù render vào <body> */}
<PopoverSurface withArrow>Nền tối, brand Web.</PopoverSurface>
</Popover>
</YugenProvider>
// Tự viết bề mặt portal mới? Gắn props của provider gần nhất:
import { usePortalThemeProps } from "@/components/ui";
function MyFloatingSurface(props) {
const portalTheme = usePortalThemeProps(); // {} khi ở provider gốc
return <SomePrimitive.Portal><SomePrimitive.Content {...portalTheme} {...props} /></SomePrimitive.Portal>;
}- Theme cho cả ứng dụng: luôn dùng
<YugenProvider applyToDocument>ở gốc — attribute nằm trên<html>nên mọi portal tự đúng. - Theme lồng: mọi bề mặt nổi của Yugen (PopoverSurface, MenuPopover, TooltipContent, HoverCardContent, DialogSurface, DrawerSurface, ContextMenu, Menubar, listbox Dropdown / Combobox / TagPicker) tự gắn
data-theme/data-brand/dircủa provider lồng qua hookusePortalThemeProps(). Bề mặt tự viết thì gọi hook này. - Hướng chữ không cần xử lý: các bề mặt nổi (PopoverSurface, TooltipContent, HoverCardContent, DialogSurface, DrawerSurface, menu và listbox) tự gắn
dircủa provider gần nhất, vì context React (kể cảDirectionProvider) đi xuyên qua portal.
Khoá cuộn
Menu, ContextMenu và Dropdown khoá cuộn trang khi mở: lăn chuột và vuốt bên ngoài bị chặn, thanh cuộn của trang ẩn đi và body được bù lề để nội dung không xê dịch. App chọn giữ hay ẩn thanh cuộn.
| Bề mặt | Khoá cuộn | Tắt được |
|---|---|---|
| Menu (cả MenuButton, SplitButton, mục thừa của Breadcrumb, DataGridColumnPicker) | modal mặc định true | modal={false} |
| ContextMenu | modal mặc định true | modal={false} |
| Dropdown | Luôn khoá | Không; dùng scrollLock để giữ thanh cuộn |
| Menubar | Không (menu của Menubar không modal) | — |
| Popover và component dựa trên Popover (Combobox, TagPicker, TimePicker…) | Không, trừ khi modal | — |
| Dialog, Drawer có lớp phủ, Sheet, Tour | Có | Đúng ý đồ: nội dung phía sau không dùng được |
// Cả app: giữ thanh cuộn khi Menu, ContextMenu, Dropdown mở
<YugenProvider applyToDocument scrollLock="keep-scrollbar">
<App />
</YugenProvider>
// Không dùng provider: đặt attribute trên <html>
<html data-yugen-scroll-lock="keep-scrollbar">
// Một menu không khoá cuộn
<Menu modal={false}>…</Menu>scrollLock="keep-scrollbar"trên provider gốc ghidata-yugen-scroll-locklên<html>: thanh cuộn vẫn hiện và không có gì xê dịch, nhưng lăn chuột / vuốt bên ngoài bề mặt vẫn bị chặn. App không dùng provider thì tự đặt attribute này. Mặc định hiện tại là"hide-scrollbar"; bản 1.0 sẽ đổi sang"keep-scrollbar".modal={false}trên Menu hoặc ContextMenu: trang vẫn cuộn được, bấm ra ngoài tới trang ngay, và phần còn lại của trang không bị ẩn khỏi trình đọc màn hình. Hợp với menu mở từ thanh công cụ trong vùng nội dung dài.- Phần tử
position: fixedsát mép phải (header, nút nổi) xê dịch theo bề rộng thanh cuộn khi thanh cuộn ẩn, trừ khi có classright-scroll-bar-position(hoặcwidth-before-scroll-barcho phần tử rộng hết trang). Vớikeep-scrollbarthì không cần.
Thang z-index
Chỉ dùng vài bậc cố định; các bề mặt cùng bậc xếp chồng theo thứ tự mở (portal mở sau nằm trên).
| Bậc | Dùng cho |
|---|---|
z-30 | Thanh trên cùng sticky (TopBar của site docs). Nội dung sticky của ứng dụng không vượt bậc này. |
z-40 | Lớp phủ của sidebar trên mobile (site docs). |
z-50 | Mọi overlay: lớp phủ + bề mặt Dialog / Drawer, Popover, Menu, Tooltip, HoverCard, listbox Dropdown / Combobox; sidebar mobile. |
z-100 | Vùng Toast — luôn nằm trên cả Dialog để thông báo không bị che. |
- Để bề mặt nổi trong portal của component; dùng collisionPadding cho header sticky.
- Chọn side theo nội dung: Tooltip ở trên, Menu và listbox ở dưới, panel phụ ở cạnh.
- Giữ z-index trong thang 30 / 40 / 50 / 100.
- Không đặt z-index tuỳ ý (z-[…] cỡ hàng nghìn) để “thắng” overlay.
- Không tắt avoidCollisions chỉ để bề mặt luôn ở một phía.
- Không tự viết Portal mà quên usePortalThemeProps() — bề mặt sẽ lấy theme của <html>.