Navs and tabs
Navs and tabs organize and allow navigation between groups of content that are related and at the same level of hierarchy.
Overview #
Halfmoon comes with three different styles of navigation component: tabs, pills, and underline. They all share the same general markup and styles, extending from the base .nav
class. While the base is generally not useful on its own (mainly because of its lack of styling for the .active
state), you can extend it to create custom navigation. Read more about the base .nav
component in its own section.
Tabs #
Create tabbed interfaces with the .nav-tabs
class added to the navigation component. Use them to build tabbable regions with the tab JavaScript plugin.
HTML
<!-- Tabs -->
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" aria-current="true" href="#">Tab 1</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Tab 2</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Tab 3</a>
</li>
<li class="nav-item">
<a class="nav-link disabled">Tab 4</a>
</li>
</ul>
Heads up! The active tab link has its background-color
set to var(--bs-content-bg)
(so that it can be used seamlessly inside cards). If you want to use it outside of cards, please set the background-color
to var(--bs-body-bg)
(or whatever is the background color of the area you want the tab to blend into):
HTML
<!-- Tabs -->
<ul class="nav nav-tabs" style="--bs-nav-tabs-link-active-bg: var(--bs-body-bg);">
...
</ul>
Information
While most of the examples on this page use<ul>
and <li>
elements in their markup, you can also roll your own with the <nav>
element. See the base navigation component section for an example. The benefit of using lists is mainly for assistive technologies.
Pills #
Create pill-like navigation interfaces using the .nav-pills
class. Here the active item is highlighted with the var(--bs-primary)
color for its background.
HTML
<!-- Pills -->
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link active" aria-current="true" href="#">Link 1</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link 2</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link 3</a>
</li>
<li class="nav-item">
<a class="nav-link disabled">Link 4</a>
</li>
</ul>
Underline #
Create underlined navigation interfaces using the .nav-underline
class. By default, the wrapping .nav-underline
has no borders to keep it simple. However, you can easily set one on the bottom using .border-bottom
for better framing (if you want).
HTML
<!-- Underline -->
<ul class="nav nav-underline border-bottom">
<li class="nav-item">
<a class="nav-link active" aria-current="true" href="#">Link 1</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link 2</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link 3</a>
</li>
<li class="nav-item">
<a class="nav-link disabled">Link 4</a>
</li>
</ul>
Horizontal alignment #
By default, navs are left-aligned, but you can easily center them using .justify-content-center
, or align them to the right using .justify-content-end
. The custom alignments will work with all three variations.
HTML
<!-- Left-aligned nav (default) -->
<ul class="nav nav-tabs">
...
</ul>
<!-- Centered nav -->
<ul class="nav nav-pills justify-content-center">
...
</ul>
<!-- Right-aligned nav -->
<ul class="nav nav-underline justify-content-end border-bottom">
...
</ul>
Vertical #
Change the flex-direction
to column
with .flex-column
/ .flex-{breakpoint}-column
to stack the links vertically. It's important to note that due to the design of the tabs and underline variations, only pills will look good vertically without any custom CSS.
HTML
<!-- Vertical pills -->
<ul class="nav nav-pills flex-column">
...
</ul>
Fill and justify #
Force your .nav
contents to extend the full available width with one of two modifier classes. To proportionately fill all available space with your .nav-items
, use .nav-fill
. Notice that all horizontal space is occupied, but not every item has the same width (the third link takes a little extra space because of the extra content).
HTML
<!-- Nav-fill -->
<ul class="nav nav-pills nav-fill">
<li class="nav-item">
<a class="nav-link active" aria-current="true" href="#">Link 1</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link 2</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-seam me-1" viewBox="0 0 16 16">
<path d="M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5l2.404.961L10.404 2l-2.218-.887zm3.564 1.426L5.596 5 8 5.961 14.154 3.5l-2.404-.961zm3.25 1.7-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923l6.5 2.6zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464L7.443.184z"/>
</svg>
Link 3
</a>
</li>
</ul>
For equal-width elements, use .nav-justified
. All horizontal space will be occupied, but unlike the .nav-fill
above, every item will be the same width.
HTML
<!-- Nav-justified -->
<ul class="nav nav-pills nav-justified">
<li class="nav-item">
<a class="nav-link active" aria-current="true" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-seam me-1" viewBox="0 0 16 16">
<path d="M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5l2.404.961L10.404 2l-2.218-.887zm3.564 1.426L5.596 5 8 5.961 14.154 3.5l-2.404-.961zm3.25 1.7-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923l6.5 2.6zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464L7.443.184z"/>
</svg>
Link
</a>
</li>
</ul>
Working with flex utilities #
If you need responsive variations, consider using a series of flexbox utilities. While more verbose, these utilities offer greater customization across responsive breakpoints. In the example below, the navigation component will be stacked vertically on the lowest breakpoint, then adapt to a horizontal layout that fills the available width starting from the small breakpoint.
HTML
<!-- Using flex utilities -->
<ul class="nav nav-pills flex-column flex-sm-row nav-justified">
...
</ul>
General accessibility #
If you're using navs to provide a navigation bar, be sure to add a role="navigation"
to the most logical parent container of the <ul>
, or wrap a <nav>
element around the whole navigation. Do not add the role to the <ul>
itself, as this would prevent it from being announced as an actual list by assistive technologies. To convey the active state to assistive technologies, use the aria-current
attribute — using the page
value for current page, or true
for the current item in a set.
Note that navigation bars, even if visually styled as tabs with the .nav-tabs
class, should not be given role="tablist"
, role="tab"
or role="tabpanel"
attributes. These are only appropriate for dynamic tabbed interfaces, as described in the ARIA Authoring Practices Guide tabs pattern (opens in new tab). See the tab JavaScript plugin for an example of dynamic tabbed interfaces. The aria-current
attribute is not necessary on dynamic tabbed interfaces since our JavaScript handles the selected state by adding aria-selected="true"
on the active tab.
Using dropdowns #
Add dropdown menus with a little extra HTML and the dropdowns JavaScript plugin. Dropdowns will work with all three of the navigation variations.
HTML
<!-- Dropdown with tabs -->
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" aria-current="true" href="#">Tab 1</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Tab 2</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">Dropdown</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action 1</a></li>
<li><a class="dropdown-item" href="#">Action 2</a></li>
<li><a class="dropdown-item" href="#">Action 3</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">Separated link</a></li>
</ul>
</li>
</ul>
<!-- Dropdown with pills -->
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link active" aria-current="true" href="#">Link 1</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link 2</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">Dropdown</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action 1</a></li>
<li><a class="dropdown-item" href="#">Action 2</a></li>
<li><a class="dropdown-item" href="#">Action 3</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">Separated link</a></li>
</ul>
</li>
</ul>
Base navigation component #
The base .nav
component is built with flexbox and provides a strong foundation for building all types of navigation components. It includes some style overrides (for working with lists), some link padding for larger hit areas, and basic disabled styling. Please note, it does not include any .active
state. The following examples include the class, mainly to demonstrate that this particular class does not trigger any special styling.
HTML
<!-- Base nav -->
<ul class="nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled">Disabled</a>
</li>
</ul>
Classes are used throughout, so your markup can be super flexible. Use <ul>
elements like above, <ol>
if the order of your items is important, or roll your own with a <nav>
element. Because the .nav
uses display: flex
, the nav links behave the same as nav items would, but without the extra markup.
HTML
<!-- Base nav (alternate markup) -->
<nav class="nav">
<a class="nav-link active" aria-current="page" href="#">Active</a>
<a class="nav-link" href="#">Link</a>
<a class="nav-link" href="#">Link</a>
<a class="nav-link disabled">Disabled</a>
</nav>
JavaScript tab plugin #
Use the tab JavaScript plugin to create tabbable panes of local content. The tab plugin will work with all of the examples on this page, excluding the one with the dropdowns.
Home
Account
Contact
Tasks
HTML
<!-- JavaScript tab plugin -->
<ul class="nav nav-tabs" id="js-tabs-1" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#home-tab-pane" type="button" role="tab" aria-controls="home-tab-pane" aria-selected="true">Home</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="account-tab" data-bs-toggle="tab" data-bs-target="#account-tab-pane" type="button" role="tab" aria-controls="account-tab-pane" aria-selected="false">Account</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact-tab-pane" type="button" role="tab" aria-controls="contact-tab-pane" aria-selected="false">Contact</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link disabled" id="disabled-tab" data-bs-toggle="tab" data-bs-target="#disabled-tab-pane" type="button" role="tab" aria-controls="disabled-tab-pane" aria-selected="false" disabled>Tasks</button>
</li>
</ul>
<div class="tab-content" id="js-tabs-content-1">
<div class="tab-pane fade show active" id="home-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">...</div>
<div class="tab-pane fade" id="account-tab-pane" role="tabpanel" aria-labelledby="account-tab" tabindex="0">...</div>
<div class="tab-pane fade" id="contact-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">...</div>
<div class="tab-pane fade" id="disabled-tab-pane" role="tabpanel" aria-labelledby="disabled-tab" tabindex="0">...</div>
</div>
To help fit your needs, this works with <ul>
-based markup, as shown above, or with any arbitrary "roll your own" markup. Note that if you're using <nav>
, you shouldn't add role="tablist"
directly to it, as this would override the element's native role as a navigation landmark. Instead, switch to an alternative element (in the example below, a simple <div>
) and wrap the <nav>
around it.
HTML
<!-- JavaScript tab plugin (alternate markup) -->
<nav>
<div class="nav nav-tabs" id="js-tabs-2" role="tablist">
<button class="nav-link active" id="nav-home-tab" data-bs-toggle="tab" data-bs-target="#nav-home" type="button" role="tab" aria-controls="nav-home" aria-selected="true">Home</button>
<button class="nav-link" id="nav-account-tab" data-bs-toggle="tab" data-bs-target="#nav-account" type="button" role="tab" aria-controls="nav-account" aria-selected="false">Account</button>
<button class="nav-link" id="nav-contact-tab" data-bs-toggle="tab" data-bs-target="#nav-contact" type="button" role="tab" aria-controls="nav-contact" aria-selected="false">Contact</button>
<button class="nav-link disabled" id="nav-disabled-tab" data-bs-toggle="tab" data-bs-target="#nav-disabled" type="button" role="tab" aria-controls="nav-disabled" aria-selected="false" disabled>Tasks</button>
</div>
</nav>
<div class="tab-content" id="js-tabs-content-2">
<div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab" tabindex="0">...</div>
<div class="tab-pane fade" id="nav-account" role="tabpanel" aria-labelledby="nav-account-tab" tabindex="0">...</div>
<div class="tab-pane fade" id="nav-contact" role="tabpanel" aria-labelledby="nav-contact-tab" tabindex="0">...</div>
<div class="tab-pane fade" id="nav-disabled" role="tabpanel" aria-labelledby="nav-disabled-tab" tabindex="0">...</div>
</div>
The tabs plugin also works with pills (data-bs-toggle="pill"
). Here's an example showing this with vertical pills. Ideally, for vertical tabs, you should also add aria-orientation="vertical"
to the tab list container.
Profile
Billing
Messages
Stats
HTML
<!-- JavaScript pill plugin (vertical) -->
<div class="row">
<div class="col-sm-4">
<ul class="nav nav-pills flex-column p-2 border rounded-3" id="js-pills-1" role="tablist" aria-orientation="vertical">
<li class="nav-item" role="presentation">
<button class="nav-link active w-100 text-start d-flex align-items-center" id="profile-pill" data-bs-toggle="pill" data-bs-target="#profile-pill-pane" type="button" role="tab" aria-controls="profile-pill-pane" aria-selected="true">
<i class="fa-light fa-user" style="width: 30px;"></i>
Profile
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center" id="billing-pill" data-bs-toggle="pill" data-bs-target="#billing-pill-pane" type="button" role="tab" aria-controls="billing-pill-pane" aria-selected="false">
<i class="fa-light fa-credit-card" style="width: 30px;"></i>
Billing
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center" id="messages-pill" data-bs-toggle="pill" data-bs-target="#messages-pill-pane" type="button" role="tab" aria-controls="messages-pill-pane" aria-selected="false">
<i class="fa-light fa-envelope" style="width: 30px;"></i>
Messages
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center" id="stats-pill" data-bs-toggle="pill" data-bs-target="#stats-pill-pane" type="button" role="tab" aria-controls="stats-pill-pane" aria-selected="false">
<i class="fa-light fa-line-chart" style="width: 30px;"></i>
Stats
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link w-100 text-start d-flex align-items-center" id="premium-pill" data-bs-toggle="pill" data-bs-target="#premium-pill-pane" type="button" role="tab" aria-controls="premium-pill-pane" aria-selected="false">
<i class="fa-light fa-star" style="width: 30px;"></i>
Premium
</button>
</li>
</ul>
</div>
<div class="col-sm-8">
<div class="tab-content" id="js-pills-content-1">
<div class="tab-pane fade show active" id="profile-pill-pane" role="tabpanel" aria-labelledby="profile-pill" tabindex="0">...</div>
<div class="tab-pane fade" id="billing-pill-pane" role="tabpanel" aria-labelledby="billing-pill" tabindex="0">...</div>
<div class="tab-pane fade" id="messages-pill-pane" role="tabpanel" aria-labelledby="messages-pill" tabindex="0">...</div>
<div class="tab-pane fade" id="stats-pill-pane" role="tabpanel" aria-labelledby="stats-pill" tabindex="0">...</div>
<div class="tab-pane fade" id="premium-pill-pane" role="tabpanel" aria-labelledby="premium-pill" tabindex="0">...</div>
</div>
</div>
</div>
Note the generous use of utility classes to create the above design. Also note, the icons being used are taken from the Font Awesome icon library (opens in new tab).
JavaScript tab plugin Accessibility #
Dynamic tabbed interfaces, as described in the ARIA Authoring Practices Guide tabs pattern (opens in new tab), require role="tablist"
, role="tab"
, role="tabpanel"
, and additional aria-*
attributes in order to convey their structure, functionality, and current state to users of assistive technologies (such as screen readers). As a best practice, we recommend using <button>
elements for the tabs, as these are controls that trigger a dynamic change, rather than links that navigate to a new page or location.
In line with the ARIA Authoring Practices pattern, only the currently active tab receives keyboard focus. When the JavaScript plugin is initialized, it will set tabindex="-1"
on all inactive tab controls. Once the currently active tab has focus, the cursor keys activate the previous/next tab, with the plugin changing the roving tabindex (opens in new tab) accordingly. However, note that the JavaScript plugin does not distinguish between horizontal and vertical tab lists when it comes to cursor key interactions: regardless of the tab list's orientation, both the up and left cursor go to the previous tab, and down and right cursor go to the next tab.
In general, to facilitate keyboard navigation, it's recommended to make the tab panels themselves focusable as well, unless the first element containing meaningful content inside the tab panel is already focusable. The JavaScript plugin does not try to handle this aspect—where appropriate, you'll need to explicitly make your tab panels focusable by adding tabindex="0"
in your markup.
Warning
The tab JavaScript plugin does not support tabbed interfaces that contain dropdown menus, as these cause both usability and accessibility issues.
From a usability perspective, the fact that the currently displayed tab's trigger element is not immediately visible (as it's inside the closed dropdown menu) can cause confusion. From an accessibility point of view, there is currently no sensible way to map this sort of construct to a standard WAI ARIA pattern, meaning that it cannot be easily made understandable to users of assistive technologies.
JavaScript usage #
halfmoon.js
Halfmoon is a drop-in replacement for Bootstrap. We implement no JavaScript on our own, therefore, there is no halfmoon.js
. Instead we rely entirely on bootstrap.bundle.js
(which can be downloaded from Bootstrap's website). Read the JavaScript section on our download page to learn more. It also contains a starter template to help you get started quickly.
The examples on this page use data-bs-*
attributes for functionality, which is usually enough to cover a majority of use-cases. You can read about methods, events, and more in the official Bootstrap documentation.
Navs & tabs - Bootstrap
Continue reading on the offical documentation website
Help us grow
Our main goal is to make Halfmoon the go-to framework for building websites, dashboards and tools. If you believe in our mission, consider becoming a sponsor and help us grow.
You can email us directly if you have any queries. We are always happy to answer.
Subscribe for updates
We will notify you when the framework gets a substantial update. No spam ever.
Follow us on Twitter so that you can stay updated that way.