# Navigation: tabs, versions, languages, products, dropdowns

For anything past a single sidebar, `navigation` can be an object choosing exactly **one** root pattern instead of a flat array — `tabs`, `versions`, `languages`, `dropdowns`, or `products`.

<Callout type="note">
  This mirrors [Mintlify's own `writedocs.json`](https://mintlify.com/docs/organize/navigation): "choose one primary organizational pattern at the root level."
</Callout>

All five container kinds are structurally interchangeable — each owns *exactly one* of `pages`, `tabs`, `versions`, `languages`, `dropdowns`, `products`, or a bare `href`, as its content. That symmetry is what lets any of them nest inside any other, to any depth: a tab can contain versions, a version can contain languages, a product can contain versions that contain tabs, and so on — always bottoming out at a `pages` array (or `openapi`, see [API Reference (OpenAPI)](/docs/openapi/overview/)).

## `tabs`

Renders a horizontal navbar with one pill per tab. When there are more tabs than fit, the row slides sideways instead of wrapping: an arrow shows on each side that has more tabs, and the active tab always starts in view. On a touch screen, swipe the row sideways.

```json
{
  "navigation": {
    "tabs": [
      { "tab": "Guides", "pages": ["index", "getting-started"] },
      { "tab": "API Reference", "pages": ["api/overview"] },
      { "tab": "Blog", "href": "https://blog.example.com" }
    ]
  }
}
```

A tab entry can also be a bare `{ tab, href }` link instead of owning pages — like "Blog" above: it renders as a plain pill that opens `href` directly (external links open in a new tab), with no pages/sidebar of its own. `href` works this same way on any of the five container kinds below (versions, languages, dropdowns, products) — one bare-link escape hatch, not something special to tabs.

## `versions`

Renders a version-switcher dropdown.

```json
{
  "navigation": {
    "versions": [
      { "version": "v2", "label": "v2 (latest)", "tag": "Latest", "default": true, "pages": ["v2/index"] },
      { "version": "v1", "pages": ["v1/index"] }
    ]
  }
}
```

`tag` adds a badge next to the version name (e.g. "Latest", "Deprecated"). `default` picks which version "first page" links resolve to when nothing else determines it. When switching versions, Writedocs tries to keep you on the *same* page across versions (by position, not by matching file paths — versions commonly live under unrelated folder names) rather than always jumping to the target version's first page.

## `languages`

Renders a language-switcher dropdown. `label` controls the display name (defaults to the raw code, like `en`, if omitted).

```json
{
  "navigation": {
    "languages": [
      { "language": "en", "label": "English", "pages": ["index"] },
      { "language": "pt-br", "label": "Português", "pages": ["index"] }
    ]
  }
}
```

Every page under a language is in that language, so a language can't contain another `languages` list - `writedocs validate` and the build reject it. Put the languages at one level, with everything else (tabs, versions, ...) inside each of them.

## `products`

Renders a product-switcher dropdown — for docs covering several distinct offerings.

```json
{
  "navigation": {
    "products": [
      { "product": "Core Platform", "pages": ["core/index"] },
      { "product": "Mobile SDK", "pages": ["mobile/index"] },
      { "product": "Status Page", "href": "https://status.example.com" }
    ]
  }
}
```

At the root, the switcher sits in the top bar. Inside a tab or a dropdown, it sits at the top of the sidebar instead, showing each product's `icon` and `description`:

```json
{
  "navigation": {
    "tabs": [
      { "tab": "Guides", "pages": ["guides/intro"] },
      {
        "tab": "Platform",
        "products": [
          { "product": "Payments", "icon": "credit-card", "description": "Accept cards and wallets", "pages": ["platform/payments/overview"] },
          { "product": "Billing", "icon": "receipt", "description": "Subscriptions and invoices", "pages": ["platform/billing/overview"] }
        ]
      }
    ]
  }
}
```

A page with no sidebar (`mode: custom` or `blank`) shows that switcher in the top bar.

## `dropdowns` (as root)

Each entry is its own independent, always-visible navbar dropdown trigger — rather than one trigger listing several items.

```json
{
  "navigation": {
    "dropdowns": [
      { "dropdown": "Docs", "pages": ["index"] },
      { "dropdown": "API", "pages": ["api/index"] }
    ]
  }
}
```

## A tab (or dropdown) that owns `dropdowns` instead of `pages`

Any container can own `dropdowns` instead of `pages` as its content — it renders as a dropdown-trigger button showing the current selection, instead of a plain pill link:

```json
{
  "navigation": {
    "tabs": [
      {
        "tab": "API Reference",
        "dropdowns": [
          { "dropdown": "REST API", "pages": ["api/rest/index"] },
          { "dropdown": "GraphQL", "pages": ["api/graphql/index"] }
        ]
      }
    ]
  }
}
```

## Nesting containers

Any of the five kinds can contain any other, to any depth - except a language inside another language (see [`languages`](#languages)), and a third level of tabs (see below). For example, a tab containing versions, where each version has its own tabs bar:

```json
{
  "navigation": {
    "tabs": [
      {
        "tab": "API Reference",
        "versions": [
          {
            "version": "v2",
            "tabs": [
              { "tab": "Guides", "pages": ["api/v2/guides"] },
              { "tab": "Reference", "pages": ["api/v2/reference"] }
            ]
          }
        ]
      }
    ]
  }
}
```

Tabs inside tabs, like these, get a second row under the first: the outer tabs on top, and the tabs of the active one underneath. Two levels of tabs is the limit, counting through anything in between - `writedocs validate` and the build reject a third. Use a dropdown or groups for that level instead.

Every branch of the tree is free to be as deep or shallow as that section needs — a product with versions and tabs can sit alongside a sibling product that skips straight to `pages`.

## Hidden sections

Any tab, product, version, language or dropdown can be `hidden`: reachable only by its address. Use it for docs that share a site but are meant for a different audience — an admin guide next to the user guide, a partner program, an internal API:

```json
{
  "navigation": {
    "products": [
      { "product": "User guide", "pages": ["user/intro", "user/setup"] },
      { "product": "Admin", "hidden": true, "tabs": [
        { "tab": "Setup", "pages": ["admin/setup"] },
        { "tab": "Security", "pages": ["admin/security"] }
      ] },
      { "product": "Partners", "hidden": true, "searchPublic": true, "pages": ["partners/intro"] }
    ]
  }
}
```

- **Nothing links to it.** No tab, switcher, menu or mobile menu lists a hidden item, and the site's address (`/`) never opens it. Share its pages' addresses (`/admin/setup/`) with the people it's for.
- **Inside it, it reads like a site of its own.** The level it sits on isn't shown — a reader of the Admin docs sees no product switcher, since it would list the other products. Everything under it works as usual: its tabs, versions, languages, sidebar, and previous/next links.
- **Its search is its own.** Searching from a hidden item's pages finds only those pages, and the site's search never finds them. With `"searchPublic": true`, its search finds the public pages too — useful when, say, the Admin docs build on the user guide.

<Parameter name="hidden" type="boolean" default="false">
  Leaves the item out of every tab, switcher and menu. Its pages are still built, at their usual addresses.
</Parameter>

<Parameter name="searchPublic" type="boolean" default="false">
  For a hidden item: its search also finds the site's public pages. `writedocs validate` rejects it on an item that isn't hidden.
</Parameter>

<Callout type="warning">
  Hidden isn't private. Anyone with a page's address can read it, and its pages are listed in `sitemap.xml`, `llms.txt` and the MCP server like any other — set `noindex: true` in a page's frontmatter to leave it out of those. For docs that must stay private, put a login in front of their addresses (on Cloudflare, Cloudflare Access) or publish them as a separate site.
</Callout>

If every top-level item is hidden, add an `index.mdx` at the project root for the site's home page — otherwise its address shows "not found", and `writedocs validate` warns about it.