Wiki — Collapsible Doc Tree Nav
A dev-docs style collapsible documentation tree sidebar with nested sections, expand and collapse chevrons, indented child links, and a current-page highlight. A sticky filter box live-searches the tree, auto-expands matching branches, and highlights matched text. Keyboard arrows move between visible rows and Enter opens a page. On small screens the sidebar becomes a slide-in drawer beside a readable serif article column.
MCP
Código
:root {
--bg: #ffffff;
--bg-2: #f7f8fa;
--panel: #ffffff;
--ink: #1a1a1f;
--ink-2: #3a3a42;
--muted: #6b7280;
--line: rgba(20, 20, 30, 0.1);
--line-2: rgba(20, 20, 30, 0.18);
--link: #2563eb;
--link-hover: #1d4ed8;
--accent: #2563eb;
--accent-soft: rgba(37, 99, 235, 0.09);
--note: #2563eb;
--tip: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--code-bg: #f4f4f6;
--kbd-bg: #eceef2;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 14px;
--shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.05);
--shadow-md: 0 8px 28px rgba(20, 20, 30, 0.12);
--sans: "Inter", system-ui, -apple-system, sans-serif;
--serif: "Source Serif 4", Georgia, "Times New Roman", serif;
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
--sidebar-w: 288px;
--toc-w: 220px;
--topbar-h: 56px;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: var(--sans);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
line-height: 1.55;
}
a {
color: var(--link);
text-decoration: none;
}
a:hover {
color: var(--link-hover);
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 4px;
}
.skip-link {
position: fixed;
top: -60px;
left: 12px;
z-index: 100;
background: var(--ink);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
font-size: 13px;
font-weight: 600;
transition: top 0.15s;
}
.skip-link:focus {
top: 12px;
color: #fff;
}
/* ───────────────────────── Topbar ───────────────────────── */
.topbar {
position: sticky;
top: 0;
z-index: 40;
height: var(--topbar-h);
display: flex;
align-items: center;
gap: 12px;
padding: 0 18px;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: saturate(1.4) blur(10px);
border-bottom: 1px solid var(--line);
}
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border: 1px solid transparent;
border-radius: var(--r-sm);
background: transparent;
color: var(--ink-2);
cursor: pointer;
transition: background 0.12s, color 0.12s;
}
.icon-btn:hover {
background: var(--bg-2);
color: var(--ink);
}
.nav-toggle {
display: none;
}
.brand {
display: inline-flex;
align-items: baseline;
gap: 8px;
font-weight: 700;
color: var(--ink);
}
.brand-mark {
color: var(--accent);
font-size: 15px;
transform: translateY(1px);
}
.brand-name {
font-size: 16px;
letter-spacing: -0.01em;
}
.brand-tag {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: 4px;
padding: 1px 6px;
}
.topbar-spacer {
flex: 1;
}
.version-pill {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px 11px;
}
.version-pill .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--tip);
box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.15);
}
.ghost-link {
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
}
.ghost-link:hover {
color: var(--accent);
}
/* ───────────────────────── Shell ───────────────────────── */
.shell {
display: grid;
grid-template-columns: var(--sidebar-w) minmax(0, 1fr) var(--toc-w);
align-items: start;
max-width: 1380px;
margin: 0 auto;
}
.scrim {
display: none;
}
/* ───────────────────────── Sidebar ───────────────────────── */
.sidebar {
position: sticky;
top: var(--topbar-h);
height: calc(100vh - var(--topbar-h));
display: flex;
flex-direction: column;
border-right: 1px solid var(--line);
background: var(--bg);
}
.search-wrap {
padding: 14px 14px 10px;
border-bottom: 1px solid var(--line);
}
.search-field {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 11px;
color: var(--muted);
pointer-events: none;
}
.search-input {
width: 100%;
font: inherit;
font-size: 13.5px;
color: var(--ink);
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 9px 34px 9px 34px;
transition: border-color 0.12s, background 0.12s, box-shadow 0.12s;
}
.search-input::placeholder {
color: var(--muted);
}
.search-input:focus {
outline: none;
background: var(--panel);
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.search-kbd {
position: absolute;
right: 10px;
}
kbd,
.search-kbd {
font-family: var(--mono);
font-size: 11px;
line-height: 1;
color: var(--ink-2);
background: var(--kbd-bg);
border: 1px solid var(--line-2);
border-bottom-width: 2px;
border-radius: 5px;
padding: 3px 5px;
min-width: 16px;
text-align: center;
}
.search-hint {
margin: 9px 2px 0;
font-size: 11.5px;
color: var(--muted);
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.search-hint kbd {
padding: 2px 4px;
}
.tree-scroll {
flex: 1;
overflow-y: auto;
padding: 10px 8px 16px;
scrollbar-width: thin;
}
.tree-scroll::-webkit-scrollbar {
width: 8px;
}
.tree-scroll::-webkit-scrollbar-thumb {
background: var(--line-2);
border-radius: 8px;
border: 2px solid var(--bg);
}
.tree,
.tree ul {
list-style: none;
margin: 0;
padding: 0;
}
.tree ul {
/* nested groups */
overflow: hidden;
}
.node-row {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
border: none;
background: transparent;
font: inherit;
font-size: 13.5px;
color: var(--ink-2);
text-align: left;
padding: 6px 10px 6px 8px;
border-radius: var(--r-sm);
cursor: pointer;
position: relative;
transition: background 0.1s, color 0.1s;
}
.node-row:hover {
background: var(--bg-2);
color: var(--ink);
}
.node-row.is-active {
background: var(--accent-soft);
color: var(--link);
font-weight: 600;
}
.node-row.is-active::before {
content: "";
position: absolute;
left: 0;
top: 6px;
bottom: 6px;
width: 3px;
border-radius: 3px;
background: var(--accent);
}
.node-row.kb-focus {
box-shadow: 0 0 0 2px var(--accent);
}
.chevron {
flex: none;
width: 16px;
height: 16px;
color: var(--muted);
transition: transform 0.18s ease;
}
.node-row[aria-expanded="true"] .chevron {
transform: rotate(90deg);
}
.chevron-spacer {
width: 16px;
flex: none;
}
.node-icon {
flex: none;
width: 15px;
height: 15px;
color: var(--muted);
opacity: 0.85;
}
.node-row.is-active .node-icon,
.node-row.is-active .chevron {
color: var(--link);
}
.node-label {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-label mark {
background: rgba(217, 119, 6, 0.22);
color: inherit;
border-radius: 3px;
padding: 0 1px;
}
.node-badge {
flex: none;
font-size: 9.5px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 1px 5px;
border-radius: 4px;
border: 1px solid var(--line);
color: var(--muted);
}
.node-badge.new {
color: var(--tip);
border-color: rgba(22, 163, 74, 0.35);
background: rgba(22, 163, 74, 0.08);
}
.node-badge.beta {
color: var(--warn);
border-color: rgba(217, 119, 6, 0.35);
background: rgba(217, 119, 6, 0.08);
}
/* nested indentation guides */
.tree > li > ul {
margin-left: 18px;
border-left: 1px solid var(--line);
padding-left: 4px;
}
.tree > li > ul ul {
margin-left: 16px;
border-left: 1px solid var(--line);
padding-left: 4px;
}
li[hidden] {
display: none !important;
}
.tree-empty {
padding: 16px 12px;
font-size: 13px;
color: var(--muted);
text-align: center;
}
.side-foot {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-top: 1px solid var(--line);
font-size: 12px;
color: var(--muted);
}
.text-btn {
border: none;
background: none;
font: inherit;
font-size: 12px;
font-weight: 600;
color: var(--accent);
cursor: pointer;
padding: 0;
}
.text-btn:hover {
color: var(--link-hover);
text-decoration: underline;
}
/* ───────────────────────── Content ───────────────────────── */
.content {
min-width: 0;
padding: 30px clamp(20px, 4vw, 56px) 80px;
}
.content:focus {
outline: none;
}
.crumbs {
display: flex;
align-items: center;
gap: 7px;
font-size: 12.5px;
color: var(--muted);
margin-bottom: 18px;
}
.crumbs a {
color: var(--muted);
}
.crumbs a:hover {
color: var(--accent);
}
.crumbs [aria-current] {
color: var(--ink-2);
font-weight: 600;
}
.prose {
max-width: 740px;
}
.eyebrow {
display: inline-block;
font-size: 11.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.09em;
color: var(--accent);
margin-bottom: 8px;
}
.prose h1 {
font-family: var(--sans);
font-size: clamp(1.9rem, 4vw, 2.5rem);
line-height: 1.12;
letter-spacing: -0.02em;
margin: 0 0 14px;
font-weight: 800;
}
.prose h2 {
font-family: var(--sans);
font-size: 1.42rem;
letter-spacing: -0.01em;
margin: 42px 0 12px;
padding-top: 8px;
font-weight: 700;
scroll-margin-top: calc(var(--topbar-h) + 14px);
}
.lede {
font-family: var(--serif);
font-size: 1.18rem;
line-height: 1.6;
color: var(--ink-2);
margin: 0 0 8px;
}
.prose p,
.prose li {
font-family: var(--serif);
font-size: 1.02rem;
line-height: 1.7;
color: var(--ink-2);
}
.prose p {
margin: 0 0 16px;
}
.prose strong {
color: var(--ink);
font-weight: 600;
}
.prose em {
font-style: italic;
}
.prose :not(pre) > code {
font-family: var(--mono);
font-size: 0.86em;
background: var(--code-bg);
border: 1px solid var(--line);
border-radius: 5px;
padding: 1px 5px;
color: #b3286b;
}
pre {
font-family: var(--mono);
font-size: 13px;
line-height: 1.65;
background: var(--code-bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 18px;
overflow-x: auto;
margin: 0 0 20px;
color: var(--ink);
}
pre code {
font-family: inherit;
}
pre .c {
color: #6b7280;
font-style: italic;
}
pre .k {
color: #7c3aed;
font-weight: 600;
}
pre .s {
color: #16a34a;
}
pre .n {
color: #d97706;
}
pre .fn {
color: #2563eb;
}
.doc-table {
width: 100%;
border-collapse: collapse;
font-family: var(--sans);
font-size: 13.5px;
margin: 0 0 22px;
}
.doc-table th,
.doc-table td {
text-align: left;
padding: 9px 12px;
border-bottom: 1px solid var(--line);
}
.doc-table thead th {
font-weight: 700;
color: var(--ink);
background: var(--bg-2);
border-bottom: 1px solid var(--line-2);
}
.doc-table tbody tr:hover {
background: var(--bg-2);
}
.doc-table td {
color: var(--ink-2);
}
.doc-table code {
font-size: 0.82em;
}
.callout {
display: flex;
gap: 12px;
align-items: flex-start;
border: 1px solid var(--line);
border-left-width: 3px;
border-radius: var(--r-md);
padding: 13px 16px;
margin: 0 0 22px;
background: var(--bg-2);
}
.callout div {
font-family: var(--sans);
font-size: 13.5px;
line-height: 1.6;
color: var(--ink-2);
}
.callout code {
font-size: 0.84em;
}
.callout-icon {
flex: none;
width: 22px;
height: 22px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 12px;
font-weight: 800;
color: #fff;
}
.callout.note {
border-left-color: var(--note);
background: rgba(37, 99, 235, 0.05);
}
.callout.note .callout-icon {
background: var(--note);
font-style: italic;
font-family: var(--serif);
}
.callout.tip {
border-left-color: var(--tip);
background: rgba(22, 163, 74, 0.05);
}
.callout.tip .callout-icon {
background: var(--tip);
}
.callout.warn {
border-left-color: var(--warn);
background: rgba(217, 119, 6, 0.05);
}
.callout.warn .callout-icon {
background: var(--warn);
}
.steps {
counter-reset: step;
list-style: none;
padding: 0;
margin: 0 0 20px;
}
.steps li {
position: relative;
padding-left: 38px;
margin-bottom: 12px;
counter-increment: step;
}
.steps li::before {
content: counter(step);
position: absolute;
left: 0;
top: 1px;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--accent-soft);
color: var(--accent);
font-family: var(--sans);
font-weight: 700;
font-size: 13px;
display: grid;
place-items: center;
}
.check {
list-style: none;
padding: 0;
margin: 0 0 20px;
}
.check li {
position: relative;
padding-left: 28px;
margin-bottom: 10px;
}
.check li::before {
content: "✓";
position: absolute;
left: 0;
top: 0;
color: var(--tip);
font-weight: 800;
font-family: var(--sans);
}
blockquote {
margin: 0 0 22px;
padding: 4px 0 4px 20px;
border-left: 3px solid var(--accent);
}
blockquote p {
font-family: var(--serif);
font-style: italic;
font-size: 1.08rem;
color: var(--ink);
margin: 0 0 8px;
}
blockquote cite {
font-family: var(--sans);
font-style: normal;
font-size: 13px;
color: var(--muted);
}
.prose hr {
border: none;
border-top: 1px solid var(--line);
margin: 32px 0 18px;
}
.page-meta {
font-family: var(--sans) !important;
font-size: 13px !important;
color: var(--muted) !important;
}
/* ───────────────────────── TOC ───────────────────────── */
.toc {
position: sticky;
top: calc(var(--topbar-h) + 24px);
align-self: start;
padding: 28px 22px 0 4px;
}
.toc-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
margin: 0 0 12px;
}
.toc-list {
list-style: none;
margin: 0;
padding: 0;
border-left: 1px solid var(--line);
}
.toc-list a {
display: block;
font-size: 13px;
color: var(--muted);
padding: 5px 0 5px 14px;
margin-left: -1px;
border-left: 2px solid transparent;
transition: color 0.12s, border-color 0.12s;
}
.toc-list a:hover {
color: var(--ink);
}
.toc-list a.active {
color: var(--link);
border-left-color: var(--accent);
font-weight: 600;
}
/* ───────────────────────── Toast ───────────────────────── */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 16px);
background: var(--ink);
color: #fff;
font-size: 13px;
font-weight: 500;
padding: 10px 16px;
border-radius: var(--r-md);
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
z-index: 80;
max-width: min(90vw, 340px);
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ───────────────────────── Responsive ───────────────────────── */
@media (max-width: 1080px) {
.shell {
grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
}
.toc {
display: none;
}
}
@media (max-width: 820px) {
.nav-toggle {
display: inline-flex;
}
.shell {
grid-template-columns: minmax(0, 1fr);
}
.sidebar {
position: fixed;
top: 0;
left: 0;
z-index: 60;
width: min(86vw, 320px);
height: 100vh;
transform: translateX(-100%);
transition: transform 0.24s ease;
box-shadow: var(--shadow-md);
}
body.nav-open .sidebar {
transform: translateX(0);
}
.scrim {
display: block;
position: fixed;
inset: 0;
z-index: 55;
background: rgba(15, 15, 25, 0.4);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
body.nav-open .scrim {
opacity: 1;
pointer-events: auto;
}
}
@media (max-width: 520px) {
.topbar {
padding: 0 12px;
gap: 8px;
}
.version-pill,
.ghost-link {
display: none;
}
.content {
padding: 22px 16px 64px;
}
.prose h1 {
font-size: 1.7rem;
}
.lede {
font-size: 1.08rem;
}
}
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}/* Aurora DB Docs — collapsible doc tree nav
Vanilla JS: expand/collapse, live filter, keyboard nav, mobile drawer. */
(function () {
"use strict";
/* ─── Doc tree data (fictional Aurora DB docs) ─── */
const TREE = [
{
label: "Getting started",
icon: "rocket",
open: true,
children: [
{ label: "Introduction", href: "#intro", active: false },
{ label: "Install the CLI", href: "#install" },
{ label: "Your first cluster", href: "#first-cluster" },
{ label: "Connecting a client", href: "#connect" },
],
},
{
label: "Guides",
icon: "book",
open: true,
children: [
{ label: "Schema modeling", href: "#schema" },
{ label: "Indexes & query plans", href: "#indexes" },
{
label: "Replication & failover",
href: "#replication",
active: true,
},
{ label: "Backups & restore", href: "#backups" },
{ label: "Scaling read replicas", href: "#scaling", badge: "new" },
{
label: "Operations",
icon: "gear",
children: [
{ label: "Health checks", href: "#health" },
{ label: "Rolling upgrades", href: "#upgrades" },
{ label: "Disaster recovery", href: "#dr" },
],
},
],
},
{
label: "API reference",
icon: "code",
children: [
{ label: "connect()", href: "#api-connect" },
{ label: "query()", href: "#api-query" },
{ label: "transaction()", href: "#api-tx" },
{
label: "Cluster admin",
icon: "gear",
children: [
{ label: "failover()", href: "#api-failover" },
{ label: "promote()", href: "#api-promote" },
{ label: "snapshot()", href: "#api-snapshot", badge: "beta" },
],
},
],
},
{
label: "Reference",
icon: "book",
children: [
{ label: "Configuration keys", href: "#config" },
{ label: "Consistency model (Helios)", href: "#helios" },
{ label: "Error codes", href: "#errors" },
{ label: "Glossary", href: "#glossary" },
{ label: "CLI command index", href: "#cli-index" },
],
},
];
const ICONS = {
rocket:
'<path d="M5 13l2 2-3 4 4-3 2 2 7-7c2-2 3-7 3-7s-5 1-7 3l-7 7z" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linejoin="round"/>',
book: '<path d="M5 4h9a2 2 0 012 2v13H7a2 2 0 00-2 2V4z" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linejoin="round"/>',
gear: '<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.6" fill="none"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1" stroke="currentColor" stroke-width="1.4"/>',
code: '<path d="M9 8l-4 4 4 4M15 8l4 4-4 4" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
page: '<path d="M7 3h7l4 4v14H7V3z" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linejoin="round"/>',
chevron:
'<path d="M9 6l6 6-6 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
};
const tree = document.getElementById("tree");
const searchInput = document.getElementById("treeSearch");
const resultCount = document.getElementById("resultCount");
const treeEmpty = document.getElementById("treeEmpty");
const emptyTerm = document.getElementById("emptyTerm");
const toastEl = document.getElementById("toast");
let leafCount = 0;
const rows = []; // flat list of all .node-row buttons, in DOM order
let kbIndex = -1;
function svg(inner, cls) {
return `<svg class="${cls}" viewBox="0 0 24 24" aria-hidden="true">${inner}</svg>`;
}
/* ─── Render tree recursively ─── */
function render(nodes, parent, depth) {
nodes.forEach((node) => {
const li = document.createElement("li");
li.setAttribute("role", "none");
const hasChildren = Array.isArray(node.children) && node.children.length;
const row = document.createElement(hasChildren ? "button" : "a");
row.className = "node-row";
row.setAttribute("role", "treeitem");
row.dataset.label = node.label.toLowerCase();
let html = "";
if (hasChildren) {
row.type = "button";
row.setAttribute("aria-expanded", node.open ? "true" : "false");
html += svg(ICONS.chevron, "chevron");
} else {
row.href = node.href || "#";
html += '<span class="chevron-spacer"></span>';
}
html += svg(ICONS[node.icon] || ICONS.page, "node-icon");
html += `<span class="node-label">${node.label}</span>`;
if (node.badge) {
html += `<span class="node-badge ${node.badge}">${node.badge}</span>`;
}
row.innerHTML = html;
if (node.active) {
row.classList.add("is-active");
row.setAttribute("aria-current", "page");
}
li.appendChild(row);
rows.push(row);
if (hasChildren) {
const ul = document.createElement("ul");
ul.setAttribute("role", "group");
if (!node.open) li.dataset.collapsed = "true";
ul.hidden = !node.open;
render(node.children, ul, depth + 1);
li.appendChild(ul);
row.addEventListener("click", () => toggle(row, ul));
} else {
leafCount++;
row.addEventListener("click", (e) => {
e.preventDefault();
activate(row);
toast("Opened “" + node.label + "”");
closeDrawer();
});
}
parent.appendChild(li);
});
}
function toggle(row, ul, force) {
const willOpen =
typeof force === "boolean"
? force
: row.getAttribute("aria-expanded") !== "true";
row.setAttribute("aria-expanded", willOpen ? "true" : "false");
ul.hidden = !willOpen;
}
function activate(row) {
rows.forEach((r) => {
r.classList.remove("is-active");
r.removeAttribute("aria-current");
});
row.classList.add("is-active");
row.setAttribute("aria-current", "page");
}
render(TREE, tree, 0);
if (resultCount) resultCount.textContent = leafCount;
/* ─── Expand / collapse all ─── */
function setAll(open) {
tree.querySelectorAll('.node-row[aria-expanded]').forEach((row) => {
const ul = row.parentElement.querySelector(":scope > ul");
if (ul) toggle(row, ul, open);
});
}
document
.getElementById("expandAll")
.addEventListener("click", () => setAll(true));
document
.getElementById("collapseAll")
.addEventListener("click", () => setAll(false));
/* ─── Live filter ─── */
function escapeRe(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function clearMarks() {
tree.querySelectorAll(".node-label").forEach((lbl) => {
if (lbl.dataset.raw) lbl.innerHTML = lbl.dataset.raw;
});
}
function filter(term) {
term = term.trim().toLowerCase();
clearMarks();
if (!term) {
// reset: show everything, restore each branch's natural open state
tree.querySelectorAll("li").forEach((li) => (li.hidden = false));
tree.querySelectorAll('.node-row[aria-expanded]').forEach((row) => {
const ul = row.parentElement.querySelector(":scope > ul");
if (ul) toggle(row, ul, li_initiallyOpen(row));
});
treeEmpty.hidden = true;
tree.hidden = false;
resultCount.textContent = leafCount;
return;
}
const re = new RegExp("(" + escapeRe(term) + ")", "ig");
let matches = 0;
// First pass: decide which rows match.
rows.forEach((row) => {
const li = row.parentElement;
const label = row.dataset.label;
const hit = label.indexOf(term) !== -1;
li.dataset.matchSelf = hit ? "1" : "0";
if (hit && row.classList.contains("node-row") && !row.querySelector(".chevron")) {
// leaf hit
}
});
// Second pass: a li is visible if it matches OR has a visible descendant.
function visit(ul) {
let anyVisible = false;
ul.querySelectorAll(":scope > li").forEach((li) => {
const row = li.querySelector(":scope > .node-row");
const childUl = li.querySelector(":scope > ul");
const selfHit = li.dataset.matchSelf === "1";
let childVisible = false;
if (childUl) childVisible = visit(childUl);
const visible = selfHit || childVisible;
li.hidden = !visible;
if (visible) {
anyVisible = true;
if (row && !row.querySelector(".chevron")) matches++;
// auto-expand branches that contain matches
if (childUl) {
toggle(row, childUl, true);
}
// highlight the matching part of the label
const lbl = row && row.querySelector(".node-label");
if (lbl && selfHit) {
if (!lbl.dataset.raw) lbl.dataset.raw = lbl.innerHTML;
lbl.innerHTML = lbl.dataset.raw.replace(re, "<mark>$1</mark>");
}
}
});
return anyVisible;
}
visit(tree);
resultCount.textContent = matches;
if (matches === 0) {
tree.hidden = true;
treeEmpty.hidden = false;
emptyTerm.textContent = term;
} else {
tree.hidden = false;
treeEmpty.hidden = true;
}
}
// remember initial open state for reset
const initialOpen = new WeakMap();
tree.querySelectorAll('.node-row[aria-expanded]').forEach((row) => {
initialOpen.set(row, row.getAttribute("aria-expanded") === "true");
});
function li_initiallyOpen(row) {
return initialOpen.get(row) === true;
}
let filterTimer;
searchInput.addEventListener("input", () => {
clearTimeout(filterTimer);
const v = searchInput.value;
filterTimer = setTimeout(() => filter(v), 90);
kbIndex = -1;
setKbFocus(-1);
});
/* ─── Keyboard navigation ─── */
function visibleRows() {
return rows.filter((r) => {
let el = r;
while (el && el !== tree) {
if (el.tagName === "LI" && el.hidden) return false;
el = el.parentElement;
}
return true;
});
}
function setKbFocus(i) {
rows.forEach((r) => r.classList.remove("kb-focus"));
const vis = visibleRows();
if (i < 0 || i >= vis.length) return;
const row = vis[i];
row.classList.add("kb-focus");
row.scrollIntoView({ block: "nearest" });
kbCurrent = row;
}
let kbCurrent = null;
function moveKb(delta) {
const vis = visibleRows();
if (!vis.length) return;
let idx = vis.indexOf(kbCurrent);
idx = idx === -1 ? (delta > 0 ? 0 : vis.length - 1) : idx + delta;
idx = Math.max(0, Math.min(vis.length - 1, idx));
kbIndex = idx;
setKbFocus(idx);
}
searchInput.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
moveKb(1);
} else if (e.key === "ArrowUp") {
e.preventDefault();
moveKb(-1);
} else if (e.key === "Enter" && kbCurrent) {
e.preventDefault();
kbCurrent.click();
} else if (e.key === "Escape") {
searchInput.value = "";
filter("");
kbIndex = -1;
setKbFocus(-1);
} else if (
(e.key === "ArrowRight" || e.key === "ArrowLeft") &&
kbCurrent &&
kbCurrent.hasAttribute("aria-expanded")
) {
e.preventDefault();
const ul = kbCurrent.parentElement.querySelector(":scope > ul");
if (ul) toggle(kbCurrent, ul, e.key === "ArrowRight");
}
});
// Arrow navigation also works when a tree row itself has focus.
tree.addEventListener("keydown", (e) => {
const row = e.target.closest(".node-row");
if (!row) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
const vis = visibleRows();
let idx = vis.indexOf(row) + (e.key === "ArrowDown" ? 1 : -1);
idx = Math.max(0, Math.min(vis.length - 1, idx));
vis[idx].focus();
} else if (e.key === "ArrowRight" && row.hasAttribute("aria-expanded")) {
const ul = row.parentElement.querySelector(":scope > ul");
if (ul && row.getAttribute("aria-expanded") === "false") {
e.preventDefault();
toggle(row, ul, true);
}
} else if (e.key === "ArrowLeft" && row.hasAttribute("aria-expanded")) {
const ul = row.parentElement.querySelector(":scope > ul");
if (ul && row.getAttribute("aria-expanded") === "true") {
e.preventDefault();
toggle(row, ul, false);
}
}
});
/* Global "/" focuses the filter */
document.addEventListener("keydown", (e) => {
if (
e.key === "/" &&
document.activeElement !== searchInput &&
!/^(input|textarea|select)$/i.test(document.activeElement.tagName)
) {
e.preventDefault();
searchInput.focus();
searchInput.select();
}
});
/* ─── Mobile drawer ─── */
const navToggle = document.getElementById("navToggle");
const scrim = document.getElementById("scrim");
function openDrawer() {
document.body.classList.add("nav-open");
navToggle.setAttribute("aria-expanded", "true");
scrim.hidden = false;
}
function closeDrawer() {
if (!document.body.classList.contains("nav-open")) return;
document.body.classList.remove("nav-open");
navToggle.setAttribute("aria-expanded", "false");
}
navToggle.addEventListener("click", () => {
document.body.classList.contains("nav-open") ? closeDrawer() : openDrawer();
});
scrim.addEventListener("click", closeDrawer);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeDrawer();
});
/* ─── TOC scroll spy ─── */
const tocLinks = Array.from(document.querySelectorAll(".toc-list a"));
const sections = tocLinks
.map((a) => document.querySelector(a.getAttribute("href")))
.filter(Boolean);
if ("IntersectionObserver" in window && sections.length) {
const obs = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const id = entry.target.id;
tocLinks.forEach((a) =>
a.classList.toggle("active", a.getAttribute("href") === "#" + id)
);
}
});
},
{ rootMargin: "-10% 0px -70% 0px", threshold: 0 }
);
sections.forEach((s) => obs.observe(s));
}
/* ─── Toast helper ─── */
let toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 2200);
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aurora DB Docs — Collapsible Doc Tree Nav</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Source+Serif+4:opsz,[email protected],400;8..60,500;8..60,600&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#article">Skip to content</a>
<header class="topbar">
<button
class="icon-btn nav-toggle"
id="navToggle"
aria-label="Open navigation"
aria-expanded="false"
aria-controls="sidebar"
>
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
<path d="M3 6h18M3 12h18M3 18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
<a class="brand" href="#" aria-label="Aurora DB documentation home">
<span class="brand-mark" aria-hidden="true">◆</span>
<span class="brand-name">Aurora DB</span>
<span class="brand-tag">Docs</span>
</a>
<div class="topbar-spacer"></div>
<div class="version-pill" title="Documentation version">
<span class="dot" aria-hidden="true"></span>v3.2 · stable
</div>
<a class="ghost-link" href="#changelog">Changelog</a>
</header>
<div class="shell">
<div class="scrim" id="scrim" hidden></div>
<!-- ───────────────────────── Sidebar ───────────────────────── -->
<nav class="sidebar" id="sidebar" aria-label="Documentation">
<div class="search-wrap">
<div class="search-field">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" class="search-icon">
<circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" fill="none" />
<path d="M16.5 16.5L21 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
<input
type="search"
id="treeSearch"
class="search-input"
placeholder="Filter docs…"
autocomplete="off"
spellcheck="false"
aria-label="Filter documentation tree"
aria-describedby="searchHint"
/>
<kbd class="search-kbd" aria-hidden="true">/</kbd>
</div>
<p class="search-hint" id="searchHint">
<span id="resultCount">42</span> pages · use <kbd>↑</kbd> <kbd>↓</kbd> to move,
<kbd>Enter</kbd> to open
</p>
</div>
<div class="tree-scroll">
<ul class="tree" id="tree" role="tree" aria-label="Documentation pages"></ul>
<p class="tree-empty" id="treeEmpty" hidden>
No pages match <strong id="emptyTerm"></strong>.
</p>
</div>
<footer class="side-foot">
<button class="text-btn" id="expandAll" type="button">Expand all</button>
<span aria-hidden="true">·</span>
<button class="text-btn" id="collapseAll" type="button">Collapse all</button>
</footer>
</nav>
<!-- ───────────────────────── Article ───────────────────────── -->
<main class="content" id="article" tabindex="-1">
<nav class="crumbs" aria-label="Breadcrumb">
<a href="#">Docs</a><span aria-hidden="true">/</span>
<a href="#">Guides</a><span aria-hidden="true">/</span>
<span aria-current="page">Replication & failover</span>
</nav>
<article class="prose">
<span class="eyebrow">Guides · Operations</span>
<h1 id="top">Replication & failover</h1>
<p class="lede">
Aurora DB replicates every committed write to a quorum of storage nodes spread across
availability zones. This guide explains how the <strong>Helios</strong> consensus layer
elects a primary, how read replicas stay current, and what your application observes
during a failover event.
</p>
<div class="callout note">
<span class="callout-icon" aria-hidden="true">i</span>
<div>
<strong>Note —</strong> Failover behaviour changed in <code>v3.0</code>. If you are
running the legacy <code>Stratus</code> driver, see
<a href="#changelog">the migration notes</a> before upgrading a production cluster.
</div>
</div>
<h2 id="topology">Cluster topology</h2>
<p>
A cluster is a single <em>writer</em> endpoint backed by up to fifteen
<em>reader</em> endpoints. The writer owns the authoritative log; readers tail that log
and serve <code>SELECT</code> traffic with bounded staleness. The <strong>Helios</strong>
coordinator continuously health-checks every node and maintains the membership list that
clients resolve through DNS.
</p>
<table class="doc-table">
<thead>
<tr><th>Role</th><th>Endpoint</th><th>Lag target</th><th>Promotable</th></tr>
</thead>
<tbody>
<tr><td>Writer</td><td><code>cluster.aurora</code></td><td>—</td><td>—</td></tr>
<tr><td>Reader (sync)</td><td><code>ro-1.aurora</code></td><td>< 20 ms</td><td>Yes</td></tr>
<tr><td>Reader (async)</td><td><code>ro-2.aurora</code></td><td>< 500 ms</td><td>Yes</td></tr>
<tr><td>Archive</td><td><code>cold.aurora</code></td><td>minutes</td><td>No</td></tr>
</tbody>
</table>
<h2 id="quorum">Quorum writes</h2>
<p>
Each write is acknowledged only after it lands on a majority of storage segments. With
six segments across three zones, a write needs four acknowledgements to commit — so the
cluster survives the loss of an entire zone plus one additional node without data loss.
</p>
<pre><code><span class="c">// configure the write quorum on connect</span>
<span class="k">const</span> pool = aurora.<span class="fn">connect</span>({
cluster: <span class="s">"cluster.aurora"</span>,
quorum: { write: <span class="n">4</span>, read: <span class="n">3</span> },
failover: <span class="s">"auto"</span>, <span class="c">// promote a reader on writer loss</span>
});</code></pre>
<div class="callout warn">
<span class="callout-icon" aria-hidden="true">!</span>
<div>
<strong>Heads up —</strong> Setting <code>read: 1</code> trades consistency for
latency. Stale reads become possible during the few seconds a failover is in flight.
</div>
</div>
<h2 id="failover">Failover lifecycle</h2>
<p>
When Helios loses three consecutive heartbeats from the writer it opens an election.
The reader with the lowest replication lag wins, replays any outstanding log entries,
and re-points the writer DNS record. The whole sequence typically completes in under
<strong>thirty seconds</strong>.
</p>
<ol class="steps">
<li><strong>Detect</strong> — heartbeats miss their deadline; the writer is fenced.</li>
<li><strong>Elect</strong> — Helios selects the most caught-up promotable reader.</li>
<li><strong>Replay</strong> — the candidate applies the tail of the shared log.</li>
<li><strong>Promote</strong> — DNS flips; new connections resolve to the new writer.</li>
</ol>
<blockquote>
<p>
Design your application to retry idempotent writes. A failover surfaces to the client
as a single dropped connection — reconnect and the new writer answers.
</p>
<cite>— Aurora DB reliability handbook</cite>
</blockquote>
<h2 id="checklist">Operator checklist</h2>
<p>Before declaring a cluster production-ready, confirm each item below.</p>
<ul class="check">
<li>At least one <em>synchronous</em> reader in a second zone.</li>
<li>Client driver pinned to <code>v3.x</code> with <code>failover: "auto"</code>.</li>
<li>Connection retries with jittered back-off enabled.</li>
<li>Alerting on replication lag above the role's target.</li>
</ul>
<div class="callout tip">
<span class="callout-icon" aria-hidden="true">✓</span>
<div>
<strong>Tip —</strong> Run a planned failover during a maintenance window with
<code>aurora cluster failover --dry-run</code> to rehearse before you depend on it.
</div>
</div>
<hr />
<p class="page-meta" id="changelog">
Last reviewed for <strong>Aurora DB v3.2</strong> · Estimated read time 6 min ·
<a href="#top">Back to top ↑</a>
</p>
</article>
</main>
<!-- ───────────────────────── TOC rail ───────────────────────── -->
<aside class="toc" aria-label="On this page">
<p class="toc-title">On this page</p>
<ul class="toc-list">
<li><a href="#topology">Cluster topology</a></li>
<li><a href="#quorum">Quorum writes</a></li>
<li><a href="#failover">Failover lifecycle</a></li>
<li><a href="#checklist">Operator checklist</a></li>
</ul>
</aside>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Collapsible Doc Tree Nav
A documentation shell modeled on developer reference sites. A persistent left sidebar holds a nested
doc tree — Getting started, Guides, API reference, Reference — where parent sections expand and
collapse with a rotating chevron, child links sit on indented guide lines, and the current page keeps
an accented highlight with an aria-current marker. The centered article column uses a serif body for
long-form prose, callouts, tables, and code blocks, and a sticky right-rail table of contents tracks
your scroll position.
A sticky search box at the top of the sidebar live-filters the tree as you type: matching branches
auto-expand, the matched substring is highlighted with a <mark>, non-matching rows hide, and a
running result count updates. Press / anywhere to jump into the filter, use ↑
and ↓ to walk the visible rows, Enter to open the focused page, and
Esc to clear. Tree rows themselves are keyboard operable too, with left and right arrows
collapsing and expanding a focused section.
Everything is self-contained vanilla JS with no dependencies. Below the breakpoint the sidebar turns
into a slide-in drawer triggered by the header button, dimming the page with a scrim that closes on
click or Esc. A small toast() helper confirms navigation, the layout reflows down to about
360px, and focus styles plus reduced-motion handling keep it accessible.
Illustrative UI only — fictional articles, products, and data.