Navbar
A top navigation bar with a title and actions.
A top navigation bar — centered title on iOS, left-aligned on Material — with leading and trailing action slots.
Large title
Set largeTitle to opt into the iOS large-title idiom: the bar renders its compact row (a centered
17pt title, transparent at rest) plus a big left-aligned 34px title block below it, and collapses the
large title into the compact one as the page scrolls. The scroll tracking runs against the nearest
tcn-page scroll container and drives a data-collapsed attribute that theme.css transitions off
(instant when prefers-reduced-motion is set). The page reserves the extra top clearance automatically.
Material has no large-title idiom, so there largeTitle is a visual no-op — the standard top app bar,
using the same title.
Installation
npx touchcn add navbar
Usage
<tcn-navbar title="Settings">
<button navbar-leading tcnButton variant="ghost">Back</button>
<button navbar-trailing tcnButton variant="ghost">Edit</button>
</tcn-navbar>import { TcnNavbar } from '@/components/ui/navbar';
import { TcnButton } from '@/components/ui/button';
<TcnNavbar
title="Settings"
leading={<TcnButton variant="ghost">Back</TcnButton>}
trailing={<TcnButton variant="ghost">Edit</TcnButton>}
/><script setup lang="ts">
import { TcnNavbar } from '@/components/ui/navbar';
import { TcnButton } from '@/components/ui/button';
</script>
<template>
<TcnNavbar title="Settings">
<template #leading>
<TcnButton variant="ghost">Back</TcnButton>
</template>
<template #trailing>
<TcnButton variant="ghost">Edit</TcnButton>
</template>
</TcnNavbar>
</template>Props
| Prop | Type | Description |
|---|---|---|
title |
string / ReactNode |
Bar title — centered on iOS, left-aligned on MD. |
leading |
slot / ReactNode |
Leading cluster (e.g. a back control). |
trailing |
slot / ReactNode |
Trailing cluster (e.g. actions). |
largeTitle |
boolean |
Opt into the iOS large-title idiom (collapses on scroll); a no-op on MD. |
threshold |
number |
Scroll distance (px) that collapses the large title. Defaults to 52. |
On Angular, leading / trailing are the [navbar-leading] / [navbar-trailing] projection slots.
Source · Angular
export * from './tcn-navbar';import { Component, inject, input } from '@angular/core';
import { TcnNavbarCollapseDirective } from '@touchcn/angular/navbar';
/**
* Top navigation bar — centered title on iOS, left-aligned on Material — with
* leading and trailing action slots.
*
* `largeTitle` opts into the iOS large-title idiom: the bar renders its compact
* row (initially transparent) plus a big left-aligned title block below, and
* collapses to the compact row as the page scrolls. All the scroll tracking and
* the `data-collapsed` flag live in the engine `TcnNavbarCollapseDirective`
* (applied here as a host directive, gated on `largeTitle`); `theme.css` drives
* every transition off `data-large-title` / `data-collapsed`. Material has no
* large-title idiom, so there `largeTitle` is a visual no-op — the standard bar.
*/
@Component({
selector: 'tcn-navbar',
hostDirectives: [{ directive: TcnNavbarCollapseDirective, inputs: ['enabled: largeTitle', 'threshold'] }],
template: `
<div class="tcn-navbar flex items-center gap-2 px-3">
<div class="tcn-navbar-cluster flex shrink-0 items-center gap-1">
<ng-content select="[navbar-leading]" />
</div>
<div class="tcn-navbar-title">{{ title() }}<ng-content select="[navbar-title]" /></div>
<div class="tcn-navbar-cluster flex shrink-0 items-center justify-end gap-1">
<ng-content select="[navbar-trailing]" />
</div>
</div>
@if (collapse.enabled()) {
<div class="tcn-navbar-large">
<h1 class="tcn-navbar-large-title">{{ title() }}</h1>
</div>
}
`,
host: {
class: 'fixed inset-x-0 top-0 z-40 block',
'[attr.data-large-title]': 'collapse.enabled() ? "" : null',
},
})
export class TcnNavbar {
protected readonly collapse = inject(TcnNavbarCollapseDirective);
readonly title = input('');
}Source · React
export * from './tcn-navbar';import { forwardRef } from 'react';
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import { cn } from '@touchcn/core';
import { mergeRefs, useNavbarCollapse } from '@touchcn/react';
export interface TcnNavbarProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
/** Centered (iOS) / left-aligned (MD) bar title. */
title?: ReactNode;
/** Leading cluster (e.g. a back control). */
leading?: ReactNode;
/** Trailing cluster (e.g. actions). */
trailing?: ReactNode;
/** Opts into the iOS large-title idiom — a big title below the bar that
* collapses into the compact row on scroll. Material has no large-title
* idiom, so this is a visual no-op there (the standard bar). */
largeTitle?: boolean;
/** Scroll distance (px) that collapses the large title. Defaults to 52. */
threshold?: number;
}
/**
* Top navigation bar. `largeTitle` renders the compact row (initially
* transparent on iOS) plus a big left-aligned title block that collapses into
* the compact row as the page scrolls — all scroll tracking and the
* `data-collapsed` flag live in the engine `useNavbarCollapse` hook, and
* `theme.css` drives every transition off `data-large-title` / `data-collapsed`.
*/
export const TcnNavbar = forwardRef<HTMLDivElement, TcnNavbarProps>(function TcnNavbar(
{ title, leading, trailing, largeTitle = false, threshold, className, children, ...props },
ref,
) {
const { hostRef, collapsed } = useNavbarCollapse({ enabled: largeTitle, threshold });
return (
<div
ref={mergeRefs(ref, hostRef)}
className={cn('fixed inset-x-0 top-0 z-40 block', className)}
data-large-title={largeTitle ? '' : undefined}
data-collapsed={largeTitle ? collapsed : undefined}
{...props}
>
<div className="tcn-navbar flex items-center gap-2 px-3">
<div className="tcn-navbar-cluster flex shrink-0 items-center gap-1">{leading}</div>
<div className="tcn-navbar-title">
{title}
{children}
</div>
<div className="tcn-navbar-cluster flex shrink-0 items-center justify-end gap-1">{trailing}</div>
</div>
{largeTitle && (
<div className="tcn-navbar-large">
<h1 className="tcn-navbar-large-title">{title}</h1>
</div>
)}
</div>
);
});Source · Vue
<script setup lang="ts">
import { useNavbarCollapse } from '@touchcn/vue';
const props = defineProps<{
/** Centered (iOS) / left-aligned (MD) bar title. */
title?: string;
/**
* Opts into the iOS large-title idiom — a big title below the bar that
* collapses into the compact row on scroll. Material has no large-title
* idiom, so this is a visual no-op there (the standard bar).
*/
largeTitle?: boolean;
/** Scroll distance (px) that collapses the large title. Defaults to 52. */
threshold?: number;
}>();
/**
* All scroll tracking and the `data-collapsed` flag live in the engine
* `useNavbarCollapse` hook; `theme.css` drives every transition off
* `data-large-title` / `data-collapsed`.
*/
const { setHost, collapsed } = useNavbarCollapse(() => ({
enabled: props.largeTitle,
threshold: props.threshold,
}));
</script>
<template>
<div
:ref="setHost"
class="fixed inset-x-0 top-0 z-40 block"
:data-large-title="largeTitle ? '' : undefined"
:data-collapsed="largeTitle ? collapsed : undefined"
>
<div class="tcn-navbar flex items-center gap-2 px-3">
<div class="tcn-navbar-cluster flex shrink-0 items-center gap-1">
<slot name="leading" />
</div>
<div class="tcn-navbar-title">
<slot name="title">{{ title }}</slot>
<slot />
</div>
<div class="tcn-navbar-cluster flex shrink-0 items-center justify-end gap-1">
<slot name="trailing" />
</div>
</div>
<div v-if="largeTitle" class="tcn-navbar-large">
<h1 class="tcn-navbar-large-title">{{ title }}</h1>
</div>
</div>
</template>export { default as TcnNavbar } from './TcnNavbar.vue';