Navbar
A sticky header whose links are server HTML. One small client part publishes its height as --tc-sticky-top.
split - size not measured yet
Navbar
split - size not measured yetA sticky header whose links are server HTML. One small client part publishes its height as --tc-sticky-top.
Try it
sticky
current
The code
navbar.tsx
import { Button } from 'topclassui/button'
import { NavbarActions, NavbarBrand, NavbarLink, NavbarLinks, NavbarRoot } from 'topclassui/navbar'
<NavbarRoot>
<NavbarBrand href="/">TopClass UI</NavbarBrand>
<NavbarLinks>
<NavbarLink href="/docs">Docs</NavbarLink>
<NavbarLink href="/components">Components</NavbarLink>
<NavbarLink href="/icons">Icons</NavbarLink>
</NavbarLinks>
<NavbarActions>
<Button render={<a href="/login" />}>Sign in</Button>
</NavbarActions>
</NavbarRoot>The bar writes its own height to --tc-sticky-top, so an #anchor target stops below it, not under it.
Every state
2 statesExamples
- Five flat parts and no bare
Navbar:NavbarRoot(a header),NavbarBrand,NavbarLinks(a nav),NavbarLinkandNavbarActions. - In Next.js, pass the framework link as
render={<Link href="/docs" />}onNavbarBrandorNavbarLink. An element only, never a function. sticky={false}lets the bar scroll away with the page, and then it sends no JavaScript to the browser.- Without script the variable stays 0px. Set
:root { --tc-sticky-top: 3.75rem }in your CSS for one row; the bar replaces it with the measured value. - One sticky bar a page: two would write the same variable, so give the second one
sticky={false}. - A page that scrolls inside a box of your own needs
scroll-padding-top: var(--tc-sticky-top)on that box. - On a narrow screen the links scroll sideways inside the bar and the actions drop to a second row. The page never scrolls sideways.
basic
basic.tsx
import { Button } from 'topclassui/button'
import { NavbarActions, NavbarBrand, NavbarLink, NavbarLinks, NavbarRoot } from 'topclassui/navbar'
/** The drawn bar: the brand, four links with the page being read marked, and one action.
* Each link is the anchor the part renders; a Next.js app passes render={<Link href="/docs" />}. */
export default function Demo() {
return (
<NavbarRoot>
<NavbarBrand href="/">TopClass UI</NavbarBrand>
<NavbarLinks>
<NavbarLink href="/docs" current>Docs</NavbarLink>
<NavbarLink href="/components">Components</NavbarLink>
<NavbarLink href="/icons">Icons</NavbarLink>
<NavbarLink href="/theming">Theming</NavbarLink>
</NavbarLinks>
<NavbarActions>
<Button render={<a href="/login" />}>Sign in</Button>
</NavbarActions>
</NavbarRoot>
)
}
with-search
with-search.tsx
import { AvatarFallback, AvatarRoot } from 'topclassui/avatar'
import { Input } from 'topclassui/input'
import { NavbarActions, NavbarBrand, NavbarLink, NavbarLinks, NavbarRoot } from 'topclassui/navbar'
/** A search field and the account at the end of the bar. NavbarActions styles neither: the
* form is the caller's, and it works with no script because it is a plain GET. */
export default function Demo() {
return (
<NavbarRoot>
<NavbarBrand href="/">TopClass UI</NavbarBrand>
<NavbarLinks>
<NavbarLink href="/docs" current>Docs</NavbarLink>
<NavbarLink href="/components">Components</NavbarLink>
<NavbarLink href="/icons">Icons</NavbarLink>
</NavbarLinks>
<NavbarActions>
<form role="search" action="/search" className="min-w-0">
<Input type="search" name="q" size="sm" placeholder="Search" aria-label="Search the docs" />
</form>
<AvatarRoot size="sm" aria-label="Omar Abed">
<AvatarFallback>OA</AvatarFallback>
</AvatarRoot>
</NavbarActions>
</NavbarRoot>
)
}
many-links
many-links.tsx
import { githubLogo } from 'topclassui/icons/github-logo'
import { Button } from 'topclassui/button'
import { Svg } from 'topclassui/svg'
import { NavbarActions, NavbarBrand, NavbarLink, NavbarLinks, NavbarRoot } from 'topclassui/navbar'
/** Six links and two actions: more than a phone has room for. The links scroll sideways
* inside the bar, and the page stays as wide as the screen. */
export default function Demo() {
return (
<NavbarRoot>
<NavbarBrand href="/">TopClass UI</NavbarBrand>
<NavbarLinks>
<NavbarLink href="/docs">Docs</NavbarLink>
<NavbarLink href="/components" current>Components</NavbarLink>
<NavbarLink href="/icons">Icons</NavbarLink>
<NavbarLink href="/theming">Theming</NavbarLink>
<NavbarLink href="/connectors">Connectors</NavbarLink>
<NavbarLink href="/changelog">Changelog</NavbarLink>
</NavbarLinks>
<NavbarActions>
<Button variant="ghost" aria-label="GitHub" render={<a href="https://github.com/TopClassUI/topclassui" />}>
<Svg icon={githubLogo} />
</Button>
<Button render={<a href="/login" />}>Sign in</Button>
</NavbarActions>
</NavbarRoot>
)
}
not-sticky
not-sticky.tsx
import { NavbarBrand, NavbarLink, NavbarLinks, NavbarRoot } from 'topclassui/navbar'
/** sticky={false}: the bar scrolls away with the page, sends no JavaScript to the browser
* and leaves --tc-sticky-top alone. */
export default function Demo() {
return (
<NavbarRoot sticky={false}>
<NavbarBrand href="/">TopClass UI</NavbarBrand>
<NavbarLinks>
<NavbarLink href="/docs">Docs</NavbarLink>
<NavbarLink href="/blog" current>Blog</NavbarLink>
<NavbarLink href="/changelog">Changelog</NavbarLink>
</NavbarLinks>
</NavbarRoot>
)
}
tall-page
Install
Go on to the next partUsage
Go on to the next partTheming
Go on to the next partQuestions
Go on to the next parttall-page.tsx
import { Button } from 'topclassui/button'
import { NavbarActions, NavbarBrand, NavbarLink, NavbarLinks, NavbarRoot } from 'topclassui/navbar'
const SECTIONS = [
{ id: 'install', title: 'Install', next: 'usage' },
{ id: 'usage', title: 'Usage', next: 'theming' },
{ id: 'theming', title: 'Theming', next: 'questions' },
{ id: 'questions', title: 'Questions', next: 'install' },
] as const
/** A page longer than the screen under a sticky bar. Each link goes to a heading further
* down, which stops below the bar and not under it; so does a link that Tab scrolls to. */
export default function Demo() {
return (
<div>
<NavbarRoot>
<NavbarBrand href="#install">Handbook</NavbarBrand>
<NavbarLinks aria-label="On this page">
{SECTIONS.map((section) => (
<NavbarLink key={section.id} href={`#${section.id}`}>{section.title}</NavbarLink>
))}
</NavbarLinks>
<NavbarActions>
<Button variant="outline" render={<a href="/handbook/edit" />}>Edit this page</Button>
</NavbarActions>
</NavbarRoot>
{SECTIONS.map((section) => (
<section key={section.id} aria-labelledby={section.id} className="min-h-dvh px-5 pb-6">
<h2 id={section.id} className="text-base font-semibold text-body">{section.title}</h2>
<Button variant="link" render={<a href={`#${section.next}`} />}>Go on to the next part</Button>
</section>
))}
</div>
)
}
In Arabic, right to left
rtl.tsx
import { Button } from 'topclassui/button'
import { ar } from 'topclassui/labels/ar'
import { NavbarActions, NavbarBrand, NavbarLink, NavbarLinks, NavbarRoot } from 'topclassui/navbar'
/** Arabic: the brand sits at the right, the action at the left, and the nav takes its name
* from the Arabic labels. No class of its own does it: every one is logical. */
export default function Demo() {
return (
<div dir="rtl" lang="ar">
<NavbarRoot>
<NavbarBrand href="/">توب كلاس</NavbarBrand>
<NavbarLinks labels={ar}>
<NavbarLink href="/docs" current>المستندات</NavbarLink>
<NavbarLink href="/components">المكوّنات</NavbarLink>
<NavbarLink href="/icons">الأيقونات</NavbarLink>
<NavbarLink href="/theming">السمات</NavbarLink>
</NavbarLinks>
<NavbarActions>
<Button render={<a href="/login" />}>تسجيل الدخول</Button>
</NavbarActions>
</NavbarRoot>
</div>
)
}
Props
NavbarRoot
NavbarBrand
NavbarLinks
NavbarLink
NavbarActions
Keyboard and accessibility
currenton oneNavbarLinksetsaria-current="page". The link is heavier as well as darker, and underlined under forced colours.NavbarLinksis a nav landmark named "Main" fromlabels.navbar.label. With two on a page, give one anaria-label: yours wins.- The link that has the keyboard focus is brought whole into view inside the scrolling list.
Right to left
Under dir="rtl" the brand sits at the right and the actions at the left. Pass labels={ar} to NavbarLinks for the Arabic name.
Server and client
Renders on the server; the interactive part is a client component that ships only when it is used.
Files: index.tsx, client.tsx.
On the chip: split - size not measured yet.