<!-- https://getkonjo.com/design/patterns/navigation · source: docs/design/patterns/navigation.md -->

# Navigation

Where a screen sits, how you get back, and what is allowed to be a modal.

> Part of the [Konjo design language](../konjo-design-language.md). The laws in the spine
> apply here unless this chapter contradicts them.

Konjo runs one root stack, five tab stacks, and thirty-four modal presentations, with
`headerShown: false` everywhere — **the app draws its own headers**. That makes header
behaviour a design decision rather than a platform default, and it has never been written down.

## The shape

- **Four base tabs — Train | Events | Learn | Social — plus a conditional Teach** for
  instructors, which sits fifth, between Train and Events. Profile is a route inside Social,
  not a tab.
- **Each tab owns a stack.** A screen belongs to the tab whose job it serves, and it is pushed
  within that stack — never pushed onto a sibling tab.
- **Tabs keep their state.** Leaving Learn mid-scroll and coming back returns you where you
  were. A tab that resets to the top on every visit is a tab people stop trusting.
- **Tapping the active tab scrolls to top**, then on a second tap pops to the stack root. Both
  are conventions people already have.

## Push or modal

**Push when the thing is part of where you already are. Present a modal when it is a detour.**

| | Push | Modal |
|---|---|---|
| Relationship | A deeper level of the same subject | A separate task, then back |
| Back affordance | `‹ Back`, top left | `✕` Close, top left |
| Examples | A student from the roster, a class from a schedule | Compose a post, log a session, sign a waiver |
| After it completes | Stays on the stack | Dismisses — to the list the record joined if that is not where it started, so a person who just created something is looking at it |

Two rules that resolve most cases:

- **If it creates or edits something, it is a modal.** The task has a beginning and an end and
  a person should be returned to what they were doing.
- **If it can be navigated *past* — deeper into a hierarchy — it is a push.** Modals do not
  nest well and a person three modals deep has lost the thread.

**A modal never contains a tab bar** and never pushes to something the tabs also reach. If a
modal wants to send someone to a tab, it dismisses first.

## Headers

The app draws them, so they are consistent by choice rather than by framework.

- **Tab homes** use [`ScreenHeader`](../../../src/components/ScreenHeader.tsx): `display` title
  left, up to four actions right (create, search, notifications, avatar). No back affordance —
  it is a root.
- **Pushed screens** get a back affordance and *no title in the bar*. The screen's own
  `display` title sits in the content, where it can wrap and scale. A title duplicated in the
  bar and the body is the same destination twice in one viewport.
- **Modals** get `✕` Close, top left, and the same in-content title.
- **Header glyphs are 24pt in 44pt targets**, `text.primary`, and never more than four. The
  fifth action belongs in an overflow or in the content.
- **Never a title that collapses on scroll into a second copy of itself.** Konjo has one title
  per screen, in the content.

## Back

- **Back always goes back**, to the previous screen, never "up" to a different one.
- **Android's hardware back and gesture back do the same thing as the header affordance.**
  Currently only `MediaLightbox` handles `BackHandler` explicitly; anything that traps input —
  a lightbox, a sheet, an armed destructive action — must dismiss on hardware back rather than
  leaving the screen.
- **Unsaved work confirms before discarding.** A form with typed content that gets dismissed
  asks once, plainly: *Discard this report?* — with the destructive option second. See
  [forms](form.md).
- **Never disable back.** If a person cannot leave, the screen is a bug, not a wall.

## Deep links and web URLs

RN-web gets a real linking config; every route that is a *thing* should have a URL that
resolves to it.

- **A deep link lands on the screen with a working back.** Arriving at a student detail from a
  push notification and finding no way to the roster is a dead end.
- **A link to something gone shows the detail screen's error state**, not a blank screen and
  not a redirect to the tab root. "This event was cancelled" answers the question; a silent
  bounce to Home does not.
- **Auth deep links are separate** and stay with the auth handler.

## Modals and sheets are different things

- A **modal** is a full screen with its own task. Covered here.
- A **sheet** is a partial overlay for a choice or a short form, dismissed by the scrim. See
  [pickers and sheets](sheet-picker.md).

Choosing a sheet for something with more than one step, or a modal for a single choice, is the
most common structural mistake in this app.

## Scroll

- **Content clears the tab bar**: bottom inset plus 80. A last row half-hidden behind the bar
  is the most reported small bug in any tab app.
- **Scroll position is preserved** on a tab, and *reset* on a pushed screen — a new record
  starts at the top.
- **Never nest a vertical scroll inside a vertical scroll.**

## Never

- A screen pushed onto the wrong tab's stack.
- A modal inside a modal inside a modal.
- A title in the bar and the same title in the body.
- A tab that forgets where you were.
- A deep link that lands with no way back.
- Disabling back to force a decision.
