Skip to content
touchcn
Esc
navigateopen⌘Jpreview
On this page

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';

Last updated on July 24, 2026

Was this page helpful?