컴포넌트
Navigation Item
아이콘과 레이블로 탐색 항목을 표시합니다.
설치
pnpm dlx ply-ui add navigation-item badge공통 설치 설정을 먼저 적용합니다.
사용법
화면 안의 작업은 NavigationItem, 주소 이동은 NavigationLink로 구성합니다. active는 현재 위치 표시입니다. leading이 있으면 icon 대신 그 내용을 표시합니다.
페이지 이동
NavigationLink는 실제 주소를 가진 링크입니다. active로 현재 페이지를 표시합니다.
보조 정보
trailing으로 개수나 상태를 표시합니다. 행 안에는 다른 버튼을 넣지 않습니다.
비활성
NavigationItem.disabled로 작업을 막습니다. 링크의 비활성 처리는 제공하지 않습니다.
속성
이 프로젝트에서 추가하거나 제한한 속성입니다.
NavigationItem
NavigationLink
구현 코드
components/ui/navigation-item.tsx
"use client";
import type { ComponentProps, ReactNode } from "react";
import { Button as Primitive } from "@base-ui/react/button";
import { useRender } from "@base-ui/react/use-render";
import { cx, withClassName } from "../../lib/cx";
import { Icon, type IconName } from "./icon";
import "./navigation-item.css";
type NavigationContentProps = {
icon?: IconName;
/** Avatar 등으로 아이콘 자리를 교체합니다. 내부에 버튼을 넣지 마세요. */
leading?: ReactNode;
trailing?: ReactNode;
children?: ReactNode;
};
export type NavigationItemProps = ComponentProps<typeof Primitive> &
NavigationContentProps & { active?: boolean };
export type NavigationLinkProps = useRender.ComponentProps<
"a",
{ active: boolean }
> &
NavigationContentProps & { active?: boolean };
// 버튼과 링크가 같은 시각 파트를 재사용합니다. 동작은 각 HTML 역할을 보존합니다.
function NavigationContent({
icon,
leading,
trailing,
children,
}: NavigationContentProps) {
const visual = leading ?? (icon ? <Icon name={icon} size={24} /> : null);
return (
<>
{visual && <span className="rbx-navigation-item-leading">{visual}</span>}
<span className="rbx-navigation-item-label">{children}</span>
{trailing && (
<span className="rbx-navigation-item-trailing">{trailing}</span>
)}
</>
);
}
/** 화면 내 선택/작업: Base UI Button. */
export function NavigationItem({
icon,
leading,
trailing,
active = false,
children,
className,
...props
}: NavigationItemProps) {
return (
<Primitive
aria-current={active ? "page" : undefined}
{...props}
data-active={active || undefined}
data-leading={icon || leading ? "true" : undefined}
className={withClassName("rbx-navigation-item", className)}
>
<NavigationContent icon={icon} leading={leading} trailing={trailing}>
{children}
</NavigationContent>
</Primitive>
);
}
/** 페이지 이동: native anchor. render={<Link href="…" />}로 라우터도 조합합니다. */
export function NavigationLink({
icon,
leading,
trailing,
active = false,
children,
className,
render,
ref,
...props
}: NavigationLinkProps) {
return useRender({
defaultTagName: "a",
render,
ref,
state: { active },
props: {
"aria-current": active ? "page" : undefined,
...props,
"data-leading": icon || leading ? "true" : undefined,
className: cx("rbx-navigation-item", className),
children: (
<NavigationContent icon={icon} leading={leading} trailing={trailing}>
{children}
</NavigationContent>
),
},
});
}
components/ui/navigation-item.css
/* Roblox Home / Navigation.css: 40px row, 40px icon slot, 24px glyph. */
.rbx-navigation-item {
box-sizing: border-box;
position: relative;
isolation: isolate;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
height: 40px;
padding-block: 0;
padding-inline: 4px 2px;
border: 0;
border-radius: var(--rbx-radius-medium);
background: transparent;
color: var(--rbx-color-content-emphasis);
font: 700 16px/1.4 var(--rbx-font-body);
text-align: start;
text-decoration: none;
cursor: pointer;
}
/* Overlay preserves the selected background underneath hover/press. */
.rbx-navigation-item::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
pointer-events: none;
transition: background-color var(--rbx-motion-duration-feedback)
var(--rbx-motion-ease-enter);
}
.rbx-navigation-item[data-active],
.rbx-navigation-item[aria-current="page"] {
background: var(--rbx-color-shift-200);
}
@media (hover: hover) {
.rbx-navigation-item:not([data-disabled]):hover::before {
background: var(--rbx-color-state-hover);
}
}
.rbx-navigation-item:not([data-disabled]):active::before {
background: var(--rbx-color-state-press);
}
.rbx-navigation-item:focus-visible {
outline: 3px solid var(--rbx-color-selection-start);
outline-offset: 3px;
}
.rbx-navigation-item[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.rbx-navigation-item-leading {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 40px;
height: 40px;
}
.rbx-navigation-item-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rbx-navigation-item-trailing {
display: flex;
flex-shrink: 0;
margin-inline-start: auto;
}
/* Text-only catalog rows are a BLOCK/UI extension. */
.rbx-navigation-item:not([data-leading]) {
padding-inline: 12px;
}
@media (prefers-reduced-motion: reduce) {
.rbx-navigation-item::before {
transition: none;
}
}