BottomNavigation
창의 아래 가장자리에 고정되어 앱의 주요 목적지를 나란히 보여 주는 바입니다. 글리프 하나와 그 아래 이름 하나가 한 칸이고, 지금 있는 곳에는 aria-current가 붙습니다.
import { BottomNavigation, BottomNavigationItem } from 'neba';
<BottomNavigation label="Main" value={section} onValueChange={setSection}>
<BottomNavigationItem value="home" icon={<HomeIcon />}>
홈
</BottomNavigationItem>
<BottomNavigationItem value="search" icon={<SearchIcon />}>
검색
</BottomNavigationItem>
</BottomNavigation>;Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'outline' | 'text' | 'outline' | 바의 무게. 컨테이너의 방식대로 시트에 색을 들이지 않습니다. 색 계열을 입는 것은 지금 있는 목적지 하나뿐입니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 행의 최소 높이와 글리프, 이름의 스케일. md는 56px입니다 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | 그림자 깊이. 기본이 0입니다. 바는 창 가장자리에 붙어 있지 떠 있지 않고, 내용과의 구분은 divider가 합니다 |
| value | string | number | null | — | 지금 있는 목적지. onValueChange와 함께 제어할 때 씁니다 |
| defaultValue | string | number | null | — | 제어하지 않을 때 처음 선택되는 목적지 |
| onValueChange | (value: string | number) => void | — | 눌린 목적지와 함께 호출됩니다 |
| position공통 | 'static' | 'sticky' | 'fixed' | 'fixed' | 페이지 스크롤 안에서 어떻게 앉는지. 다른 컴포넌트와 달리 fixed가 기본입니다. 하단 내비게이션은 창 아래 가장자리에 고정되는 것이기 때문입니다 |
| labels | 'all' | 'selected' | 'none' | 'all' | 어떤 이름을 그릴지. 그리지 않은 이름도 문서에는 남아 스크린 리더가 읽습니다 |
| divider | boolean | true | 내용을 마주 보는 위쪽 가장자리에 헤어라인을 긋습니다. Toolbar와 반대로 기본이 켜짐입니다 |
| safeArea | boolean | true | env(safe-area-inset-bottom)만큼 아래를 띄워 홈 인디케이터를 피합니다. 시트는 화면 아래 끝까지 그대로 닿습니다 |
| disabled | boolean | false | 모든 목적지가 응답을 멈춥니다 |
| label | string | — | 바가 읽히는 이름, "Main", "Sections" |
| render | useRender.RenderProp | — | nav 대신 다른 요소로 렌더링합니다 (render={<footer />}). Base UI의 render prop 그대로이며, 여기서는 거의 필요하지 않습니다. 목적지의 줄은 내비게이션입니다 |
| children | ReactNode | — | BottomNavigationItem들 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value * | string | number | — | 목적지를 식별합니다. onValueChange가 보고하는 값 |
| icon | ReactNode | — | 이름 위의 글리프 |
| href | string | — | 버튼 대신 링크로 렌더링합니다 |
| target | string | — | href가 열리는 곳. 이 탭이 아니면 rel에 noopener noreferrer가 합쳐집니다 |
| rel | string | — | 링크의 rel. 위 두 토큰과 합쳐지며 덮어쓰지 않습니다 |
| disabled | boolean | false | 사용 불가. 자리는 지킵니다 |
| children | ReactNode | — | 목적지의 이름. labels가 그리지 않을 때에도 읽힙니다 |
나머지 <nav> 속성은 루트로, 나머지 <button> 속성은 각 목적지로 전달됩니다. 예외는 onChange로, 여기서 들을 만한 변화는 onValueChange입니다.
공통 축(variant size color density elevation position)의 의미는 Prop 규약에 있습니다.
예시
position
기본값은 fixed입니다. 라이브러리의 다른 컴포넌트가 모두 static을 기본으로 두는 것과 반대이고, 그것이 이 컴포넌트가 하는 일입니다. 페이지가 무엇을 하든 창의 아래 가장자리에 붙어 있습니다. 그러면 페이지는 첫 화면이 바 뒤에 깔리지 않도록 스스로 아래쪽 여백을 마련해야 합니다.
sticky는 바를 흐름 안에 두되 스크롤 영역의 아래 가장자리에서 멈추게 합니다. static은 그냥 흐름 안의 시트입니다.
import { BottomNavigation, BottomNavigationItem, Typography } from 'neba';
function DotIcon() {
return (
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="4.25" stroke="currentColor" strokeWidth="1.5" />
</svg>
);
}
export default function BottomNavigationPinned() {
return (
<div className="h-64 w-full max-w-xs overflow-y-auto rounded-(--neba-radius-md) border [border-color:var(--neba-border)]">
<div className="flex flex-col gap-4 p-4">
{Array.from({ length: 8 }, (_, index) => (
<Typography key={index}>
Row {index + 1}. Scroll the box: the bar stays against its bottom edge.
</Typography>
))}
</div>
<BottomNavigation position="sticky" defaultValue="feed" safeArea={false}>
<BottomNavigationItem value="feed" icon={<DotIcon />}>
Feed
</BottomNavigationItem>
<BottomNavigationItem value="saved" icon={<DotIcon />}>
Saved
</BottomNavigationItem>
</BottomNavigation>
</div>
);
}labels
all은 모든 이름을 그립니다. selected는 지금 있는 목적지의 이름만, none은 아무 이름도 그리지 않습니다.
그리지 않은 이름도 문서에는 남습니다. 글리프 하나뿐인 버튼은 접근성 이름이 아예 없으므로, 사라지는 것은 픽셀뿐입니다.
import { BottomNavigation, BottomNavigationItem, Typography } from 'neba';
function DiscIcon() {
return (
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="5.25" stroke="currentColor" strokeWidth="1.5" />
<circle cx="8" cy="8" r="1.5" fill="currentColor" />
</svg>
);
}
const modes = ['all', 'selected', 'none'] as const;
export default function BottomNavigationLabels() {
return (
<div className="flex w-full max-w-xs flex-col gap-4">
{modes.map((labels) => (
<div key={labels} className="flex flex-col gap-1">
<Typography level="caption" className="text-(--neba-muted-fg)">
labels="{labels}"
</Typography>
<BottomNavigation position="static" labels={labels} defaultValue="library">
<BottomNavigationItem value="listen" icon={<DiscIcon />}>
Listen
</BottomNavigationItem>
<BottomNavigationItem value="library" icon={<DiscIcon />}>
Library
</BottomNavigationItem>
<BottomNavigationItem value="radio" icon={<DiscIcon />}>
Radio
</BottomNavigationItem>
</BottomNavigation>
</div>
))}
</div>
);
}href
href를 준 목적지는 진짜 <a>가 됩니다. 길게 눌러 새 탭으로 열 수 있고, 주소가 상태 표시줄에 뜨며, 라우터를 부르는 <button>으로는 둘 다 되지 않습니다.
import { BottomNavigation, BottomNavigationItem } from 'neba';
function DocsIcon() {
return (
<svg viewBox="0 0 16 16" fill="none">
<path
d="M4 2.75h5L12 6v7.25H4V2.75Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path d="M8.75 2.75V6H12" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
</svg>
);
}
function GridIcon() {
return (
<svg viewBox="0 0 16 16" fill="none">
<rect
x="2.75"
y="2.75"
width="4.5"
height="4.5"
rx="1"
stroke="currentColor"
strokeWidth="1.5"
/>
<rect
x="8.75"
y="2.75"
width="4.5"
height="4.5"
rx="1"
stroke="currentColor"
strokeWidth="1.5"
/>
<rect
x="2.75"
y="8.75"
width="4.5"
height="4.5"
rx="1"
stroke="currentColor"
strokeWidth="1.5"
/>
<rect
x="8.75"
y="8.75"
width="4.5"
height="4.5"
rx="1"
stroke="currentColor"
strokeWidth="1.5"
/>
</svg>
);
}
export default function BottomNavigationLinks() {
return (
<div className="w-full max-w-xs">
<BottomNavigation position="static" label="Documentation" value="components">
<BottomNavigationItem value="guide" href="/guide/getting-started" icon={<DocsIcon />}>
Guide
</BottomNavigationItem>
<BottomNavigationItem value="components" href="/components/" icon={<GridIcon />}>
Components
</BottomNavigationItem>
<BottomNavigationItem value="design" href="/design/design-language" icon={<DocsIcon />}>
Design
</BottomNavigationItem>
</BottomNavigation>
</div>
);
}variant, divider, safeArea
variant는 다른 컨테이너에서와 같은 말을 합니다. 시트에는 색이 들지 않고, 색 계열을 입는 것은 지금 있는 목적지 하나뿐입니다. divider는 내용을 마주 보는 위쪽 가장자리의 헤어라인이고 기본이 켜짐입니다. safeArea는 env(safe-area-inset-bottom)만큼 아래를 띄워 홈 인디케이터를 피하되, 시트 자체는 화면 아래 끝까지 그대로 닿습니다.
import type { ComponentProps } from 'react';
import { BottomNavigation, BottomNavigationItem, Typography } from 'neba';
function DotIcon() {
return (
<svg viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="4.25" stroke="currentColor" strokeWidth="1.5" />
</svg>
);
}
function Bar({ children, ...props }: ComponentProps<typeof BottomNavigation>) {
return (
<BottomNavigation position="static" defaultValue="two" {...props}>
<BottomNavigationItem value="one" icon={<DotIcon />}>
One
</BottomNavigationItem>
<BottomNavigationItem value="two" icon={<DotIcon />}>
Two
</BottomNavigationItem>
<BottomNavigationItem value="three" icon={<DotIcon />}>
Three
</BottomNavigationItem>
{children}
</BottomNavigation>
);
}
export default function BottomNavigationAppearance() {
return (
<div className="flex w-full max-w-xs flex-col gap-4">
<div className="flex flex-col gap-1">
<Typography level="caption" className="text-(--neba-muted-fg)">
variant="solid" color="success"
</Typography>
<Bar variant="solid" color="success" />
</div>
<div className="flex flex-col gap-1">
<Typography level="caption" className="text-(--neba-muted-fg)">
size="sm" density="compact"
</Typography>
<Bar size="sm" density="compact" />
</div>
<div className="flex flex-col gap-1">
<Typography level="caption" className="text-(--neba-muted-fg)">
variant="text" divider={'{false}'}
</Typography>
<Bar variant="text" divider={false} />
</div>
</div>
);
}제어하기
value를 넘기면 바는 자체 상태를 갖지 않습니다. 라우터가 지금 있는 곳을 알고 있을 때 쓰는 형태입니다.
<BottomNavigation value={pathname} onValueChange={navigate}>
<BottomNavigationItem value="/home" icon={<HomeIcon />}>
홈
</BottomNavigationItem>
</BottomNavigation>접근성
- 루트는
<nav>이고label이 그 이름이 됩니다.role="tablist"가 아닙니다. 탭 목록은 세트 전체에 tab 정지점 하나와 그 안의 방향키 이동을 약속하고, 하단 내비게이션은 패널이 아니라 페이지를 바꿉니다. - 지금 있는 목적지에는
aria-current="page"가 붙습니다. - 각 목적지는 진짜
<button>이거나,href가 있으면 진짜<a>입니다. labels가 이름을 그리지 않아도 그 이름은 문서에 남아 목적지의 접근성 이름이 됩니다.position="fixed"일 때 페이지 아래쪽에 바 높이만큼 여백을 두세요. 첫 화면의 마지막 줄이 바에 가립니다.