컴포넌트

Navigation Item

아이콘과 레이블로 탐색 항목을 표시합니다.

설치

pnpm dlx ply-ui add navigation-item badge

공통 설치 설정을 먼저 적용합니다.

사용법

화면 안의 작업은 NavigationItem, 주소 이동은 NavigationLink로 구성합니다. active는 현재 위치 표시입니다. leading이 있으면 icon 대신 그 내용을 표시합니다.

페이지 이동

NavigationLink는 실제 주소를 가진 링크입니다. active로 현재 페이지를 표시합니다.

보조 정보

trailing으로 개수나 상태를 표시합니다. 행 안에는 다른 버튼을 넣지 않습니다.

비활성

NavigationItem.disabled로 작업을 막습니다. 링크의 비활성 처리는 제공하지 않습니다.

속성

이 프로젝트에서 추가하거나 제한한 속성입니다.

속성

타입

속성

타입

구현 코드

components/ui/navigation-item.tsx
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
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;
  }
}

목차