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);
}