Navigation

Navigation demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.

App.tsx

import './App.css';

import { Component, get } from '@expressive/react';
import { Link, NavLinks, Route, Router } from '@expressive/router';
import type { ReactNode } from 'react';

export default () => (
  <Router>
    <Route as={Frame}>
      <Route as={Home} label="Home" />
      <Route to="guides" label="Guides">
        <Route to="start" as={Page} label="Getting started" />
        <Route to="deploy" as={Page} label="Deploying" />
      </Route>
      <Route to="reference" label="Reference">
        <Route to="api" as={Page} label="API" />
      </Route>
    </Route>
  </Router>
);

const Frame = (props: { children?: ReactNode }) => (
  <div className="container">
    <h1>Navigation</h1>
    <p>
      <code>NavLinks</code> renders the menu from the route tree itself, so the
      JSX that decides what matches also decides what is listed - the two cannot
      drift. Override <code>List</code>, <code>Item</code> and <code>Group</code>{' '}
      to say how each layer looks; a scope with no page of its own arrives as a
      group.
    </p>
    <div className="layout">
      <Menu />
      <div className="view">{props.children}</div>
    </div>
    <small>
      A <code>Link</code> that reads <code>active</code> subscribes to navigation,
      which is all an active-link component ever needed - so there is no NavLink
      here, just a subclass that renders its own anchor.
    </small>
  </div>
);

class Menu extends NavLinks {
  List(props: { children?: ReactNode }) {
    return <nav className="menu">{props.children}</nav>;
  }

  Item(props: { route: Route; label?: string }) {
    return <Tab to={props.route.path}>{props.label}</Tab>;
  }

  Group(props: { route: Route; children?: ReactNode }) {
    return (
      <section className="group">
        <h4>{props.route.label}</h4>
        {props.children}
      </section>
    );
  }
}

class Tab extends Link {
  render() {
    const { href, active, match } = this;

    return (
      <a
        href={href}
        onClick={this.go}
        className={match ? 'tab here' : active ? 'tab near' : 'tab'}>
        {this.props.children}
      </a>
    );
  }
}

const Home = () => <p>Pick anything in the menu.</p>;

class Page extends Component {
  route = get(Route);

  render() {
    const { label, path } = this.route;

    return (
      <p className="page">
        <b>{label}</b>
        <code>{path}</code>
      </p>
    );
  }
}

App.css

.layout {
  display: flex;
  align-items: flex-start;
  gap: var(--s4);
  width: 100%;
  text-align: left;
}

.menu {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  flex: 0 0 11rem;
}

.group {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  gap: var(--s1);
}

.group h4 {
  margin: 0;
  font-size: var(--t-xs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.tab {
  padding: var(--s1) var(--s2);
  border-radius: var(--r-sm);
  font-size: var(--t-sm);
  color: var(--fg-soft);
  cursor: pointer;
  text-decoration: none;
}

.tab:hover {
  background: var(--surface-2);
  text-decoration: none;
}

.tab.near {
  color: var(--fg);
}

.tab.here {
  color: var(--accent);
  background: var(--accent-soft);
}

.view {
  flex: 1;
  min-height: 7rem;
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}

.view p {
  margin: 0;
}

.page {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
}

.page code {
  color: var(--muted);
}