Chuyển tới nội dung

Pro đang mở bán trước. Cài đặt mở từ tháng 1/2027. Xem Pro

Component

BottomNav

Thanh điều hướng capsule nổi ở dưới md, với 3 đến 5 đích cấp cao nhất. Bề mặt glass nằm trên safe area phía dưới, cách một khoảng nhỏ. Nhãn hiện theo mặc định và có thể ẩn khỏi mắt bằng showLabels. Nó tách khỏi Sidebar: app có cả hai thì tự render item của mình trong từng cái. Viewport của trang cần viewport-fit=cover.

  • npm: 1 trực tiếp, 3 tổng
  • registry: 3 trực tiếp, 3 tổng

Xem trước

Không gian làm việc của bạn

Chọn một điểm đến bên dưới.

Props

  • showLabels
  • destinations
  • badges
  • count
import { useState, type MouseEvent } from "react"
import { IconHome, IconHomeFilled, IconMail, IconMailFilled, IconSearch, IconSearchFilled } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"

const [destination, setDestination] = useState("#home")
const selectDestination = (event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); setDestination(event.currentTarget.getAttribute("href") ?? "#home") }

<div data-slot="bottom-nav-preview" className="mx-auto flex h-80 w-[393px] shrink-0 flex-col justify-between overflow-hidden rounded-container border border-border bg-background-surface">
  <div className="flex flex-col gap-xs p-lg">
    <p className="text-sm font-medium">Không gian làm việc của bạn</p>
    <p className="text-xs text-text-secondary">Chọn một điểm đến bên dưới.</p>
  </div>
  <BottomNav label="Chính" value={destination} className="relative md:block">
    <BottomNavItem href="#home" label="Trang chủ" icon={<IconHome size={20} />} activeIcon={<IconHomeFilled size={20} />} onClick={selectDestination} />
    <BottomNavItem href="#search" label="Tìm kiếm" icon={<IconSearch size={20} />} activeIcon={<IconSearchFilled size={20} />} onClick={selectDestination} />
    <BottomNavItem href="#inbox" label="Hộp thư đến" icon={<IconMail size={20} />} activeIcon={<IconMailFilled size={20} />} count={3} onClick={selectDestination} />
  </BottomNav>
</div>

Cách dùng

Khi nào dùng

  • Hiện một danh sách ngắn các đích cấp cao nhất ở dưới màn hình nhỏ.

Khi nào không dùng

  • Đặt các action như Tạo hoặc Chia sẻ vào điều hướng. Dùng Button.
  • Hiện cùng thanh đó cạnh một Sidebar đang thấy.

Ví dụ

Ba đích

Your workspace

Choose a destination below.

import { useState, type MouseEvent } from "react"
import { IconHome, IconHomeFilled, IconMail, IconMailFilled, IconSearch, IconSearchFilled } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"

const [destination, setDestination] = useState("#home")
const selectDestination = (event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); setDestination(event.currentTarget.getAttribute("href") ?? "#home") }

<div data-slot="bottom-nav-preview" className="mx-auto flex h-80 w-[393px] shrink-0 flex-col justify-between overflow-hidden rounded-container border border-border bg-background-surface">
  <div className="flex flex-col gap-xs p-lg">
    <p className="text-sm font-medium">Không gian làm việc của bạn</p>
    <p className="text-xs text-text-secondary">Chọn một điểm đến bên dưới.</p>
  </div>
  <BottomNav label="Chính" value={destination} className="relative md:block">
    <BottomNavItem href="#home" onClick={selectDestination} icon={<IconHome size={20} />} activeIcon={<IconHomeFilled size={20} />} label="Trang chủ" active />
    <BottomNavItem href="#search" onClick={selectDestination} icon={<IconSearch size={20} />} activeIcon={<IconSearchFilled size={20} />} label="Tìm kiếm" />
    <BottomNavItem href="#inbox" onClick={selectDestination} icon={<IconMail size={20} />} activeIcon={<IconMailFilled size={20} />} label="Hộp thư đến" count={3} />
  </BottomNav>
</div>

Năm đích

Your workspace

Choose a destination below.

import { useState, type MouseEvent } from "react"
import { IconHome, IconHomeFilled, IconLayoutGrid, IconLayoutGridFilled, IconMail, IconMailFilled, IconSearch, IconSearchFilled, IconUser, IconUserFilled } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"

const [destination, setDestination] = useState("#home")
const selectDestination = (event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); setDestination(event.currentTarget.getAttribute("href") ?? "#home") }

<div data-slot="bottom-nav-preview" className="mx-auto flex h-80 w-[393px] shrink-0 flex-col justify-between overflow-hidden rounded-container border border-border bg-background-surface">
  <div className="flex flex-col gap-xs p-lg">
    <p className="text-sm font-medium">Không gian làm việc của bạn</p>
    <p className="text-xs text-text-secondary">Chọn một điểm đến bên dưới.</p>
  </div>
  <BottomNav label="Chính" value={destination} className="relative md:block">
    <BottomNavItem href="#home" onClick={selectDestination} icon={<IconHome size={20} />} activeIcon={<IconHomeFilled size={20} />} label="Trang chủ" active />
    <BottomNavItem href="#search" onClick={selectDestination} icon={<IconSearch size={20} />} activeIcon={<IconSearchFilled size={20} />} label="Tìm kiếm" />
    <BottomNavItem href="#inbox" onClick={selectDestination} icon={<IconMail size={20} />} activeIcon={<IconMailFilled size={20} />} label="Hộp thư đến" count={3} />
    <BottomNavItem href="#projects" onClick={selectDestination} icon={<IconLayoutGrid size={20} />} activeIcon={<IconLayoutGridFilled size={20} />} label="Dự án" />
    <BottomNavItem href="#profile" onClick={selectDestination} icon={<IconUser size={20} />} activeIcon={<IconUserFilled size={20} />} label="Hồ sơ" />
  </BottomNav>
</div>

Route hiện tại

Your workspace

Choose a destination below.

import { useState, type MouseEvent } from "react"
import { IconHome, IconHomeFilled, IconLayoutGrid, IconLayoutGridFilled, IconMail, IconMailFilled, IconSearch, IconSearchFilled } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"

const [destination, setDestination] = useState("#home")
const selectDestination = (event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); setDestination(event.currentTarget.getAttribute("href") ?? "#home") }

<div data-slot="bottom-nav-preview" className="mx-auto flex h-80 w-[393px] shrink-0 flex-col justify-between overflow-hidden rounded-container border border-border bg-background-surface">
  <div className="flex flex-col gap-xs p-lg">
    <p className="text-sm font-medium">Không gian làm việc của bạn</p>
    <p className="text-xs text-text-secondary">Chọn một điểm đến bên dưới.</p>
  </div>
  <BottomNav label="Chính" value={destination} className="relative md:block">
    <BottomNavItem href="#home" onClick={selectDestination} icon={<IconHome size={20} />} activeIcon={<IconHomeFilled size={20} />} label="Trang chủ" active />
    <BottomNavItem href="#search" onClick={selectDestination} icon={<IconSearch size={20} />} activeIcon={<IconSearchFilled size={20} />} label="Tìm kiếm" />
    <BottomNavItem href="#inbox" onClick={selectDestination} icon={<IconMail size={20} />} activeIcon={<IconMailFilled size={20} />} label="Hộp thư đến" count={3} />
    <BottomNavItem href="#projects" onClick={selectDestination} icon={<IconLayoutGrid size={20} />} activeIcon={<IconLayoutGridFilled size={20} />} label="Dự án" />
  </BottomNav>
</div>

Your workspace

Choose a destination below.

import { useState, type MouseEvent } from "react"
import { IconHome, IconHomeFilled, IconMail, IconMailFilled, IconSearch, IconSearchFilled } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"

const [destination, setDestination] = useState("#home")
const selectDestination = (event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); setDestination(event.currentTarget.getAttribute("href") ?? "#home") }

<div data-slot="bottom-nav-preview" className="mx-auto flex h-80 w-[393px] shrink-0 flex-col justify-between overflow-hidden rounded-container border border-border bg-background-surface">
  <div className="flex flex-col gap-xs p-lg">
    <p className="text-sm font-medium">Không gian làm việc của bạn</p>
    <p className="text-xs text-text-secondary">Chọn một điểm đến bên dưới.</p>
  </div>
  <BottomNav label="Chính" value={destination} className="relative md:block">
    <BottomNavItem href="#home" onClick={selectDestination} icon={<IconHome size={20} />} activeIcon={<IconHomeFilled size={20} />} label="Trang chủ" active asChild>
      <a href="#home">Trang chủ</a>
    </BottomNavItem>
    <BottomNavItem href="#search" onClick={selectDestination} icon={<IconSearch size={20} />} activeIcon={<IconSearchFilled size={20} />} label="Tìm kiếm" asChild>
      <a href="#search">Tìm kiếm</a>
    </BottomNavItem>
    <BottomNavItem href="#inbox" onClick={selectDestination} icon={<IconMail size={20} />} activeIcon={<IconMailFilled size={20} />} label="Hộp thư đến" asChild>
      <a href="#inbox">Hộp thư đến</a>
    </BottomNavItem>
  </BottomNav>
</div>

Chỉ icon

Ẩn nhãn nhìn thấy nhưng vẫn giữ tên đích cho công nghệ hỗ trợ.

Your workspace

Choose a destination below.

import { useState, type MouseEvent } from "react"
import { IconHome, IconHomeFilled, IconMail, IconMailFilled, IconSearch, IconSearchFilled } from "@tabler/icons-react"
import { BottomNav, BottomNavItem } from "@/components/ui/bottom-nav"

const [destination, setDestination] = useState("#home")
const selectDestination = (event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault(); setDestination(event.currentTarget.getAttribute("href") ?? "#home") }

<div data-slot="bottom-nav-preview" className="mx-auto flex h-80 w-[393px] shrink-0 flex-col justify-between overflow-hidden rounded-container border border-border bg-background-surface">
  <div className="flex flex-col gap-xs p-lg">
    <p className="text-sm font-medium">Không gian làm việc của bạn</p>
    <p className="text-xs text-text-secondary">Chọn một điểm đến bên dưới.</p>
  </div>
  <BottomNav label="Chính" value={destination} showLabels={false} className="relative md:block">
    <BottomNavItem href="#home" onClick={selectDestination} icon={<IconHome size={20} />} activeIcon={<IconHomeFilled size={20} />} label="Trang chủ" active />
    <BottomNavItem href="#search" onClick={selectDestination} icon={<IconSearch size={20} />} activeIcon={<IconSearchFilled size={20} />} label="Tìm kiếm" />
    <BottomNavItem href="#inbox" onClick={selectDestination} icon={<IconMail size={20} />} activeIcon={<IconMailFilled size={20} />} label="Hộp thư đến" count={3} />
  </BottomNav>
</div>

Nên và không nên

Nên

  • Dùng 3 đến 5 đích với nhãn ngắn.
  • Đệm nội dung trang để thanh không bao giờ che hàng cuối.
  • Trong các demo nhúng này, relative md:block giữ thanh bên trong ô của nó và vẫn thấy trên desktop. Bỏ các class demo đó cho thanh mobile fixed trong một ứng dụng.
  • Dùng route hiện tại làm value, hoặc đặt active trên một item. Component không sở hữu trạng thái điều hướng.
  • Giữ nhãn hiện với các đích chưa quen. Chỉ đặt showLabels={false} khi icon đã quen; luôn cung cấp label của từng item.
  • Chừa chỗ dưới nội dung trang cho chiều cao capsule, khoảng hở phía dưới và inset của safe-area.
  • Cung cấp activeIcon filled cho mọi đích. Mỗi ví dụ nhúng dùng lựa chọn cục bộ riêng và một canvas mobile 393px; cuộn canvas theo chiều ngang khi ô tài liệu hẹp hơn.

Không nên

  • Đừng đặt action (Tạo, Chia sẻ) vào thanh; dùng một Button.
  • Đừng hiện nó cạnh một Sidebar đang mở.

Props

TênKiểuGiá trịMặc địnhMô tả
labelstring-MainTrên BottomNav: tên truy cập của landmark điều hướng. Trên BottomNavItem: chữ đích bắt buộc, hiện trừ khi showLabels là false.
countLabel(label: string, count: number) => string--Đặt tên cho item đang hiện count, thay tên truy cập của nó: (label, count) => `{label}, {count} new` theo mặc định. Tiếng Việt: `{label}, {count} mục mới`. Tùy chọn.
valuestring--Trên BottomNav: href của đích đang active, thường do router của ứng dụng cung cấp. Được ưu tiên hơn prop active của item. BottomNav không đổi nó khi nhấn.
href bắt buộcstring--Trên BottomNavItem: URL đích và id để chọn. Bắt buộc kể cả khi có asChild.
icon bắt buộcnode--Trên BottomNavItem: icon thường, 20px.
activeIcon bắt buộcnode--Trên BottomNavItem: icon filled cho trạng thái active. Cung cấp variant filled khớp với icon thường, ở 20px.
activeboolean-falseTrên BottomNavItem: đánh dấu đích này là hiện tại khi BottomNav không có value. Việc chọn vẫn dùng điều hướng link bình thường.
countnumber--Trên BottomNavItem: count lớn hơn không thêm một badge và đặt tên count qua countLabel. Count bằng không và count âm bị ẩn.
asChildboolean-falseTrên BottomNavItem: render hành vi link đích lên child duy nhất của nó.
onClickReact.MouseEventHandler<HTMLAnchorElement>--Trên BottomNavItem: handler click của link native. Dùng điều hướng của ứng dụng hoặc preventDefault() cho bản demo tại chỗ.
showLabelsboolean-trueTrên BottomNav: hiện nhãn đích dưới icon. Đặt false để ẩn nhãn khỏi mắt nhưng vẫn giữ tên cho mọi link với công nghệ hỗ trợ.

Slot

TênBắt buộcMô tả
childrencó3 đến 5 BottomNavItem với href, icon, activeIcon và label, cộng count và active tùy chọn.

Cấu tạo

PhầnMô tả
rootLớp bọc định vị fixed, trong suốt, ẩn từ md trở lên, với padding safe-area theo chiều ngang và phía dưới. Khoảng trống quanh capsule không chặn sự kiện pointer.
itemMột link đích rộng bằng nhau: icon 20px trên nhãn font-size-xs, với pill chọn bo tròn và cao ít nhất size-element-xl.
iconIcon outline 20px, được thay bằng activeIcon filled bắt buộc khi được chọn.
labelChữ đích, ẩn khỏi mắt khi showLabels là false. Nhãn hiện mà dài thì bị cắt cụt.
badgeBadge đếm danger solid tùy chọn trên icon, với text-on-danger.
listCapsule nổi căn giữa trên material-thin, với chiều rộng panel tối đa, góc rounded-full, padding bên trong và đổ bóng.
indicatorMột pill bo tròn trượt giữa các đích cùng rộng trong duration-medium. Nó đổi kích thước theo thanh; khi giảm chuyển động, việc chọn đổi mà không di chuyển.

Khả năng truy cập

Role: navigation

PhímHành động
TabChuyển giữa các đích (link), theo thứ tự.
EnterĐi theo link.
  • Một landmark nav được đặt tên bởi label. Item là link, không phải tab: không có role tab và không roving bằng phím mũi tên.
  • Item đang active có aria-current="page" và dùng icon Filled (DESIGN 9.1).
  • Nhãn hiện theo mặc định. Với showLabels=false, nhãn vẫn có cho công nghệ hỗ trợ qua chữ ẩn khỏi mắt.
  • Badge đếm thêm số của nó vào tên truy cập, qua countLabel.

Cài đặt

npx werewolf-ui add bottom-nav

Chạy npx werewolf-ui init một lần trong app trước khi thêm component. Init cài token, thêm CSS import và luật cho agent. Add cài component này cùng các dependency và giữ nguyên theme của bạn. Xem Cài đặt để biết yêu cầu và cách cài qua registry.