Hinged Margin Note
A compact editorial annotation behaves like a paper fold: activating the narrow tab makes a tucked note hinge outward from its ruled edge. The interaction reveals one useful detail in place, preserving the element’s footprint and keeping the motion legible rather than decorative.
Interactive preview
What is Hinged Margin Note?
A compact editorial annotation behaves like a paper fold: activating the narrow tab makes a tucked note hinge outward from its ruled edge. The interaction reveals one useful detail in place, preserving the element’s footprint and keeping the motion legible rather than decorative.
How it’s built
The note begins as a quiet ruled fragment beside a slim vertical edge. Clicking the trigger expands a clipped detail below it while a perspective rotation settles the folded surface into place. The control remains a native button with an explicit expanded state, and the detail is removed from the accessibility tree while closed.
Ideal for
- Inline definitions beside editorial copy
- Compact documentation annotations
- Contextual notes in portfolio indexes
Accessibility
Uses a native button, visible keyboard focus, an aria-expanded state, an associated aria-controls target, and concise live content. Reduced-motion preferences are respected by shortening transitions without removing the state change.
Performance
Uses only CSS transforms, opacity, and grid interpolation with no assets, libraries, canvas, or network requests. The interaction is driven by one click listener and remains lightweight on touch devices.
Source code
Explore, copy and reuse the original HTML, CSS and JavaScript.
<main class="showcase" aria-label="Interactive component preview">
<div class="inner-wrap">
<section class="hinge-note" data-motion="on">
<div class="note-paper" aria-live="polite">
<p class="note-copy" data-ali-i18n="copy_2">A detail can unfold without taking over the page.</p>
</div>
<button class="hinge-trigger" type="button" aria-expanded="false" aria-controls="hinge-detail">
<span class="trigger-open" data-ali-i18n="copy_1">Open note</span>
<span class="trigger-close" data-ali-i18n="copy_3">Close note</span>
<span class="hinge-mark" aria-hidden="true">↗</span>
</button>
<div id="hinge-detail" class="hinge-detail" hidden>
<div class="detail-rule" aria-hidden="true"></div>
<p data-ali-i18n="copy_4">Useful context, kept close.</p>
</div>
</section>
</div>
</main>
:root {
--bg: #F1EDE6;
--surface: #EEEAE4;
--ink: #16131A;
--muted: #5E5765;
--accent: #62447D;
--accent-soft: #C4B6D3;
--signal: #DF284D;
--fold-depth: 18deg;
--fold-speed: 520ms;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-width: 320px;
background: var(--bg);
color: var(--ink);
font-family: Georgia, "Times New Roman", serif;
}
.showcase {
min-height: 100vh;
min-height: 640px;
display: grid;
place-items: center;
overflow: hidden;
background: var(--bg);
padding: clamp(28px, 8vw, 88px) clamp(18px, 7vw, 96px);
}
.inner-wrap {
width: min(100%, 560px);
display: grid;
place-items: center;
}
.hinge-note {
width: min(100%, 420px);
position: relative;
padding-left: clamp(24px, 6vw, 42px);
border-left: 1px solid var(--ink);
}
.hinge-note::before {
content: "01";
position: absolute;
left: 0;
top: -1.2em;
transform: translateX(-50%);
padding: 0 .35rem;
background: var(--bg);
color: var(--muted);
font: 600 .68rem/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: .12em;
}
.note-paper {
width: min(100%, 310px);
min-height: 44px;
display: grid;
align-items: center;
padding: 12px 16px;
background: var(--surface);
border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent);
border-left: 3px solid var(--accent);
box-shadow: 5px 5px 0 color-mix(in srgb, var(--accent-soft) 52%, transparent);
}
.note-copy {
margin: 0;
color: var(--muted);
font-size: clamp(.78rem, 1.7vw, .92rem);
line-height: 1.45;
}
.hinge-trigger {
position: relative;
z-index: 2;
display: inline-flex;
align-items: center;
gap: .7rem;
margin-top: 18px;
padding: 0;
border: 0;
background: transparent;
color: var(--ink);
cursor: pointer;
font: 600 clamp(1.02rem, 2.4vw, 1.3rem)/1.1 Georgia, "Times New Roman", serif;
text-align: left;
}
.hinge-trigger::after {
content: "";
position: absolute;
right: 1.55rem;
bottom: -7px;
left: 0;
height: 1px;
background: var(--accent);
transform-origin: left;
transition: transform var(--fold-speed) cubic-bezier(.2,.8,.2,1);
}
.hinge-trigger:hover::after,
.hinge-trigger:focus-visible::after { transform: scaleX(.72); }
.hinge-trigger:focus-visible {
outline: 2px solid var(--signal);
outline-offset: 5px;
}
.hinge-mark {
display: inline-grid;
place-items: center;
width: 1.55rem;
height: 1.55rem;
border: 1px solid var(--accent);
color: var(--accent);
font: 500 .9rem/1 ui-monospace, SFMono-Regular, Menlo, monospace;
transition: transform var(--fold-speed) cubic-bezier(.2,.8,.2,1), background var(--fold-speed) ease;
}
.hinge-note.is-open .hinge-mark {
transform: rotate(90deg);
background: var(--accent-soft);
}
.trigger-close { display: none; }
.hinge-note.is-open .trigger-open { display: none; }
.hinge-note.is-open .trigger-close { display: inline; }
.hinge-detail {
display: grid;
grid-template-rows: 0fr;
transform-origin: top left;
transform: perspective(500px) rotateX(var(--fold-depth));
opacity: 0;
transition: grid-template-rows var(--fold-speed) cubic-bezier(.2,.8,.2,1), transform var(--fold-speed) cubic-bezier(.2,.8,.2,1), opacity 220ms ease;
}
.hinge-detail > * { min-height: 0; overflow: hidden; }
.hinge-note.is-open .hinge-detail {
grid-template-rows: 1fr;
transform: perspective(500px) rotateX(0deg);
opacity: 1;
}
.detail-rule {
height: 1px;
margin-top: 18px;
background: var(--accent);
}
.hinge-detail p {
margin: 9px 0 0;
color: var(--muted);
font: .72rem/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: .04em;
}
@media (max-width: 430px) {
.showcase { min-height: 100vh; padding: 34px 22px; }
.hinge-note { padding-left: 24px; }
.note-paper { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: 1ms !important; }
}
(() => {
const root = document.querySelector('.hinge-note');
const trigger = root?.querySelector('.hinge-trigger');
const detail = root?.querySelector('#hinge-detail');
if (!root || !trigger || !detail) return;
trigger.addEventListener('click', () => {
const isOpen = root.classList.toggle('is-open');
trigger.setAttribute('aria-expanded', String(isOpen));
detail.hidden = !isOpen;
const openLabel = root.querySelector('.trigger-open');
const closeLabel = root.querySelector('.trigger-close');
if (window.AuroraLabI18n?.t) {
if (openLabel) openLabel.textContent = window.AuroraLabI18n.t('copy_1', 'Open note');
if (closeLabel) closeLabel.textContent = window.AuroraLabI18n.t('copy_3', 'Close note');
}
});
})();