streaming-ui-script · routing ← Routing docs
Live demo · Routes + NavLink + @Navigate

A multi-page app, in a single Streaming UI Script program

One <streaming-ui-script> tag renders a four-page UI synced to the URL hash. Click the nav, use deep links, hit the browser back button — it all stays in sync, with zero framework lock-in.

Live preview

Try #/dashboard, #/users, then drill into a user (e.g. #/users/ada). Browser back / forward and bookmarks all work.

UI Script source

The nav stays visible across every page; main is a Routes(...) outlet that swaps in the page that matches the current URL. Path parameters land in params, and $route is reactive everywhere else.

$users = [
  {id: "ada",   name: "Ada Lovelace",   role: "Founding engineer", joined: "2019-04-02"},
  {id: "grace", name: "Grace Hopper",   role: "Compiler researcher", joined: "2020-01-15"},
  {id: "lin",   name: "Lin-Manuel",     role: "Product designer", joined: "2021-08-21"},
  {id: "ken",   name: "Ken Thompson",   role: "Platform engineer", joined: "2018-11-04"}
]

$visits = 0
$lastEdited = "—"

root = Stack([header, nav, main])

header = Card([
  CardHeader("Acme console", "Routing demo · current path: " + $route),
  Stack([
    Tag("$route = " + $route, "compass", "sm", "info"),
    Tag("visits = " + $visits, "eye", "sm", "neutral")
  ], "row", "xs")
])

nav = Card([
  Stack([
    NavLink("Home",      "/",          "ghost", true, "house"),
    NavLink("Dashboard", "/dashboard", "ghost", false, "chart-pie"),
    NavLink("Users",     "/users",     "ghost", false, "users"),
    NavLink("Settings",  "/settings",  "ghost", false, "gear")
  ], "row", "s")
])

main = Routes([
  Route("/",           homePage),
  Route("/dashboard",  dashboardPage),
  Route("/users",      usersListPage),
  Route("/users/:id",  userDetailPage),
  Route("/settings",   settingsHomePage),
  Route("/settings/*", settingsAreaPage),
  Route("*",           notFoundPage)
], "/")

homePage = Card([
  CardHeader("Welcome", "A multi-page UI in one Streaming UI Script program"),
  Markdown("Pick a section above, or jump straight in:"),
  Buttons([
    Button("Open dashboard", Action([@Navigate("/dashboard")]), "primary"),
    Button("Browse users",   Action([@Navigate("/users")]),     "secondary"),
    Button("Open Ada",       Action([@Navigate("/users/ada")]), "ghost")
  ])
])

dashboardPage = Card([
  CardHeader("Dashboard", "Reactive across routes"),
  Stack([
    StatCard("Users",   "" + @Count($users), "up", "+2 this month"),
    StatCard("Visits",  "" + $visits,        "up", "this session"),
    StatCard("Last edit", $lastEdited)
  ], "row", "m", "stretch", "start", true),
  Buttons([
    Button("Track a visit", Action([@Set($visits, $visits + 1)]), "primary"),
    Button("Back to home",  Action([@Navigate("/")]),            "ghost")
  ])
])

usersListPage = Card([
  CardHeader("Users", "Click a row to deep-link into the detail page"),
  Stack([@Each($users, "u", userRow)])
])

userRow = Card([
  Stack([
    Stack([
      TextContent(u.name, "body-heavy"),
      TextContent(u.role + " · joined " + u.joined, "small", "muted")
    ]),
    Buttons([Button("Open", Action([@Js("ctx.host.navigate('/users/' + ctx.args.id)", {id: u.id})]), "ghost")])
  ], "row", "m", "center", "between")
], "outlined")

userDetailPage = Card([
  CardHeader("User " + params.id, "Deep-linkable detail page"),
  detailRow,
  Buttons([
    Button("Back to users", Action([@Navigate("/users")]),                                     "ghost"),
    Button("Mark edited",   Action([@Set($lastEdited, params.id), @Navigate("/dashboard")]), "primary")
  ])
])

detailRow = Markdown("Path parameter: **" + params.id + "** · open URL: `#/users/" + params.id + "`")

settingsHomePage = Card([
  CardHeader("Settings", "Wildcard nested route below"),
  Stack([
    NavLink("Profile",       "/settings/profile",       "pill"),
    NavLink("Notifications", "/settings/notifications", "pill"),
    NavLink("Security",      "/settings/security",      "pill")
  ], "row", "s"),
  TextContent("Pick a sub-section above — it's matched by /settings/*.", "small", "muted")
])

settingsAreaPage = Card([
  CardHeader("Settings · " + params._, "Sub-section captured by wildcard"),
  TextContent("params._ = " + params._),
  Buttons([Button("Back to settings", Action([@Navigate("/settings")]), "ghost")])
])

notFoundPage = Callout("warning", "Not found", "No page matches " + $route + ". Use the nav above or go back to /.")

How it works