Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

SubNav

SubNav("Invoices") { foreach (var i in invoices) { SubNavItem(i.Number, selected: i == open, onPress: () => Open(i), hint: i.Due); } }

Navigation inside a tab: a list down the side of a tab's body that moves between that tab's own places — a record's invoices one at a time, a settings tab's sections — so a record can have tabs and still go deeper inside one without a second tab strip. Each SubNavItem is one place; below Tablet the list becomes a row of chips that scrolls sideways.

stable1 example compiled by CIuikitnavigationlayout

Summary#

A record's tabs say WHICH part of it you are looking at; SubNav says which ONE of that part's things. It is a vertical list of SubNavItems down the side of a tab's body — each with its label, an optional hint under it (a date, an amount) and an optional count — and the chosen one is selected. Put the content beside it in a grid or a row.

class Bill { public string Number; public string Due; public string Total; }

List<Bill> Bills() {
  return [
    new Bill { Number = "F-2041", Due = "31 Oct", Total = "1,840.00" },
    new Bill { Number = "F-2007", Due = "30 Sep", Total = "1,840.00" },
  ];
}

[Route("/client")] [Render(CSR)] [AllowAnonymous]
component ClientPage() {
  string open = "F-2041";
  action Show(string number) { open = number; }
  render {
    Stack(gap: 4, p: 6) {
      Tabs { Tab("Overview"); Tab("Invoices", selected: true); }
      Row(gap: 5, align: Align.Start) {
        SubNav("Client invoices") {
          foreach (var b in Bills()) {
            SubNavItem(b.Number, selected: open == b.Number, onPress: () => Show(b.Number), hint: "Due " + b.Due);
          }
        }
        Card("Invoice " + open) { Text(Bills().First(b => b.Number == open).Total); }
      }
    }
  }
}

Signature#

SubNav(string label) { SubNavItem(…); … }
SubNavItem(string label, bool selected = false, Action onPress = null, string hint = "", string count = "")

Description#

When is this better than another row of tabs?#

When the places are DATA rather than a fixed set — a record's invoices, its contacts, a list of saved views — or when there are more than a handful. A second tab strip under the first reads as two levels of the same thing and runs out of width; a list down the side scales with the data and says its places in full.

What does it become on a phone?#

Below Tablet the list lays out as a row of chips that scrolls sideways, above the content — the same items, the same selection, in the width a phone has.

How is it announced?#

As a tab list named by label, each item a tab that is selected or not — which is what it is. Give the SubNav a label that differs from the tab that holds it ("Client invoices" inside the "Invoices" tab), so a scope naming one does not match both.

How do I drive it in a test?#

Ui.Click("F-2007", within: "Client invoices"), then Assert.Selected("F-2007").

See also#

Related

Arrangements — five ways to build a whole app, and which look wears each

A look decides how an app's pixels read; an ARRANGEMENT decides how the app is built — where navigation lives, how a…

Dock

The page's drawers: the page in the middle, a drawer docked on the right (the record a row opened, an inspector) and…

Paper

A sheet of paper on the page's ground — a reading width, generous margins, the theme's hairline and lift — so an…

TabbedShell

An app shell whose primary navigation is a horizontal strip of tabs under the brand row, and a bottom tab bar within…