Exclusivobeta

Marginalia <mv-marginalia>

Notas al margen adaptables para contenido largo (documentación, artículos, términos legales, changelogs, informes de investigación) en las que cada nota decide por sí misma si el margen puede acogerla. Envuelve el texto y escribe las notas en línea con <span data-note> en el punto exacto que comentan, como <aside data-note> después de un párrafo, o conserva tus notas al pie de Markdown (GFM, markdown-it, Pandoc), que se adoptan automáticamente. En un contenedor ancho, el texto mantiene su medida y un carril de margen ocupa el espacio sobrante (unidades de contenedor, sin breakpoint que mantener); cada nota se coloca en el carril alineada con su línea, las notas apiladas se empujan hacia abajo sin superponerse nunca, los bloques marcados con data-note-wide se esquivan, las notas largas se recortan con “More” y las que sobrepasan la última línea amplían el contenedor en lugar de desbordarlo. A medida que el carril se estrecha, cada nota se mide con el ancho que tendría: la que quedaría más larga que max-lines, o que las colisiones alejarían de su línea más de max-drift, sale del margen y se convierte en un botón numerado que se abre justo debajo de su línea (un float, así que la propia línea nunca se parte), mientras que las notas cortas se quedan en el margen. Al pasar el cursor o poner el foco sobre un marcador, una nota o su texto de anclaje, se resaltan los tres y se dibuja una fina línea guía de la línea a la nota. Los tipos (note, source, warning) llevan una etiqueta de texto y un estilo de filete propio, no solo un color. Sin JS, las notas se leen en su sitio (flotando en el margen cuando hay espacio), y al imprimir cada nota se convierte en una lista numerada de notas al pie después del texto.

CategoríaLayout
TipoWeb Component (<mv-marginalia>)
Estadobeta
KitNavegación para usuarios avanzados
Keywordsexclusive, culture, sidenotes, sidenote, marginalia, margin-notes, footnotes, annotations, citations, long-form, prose, article, documentation, typography, responsive, container-queries, print, markdown, collision-avoidance

When to use

  • Documentation or a blog post has asides, sources and caveats that should sit next to the line they discuss instead of at the bottom
  • Markdown footnotes should become sidenotes on wide screens without rewriting the content or the Markdown pipeline
  • Legal terms, policies or research reports need citations and warnings in the margin that still work on phones and on paper
  • A changelog or release note needs short context (migration hints, deprecations) beside each entry without bloating the main text

Avoid when

  • The extra text is a short hint or label for a control, not an aside in running prose → use Tooltip instead
  • The aside holds actions or form fields, or must float above the app UI rather than sit in the document flow → use Popover instead
  • The page is a dense app screen or a narrow column with no reading measure to spare; a margin rail only pays off in long-form text

Instalación

node scripts/add.mjs marginalia --out ./src/marvelous

Agente de IA con el servidor MCP de Marvelous UI: install_components({ slugs: ["marginalia"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Archivos copiados (dependencias incluidas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/marginalia/marginalia.js, components/marginalia/marginalia.css.

Uso

Inicio rápido, el marcado mínimo que funciona:

<mv-marginalia>
  <p>Rotate API keys every 90 days<span data-note>NIST SP 800-57 allows up to two years; shorter limits the damage of a leak.</span>, then revoke the old key once traffic has moved.</p>
</mv-marginalia>

Marcado de referencia: parte de él y personalízalo con atributos, data-* y variables CSS:

<div id="mg-demo" style="width:min(100%,64rem);margin-inline:auto">
  <style>
    #mg-demo { display:grid; gap:1rem }
    #mg-demo .mg-bar { display:flex; align-items:center; gap:.75rem 1.5rem; flex-wrap:wrap; padding:.75rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #mg-demo .mg-width { display:flex; align-items:center; gap:.75rem; flex:1 1 20rem; min-width:0 }
    #mg-demo .mg-width label { font-size:.8125rem; font-weight:500; white-space:nowrap }
    #mg-demo .mg-width input { flex:1; min-width:6rem }
    #mg-demo .mg-width output { min-width:3.75rem; color:var(--mv-fg-muted); font-size:.75rem; font-variant-numeric:tabular-nums; text-align:end }
    #mg-demo .mg-status { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap }
    #mg-demo .mg-stage { display:flex; justify-content:center; min-width:0 }
    #mg-demo .mg-doc { width:var(--mg-w,100%); max-width:100%; box-sizing:border-box; padding:2rem clamp(1rem,4vw,2.5rem) 2.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm); transition:width var(--mv-duration-normal) var(--mv-ease-out) }
    #mg-demo .mg-crumbs { margin:0 0 .5rem; color:var(--mv-fg-subtle); font-size:.75rem; letter-spacing:.02em }
    #mg-demo mv-marginalia { color:var(--mv-fg); font-size:.9375rem; line-height:1.7; --mv-marginalia-measure:39rem; --mv-marginalia-measure-min:26rem; --mv-marginalia-width:17rem }
    #mg-demo mv-marginalia h2 { margin:0 0 .375rem; font-size:1.5rem; line-height:1.25; letter-spacing:-.02em }
    #mg-demo mv-marginalia h3 { margin:1.75rem 0 .5rem; font-size:1.0625rem; letter-spacing:-.01em }
    #mg-demo mv-marginalia p { margin:0 0 1rem }
    #mg-demo mv-marginalia .mg-meta { margin:0 0 1.5rem; color:var(--mv-fg-muted); font-size:.8125rem }
    #mg-demo mv-marginalia code { padding:.05em .3em; border-radius:var(--mv-radius-xs); background:var(--mv-bg-muted); font:.86em var(--mv-font-mono) }
    #mg-demo mv-marginalia ol.mg-steps { margin:0 0 1rem; padding-inline-start:1.25rem }
    #mg-demo mv-marginalia ol.mg-steps li { margin-bottom:.375rem }
    #mg-demo mv-marginalia pre { margin:0 0 1.25rem; padding:.875rem 1rem; overflow:auto; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle); font:.8125rem/1.6 var(--mv-font-mono) }
    #mg-demo mv-marginalia pre code { padding:0; background:none; font:inherit }
    #mg-demo mv-marginalia section[data-footnotes] { margin-top:1.5rem; padding-top:.75rem; border-top:1px solid var(--mv-border); font-size:.8125rem; color:var(--mv-fg-muted) }
    #mg-demo .mg-hint { margin:0; color:var(--mv-fg-muted); font-size:.75rem; text-align:center }
  </style>

  <div class="mg-bar">
    <div class="mg-width">
      <label for="mg-range">Reading width</label>
      <input type="range" class="mv-slider" id="mg-range" min="340" max="1024" step="4" value="1024">
      <output id="mg-out" for="mg-range">Full</output>
    </div>
    <div class="mg-status">
      <mv-segmented id="mg-mode" label="Note placement" value="auto">
        <button value="auto">Auto</button>
        <button value="inline">Inline only</button>
      </mv-segmented>
      <span class="mv-badge" data-variant="secondary" data-shape="pill" id="mg-count">-</span>
    </div>
  </div>

  <div class="mg-stage">
    <article class="mg-doc" id="mg-doc" aria-labelledby="mg-title">
      <p class="mg-crumbs">Docs / Security / API keys</p>
      <mv-marginalia id="mg-notes">
        <h2 id="mg-title">Rotating API keys without downtime</h2>
        <p class="mg-meta">Updated September 18, 2026 · 6 min read</p>

        <p>Every API key in your workspace is a long-lived secret. Rotating keys on a schedule limits the damage a leaked key can do<span data-note data-note-kind="source">NIST SP 800-57 Part 1, Rev. 5, §5.3 recommends cryptoperiods of one to two years for authentication keys.</span>, and most compliance programs ask for it. Scope each key to one service so a rotation never touches more than it has to, following the principle of <span data-note-anchor>least privilege</span><span data-note>A key used by billing should not be able to read customer files. Scopes are set once, at creation.</span>.</p>

        <h3>How rotation works</h3>
        <p>Rotation is a two-key handover. You create a successor key, deploy it everywhere the current key is used, then revoke the old key once traffic has moved over. During the overlap both keys authenticate normally<span data-note>The overlap exists so you never have to deploy every service at the same instant. Both keys share the same scopes, rate limits and audit trail, and each request records which of the two signed it, so you can see migration progress in the key's Activity tab. Keys created before March 2024 use the legacy <code>nk_</code> prefix: they rotate the same way, but their successor gets the current <code>sk_live_</code> format, so check any code that validates keys by prefix. If you use infrastructure as code, create the successor in the same change that updates the secret reference, and keep the old key out of the plan until traffic has drained; Terraform and Pulumi providers both expose a <code>rotation_window</code> argument for this. When the overlap ends before you revoke, the old key keeps working: nothing expires on its own, which is why the audit log flags keys that have been in rotation for more than 30 days.</span>, so no request fails while you roll out.</p>

        <ol class="mg-steps">
          <li>Create the successor key from <strong>Settings → API keys → Rotate</strong>.</li>
          <li>Update the secret in every environment, starting with staging.</li>
          <li>Watch the old key's traffic drop to zero, then revoke it.</li>
        </ol>
        <aside data-note data-note-kind="warning">Revoking is immediate and cannot be undone. Requests still signed with the old key fail with <code>401 Unauthorized</code>.</aside>

        <pre data-note-wide><code>curl -X POST https://api.northwind.test/v2/keys/key_7Hq2/rotate \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d overlap=72h -d [email protected]</code></pre>

        <h3>Choosing an overlap window</h3>
        <p>A 72-hour overlap covers weekly deploy trains and most mobile release cycles<span data-note>Mobile apps are the slow path: a key baked into a build lives as long as users keep that version installed. Ship keys to mobile clients through your backend instead, so rotating one never waits on an app store review, and old app versions keep working without a forced update. If you must embed a key, give it read-only scopes and rotate it with every major release.</span>. Long-running batch jobs are the usual exception: a nightly export that starts with the old key keeps using it until it finishes<span data-note data-note-kind="source">Measured across 1,240 workspaces in Q2 2026: the median export job runs for 41 minutes, the 99th percentile for 7.5 hours.</span>, so revoke after the last run that began inside the window.</p>
        <p>Teams on the Enterprise plan can schedule rotations and get a chat or email reminder before each one<sup><a href="#mg-fn-1" id="mg-fnref-1" data-footnote-ref>1</a></sup>. Scheduled rotations use the same overlap window as manual ones.</p>

        <section data-footnotes class="footnotes" aria-label="Footnotes">
          <ol>
            <li id="mg-fn-1"><p>Reminders go to workspace owners and to anyone subscribed to the <em>security</em> channel, 7 days and 24 hours ahead. <a href="#mg-fnref-1" data-footnote-backref aria-label="Back to reference 1">↩</a></p></li>
          </ol>
        </section>
      </mv-marginalia>
    </article>
  </div>
  <p class="mg-hint">Narrow the page: long notes leave the margin first and become numbered toggles under their line · Hover a note or its number to link them</p>

  <script type="module">
    const notes = document.getElementById("mg-notes");
    const doc = document.getElementById("mg-doc");
    const range = document.getElementById("mg-range");
    const out = document.getElementById("mg-out");
    const count = document.getElementById("mg-count");
    const mode = document.getElementById("mg-mode");

    const fit = () => {
      const max = Math.floor(doc.parentElement.clientWidth);
      range.max = String(Math.max(360, max));
      const full = Number(range.value) >= Number(range.max);
      doc.style.setProperty("--mg-w", full ? "100%" : `${range.value}px`);
      out.textContent = full ? "Full" : `${range.value} px`;
    };
    range.addEventListener("input", fit);
    new ResizeObserver(fit).observe(doc.parentElement);
    fit();

    mode.addEventListener("mv-change", (e) => { notes.mode = e.detail.value; });

    const paint = () => {
      const list = notes.notes;
      if (!list.length) return;
      const m = list.filter((n) => n.mode === "margin").length;
      count.textContent = notes.layout === "margin" ? `${m} in the margin · ${list.length - m} inline` : `All ${list.length} inline`;
      count.dataset.variant = notes.layout === "margin" ? "info" : "secondary";
    };
    notes.addEventListener("mv-layout", paint);
    await customElements.whenDefined("mv-marginalia");
    requestAnimationFrame(paint);
  </script>
</div>

Referencia cultural

El último teorema de Fermat (nota al margen en la Arithmetica de Diofanto), Pierre de Fermat (1637, libro). Fermat escribió su comentario en el margen del libro, junto al problema que trataba, y anotó que el margen era demasiado estrecho para contener la demostración. En la interfaz, cada nota se coloca en el margen junto a su línea cuando el margen puede acogerla, y una nota para la que el margen es demasiado estrecho se pliega en un botón numerado que se abre justo debajo de su línea.

API

Attributes

NameTipoDefaultDescription
mode"auto" | "inline"auto"auto": notes go to the margin when the rail is at least --mv-marginalia-min wide, each one then checked for length and drift. "inline": every note is a numbered toggle, whatever the width (narrow sidebars, embeds).
side"end" | "start"endWhich side of the text the margin rail is on (logical: end = right in LTR, left in RTL).
max-linesnumber12A note whose full text would run longer than this many lines at the current rail width is too long for the margin and becomes an inline toggle. 0 = no limit (long notes are only clamped).
clampnumber6Margin notes longer than this many lines (plus one, so a single line is never hidden) are clamped with a fade and a “More” button that expands them in place, pushing the following notes down. 0 = never clamp.
max-driftnumber (px)360When collisions push a margin note further than this below its line, the reader would lose the link: the note becomes an inline toggle instead. 0 = no limit.
footnotes"adopt" | "off"adoptAdopts Markdown footnotes as sidenotes: references matching a[data-footnote-ref], a[role="doc-noteref"], a.footnote-ref or .footnote-ref > a pointing at an id inside the page. The item's content is cloned without its back-link, the original reference is hidden, and a footnote list whose every item was adopted is hidden on screen.
data-noteon a child elementMarks a note. An inline element (span, small…) is anchored to the exact point where it is written; a block (aside, div, p…) is anchored to the start of the preceding block and its marker is appended to that block's text.
data-note-kind"note" | "source" | "warning" | stringnoteOn a note: kind shown as a text label after the number (none for plain notes) and as a rule style (dashed for source, warning color for warning). Custom kinds are labeled with their capitalized name or strings[kind].
data-note-anchoron an element right before an inline noteMarks the exact words the note is about: dotted underline, highlighted with the note, and used in the note's accessible name. data-note-for="id" on the note points at any other anchor element.
data-note-markerstringOn a note: custom marker text ("*", "a", "†") instead of its number.
data-note-mode"inline"On a note: always an inline toggle, even when it would fit in the margin.
data-note-wideon a direct childFull-bleed block (code sample, figure, table) that runs under the rail; margin notes are pushed below it.
data-layout"margin" | "inline"Set by the component (styleable). data-settled is added after the first layout: position changes animate from then on.

Properties

NameTipoDescription
notesArray<{ id, index, marker, kind, mode, reason, open, clamped, expanded, note, ref, anchorText }>Snapshot of every note in reading order (read-only). mode: "margin" | "inline"; reason why a note is inline: "length" (too long for this rail), "crowded" (pushed past max-drift), "width" (no rail at all), "mode", "author" (data-note-mode).
layout"margin" | "inline" | nullCurrent layout of the container (read-only).
stringsPartial<Record<string, string>>Overrides for visible text and accessible names. Keys: note, source, warning (and any custom kind), more, less, name ("{kind} {n}"), nameOn ("{kind} {n}, on “{anchor}”"), printTitle. English defaults.
mode / side / maxLines / clamp / maxDrift / footnotesreflectedMirror the attributes.

Methods

NameDescription
refresh()Scans for notes added since (also done automatically by a MutationObserver) and lays everything out again synchronously.
reveal(target)Scrolls the note's line into view, then focuses the note in the margin (with a brief highlight) or opens it inline. target: 1-based index, note id, selector or element. Returns false when not found.
toggle(target, force?)Opens or closes an inline note, or expands / re-clamps a long margin note. Returns the new state.

Events

NameDescription
mv-layoutThe layout or the margin/inline split changed. detail: { layout, railWidth, margin, inline }.
mv-note-modeA note moved between the margin and inline. detail: { note, index, mode, reason }.
mv-note-toggleCancelable, before an inline note opens or closes, or a clamped note expands or collapses. detail: { note, index, open, via: "marker" | "more" | "escape" | "api" }.

Content structure

NameDescription
(content)Your long-form markup. Direct children form the text column (max-width set by the component); notes are restyled in place, never moved out of their paragraph, so the reading order stays intact.

CSS classes

NameDescription
mv-marginalia-noteAdded to each [data-note]: role="note", data-mode="margin | inline", data-long / data-clamped, data-open (inline), data-active while linked. Contains .mv-marginalia-text (.mv-marginalia-label with -num and -kind, then .mv-marginalia-body with your content) and .mv-marginalia-more.
mv-marginalia-refGenerated marker (<a href="#note">) placed where the note is anchored, with .mv-marginalia-ref-num inside; data-mode, role="button" + aria-expanded in inline mode, data-active while linked.
mv-marginalia-anchorAdded to the note's anchor element; data-active while linked.
mv-marginalia-leaderSVG overlay (aria-hidden) drawing the line from the text to the active margin note.
mv-marginalia-printFootnote list built on beforeprint, displayed only in print (-title, -list, -num, -kind).

CSS variables

NameDefaultDescription
--mv-marginalia-measure40remMaximum width of the text column.
--mv-marginalia-measure-min28remThe text column shrinks down to this width to make room for the rail before the rail itself narrows.
--mv-marginalia-width16remMaximum width of the margin rail.
--mv-marginalia-gap2.5remSpace between the text and the rail (where the leader is drawn).
--mv-marginalia-min8remNarrowest rail worth using; below it every note is inline and the text takes the full measure.
--mv-marginalia-spacing0.875remMinimum vertical space between stacked margin notes.
--mv-marginalia-accentvar(--mv-accent)Markers, numbers, active rule, leader and “More”.
--mv-marginalia-rulevar(--mv-border-strong)Resting rule on the note's edge and anchor underline.
--mv-marginalia-warningvar(--mv-warning)Rule color of warning notes.

Accessibility

Notes stay in their source position in the DOM, so screen readers meet them in reading order, and each one is role="note" with an accessible name that points back to what it discusses (“Source 2, on “least privilege””: the data-note-anchor words, else the words just before the marker, else the start of the paragraph for block notes). The generated marker is a real link (<a href="#note">, named “Note 3”) in margin mode: Enter moves focus to the note (tabindex=-1, focus-visible ring, brief highlight, scrolled into view only if needed) and Escape brings focus back to the marker; from a focused note, ArrowDown and ArrowUp walk to the next or previous note. In inline mode the same marker becomes role="button" with aria-expanded and aria-controls, toggled by Enter or Space; the note opens right after the line and Escape closes it and returns focus to the marker. Clamped notes have a real “More” / “Less” button with aria-expanded. Hover and focus highlight the marker, the note and the anchor together, and the kind is always written (Source, Warning) and drawn as a rule style, never conveyed by color alone. The leader and rail probe are aria-hidden with pointer-events: none. Without JS the notes are plain readable text in place. Reduced motion (prefers-reduced-motion or data-motion="reduce"): notes jump to their new position, no open animation, no leader draw-in. Forced colors: rules use CanvasText, the active note and open markers use Highlight, markers use LinkText, the leader is hidden. Print: notes are removed from the text and listed as numbered footnotes after it.

Esta página se tradujo con IA. Informar de un problema de traducción