v2.6.0

<bmx-review-layer>

Comments pinned to the page: wrap any content - a page, a design, a dashboard, an image, a document - and reviewers click anything to start a thread on it. Pins are tied to elements, not screen positions, so they follow the content as it reflows, scrolls, zooms or opens on another screen size. Replies, mentions of the page's own people, edit and delete your own comments, resolve and reopen, and a list of every thread with filters and search.

15 properties · 10 events · 10 methods · 10 parts

Example

Spring launch landing page

Meet Aurora
Starter
£9 a month
Team
£29 a month
Enterprise
Talk to us

Press Comment (or Ctrl+Alt+M), then click anything. Type @ to mention someone.

Show markup
<bmx-review-layer id="ex-rl" panel>
  <div style="display: grid; gap: 14px; padding: 20px; border: 1px solid var(--bmx-border); border-radius: 12px">
    <h3 id="ex-rl-title" style="margin: 0">Spring launch landing page</h3>
    <div data-review-anchor="hero" style="display: grid; place-items: center; block-size: 140px; border-radius: 10px; background: linear-gradient(135deg, #1a73e8, #9334e6); color: #fff; font-size: 24px; font-weight: 700">Meet Aurora</div>
    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px">
      <article data-review-anchor="plan-starter" style="padding: 12px; border: 1px solid var(--bmx-border); border-radius: 10px"><strong>Starter</strong><br />£9 a month</article>
      <article data-review-anchor="plan-team" style="padding: 12px; border: 1px solid var(--bmx-border); border-radius: 10px"><strong>Team</strong><br />£29 a month</article>
      <article data-review-anchor="plan-enterprise" style="padding: 12px; border: 1px solid var(--bmx-border); border-radius: 10px"><strong>Enterprise</strong><br />Talk to us</article>
    </div>
    <p id="ex-rl-legal" style="margin: 0">Prices exclude VAT. Cancel any time.</p>
  </div>
</bmx-review-layer>
<p class="note" id="ex-rl-out" role="status" style="margin: 12px 0 0">Press Comment (or Ctrl+Alt+M), then click anything. Type @ to mention someone.</p>

<script type="module">
  await customElements.whenDefined('bmx-review-layer');
  const layer = document.getElementById('ex-rl');
  const out = document.getElementById('ex-rl-out');
  layer.people = [
    { id: 'ana', name: 'Ana Silva', color: '#7b1fa2' },
    { id: 'bo', name: 'Bo Chen', color: '#00796b' },
    { id: 'sam', name: 'Sam Reviewer', color: '#1a73e8' },
  ];
  layer.user = { id: 'sam', name: 'Sam Reviewer', color: '#1a73e8' };
  const ago = minutes => new Date(Date.now() - minutes * 60000).toISOString();
  layer.threads = [
    { id: 't1', anchor: { path: ['[data-review-anchor="hero"]'], x: 0.62, y: 0.4, label: 'Meet Aurora' }, status: 'open', createdAt: ago(300), comments: [
      { id: 'c1', author: 'ana', text: 'Can we try the darker gradient here? @Bo Chen', mentions: ['bo'], createdAt: ago(300) },
      { id: 'c2', author: 'bo', text: 'Yes - exporting it now.', createdAt: ago(120) },
    ] },
    { id: 't2', anchor: { path: ['[data-review-anchor="plan-team"]'], x: 0.8, y: 0.3, label: 'Team £29 a month' }, status: 'open', createdAt: ago(60), comments: [
      { id: 'c3', author: 'sam', text: 'Should this say "per seat"?', createdAt: ago(60) },
    ] },
    { id: 't3', anchor: { path: ['#ex-rl-legal'], x: 0.3, y: 0.5, label: 'Prices exclude VAT.' }, status: 'resolved', createdAt: ago(2000), resolvedAt: ago(1000), resolvedBy: 'ana', comments: [
      { id: 'c4', author: 'bo', text: 'Legal approved this wording.', createdAt: ago(2000) },
    ] },
  ];
  // The page keeps the threads: here they are only described, not saved.
  layer.addEventListener('bmxChange', e => (out.textContent = `${e.detail.reason}: ${e.detail.threads.length} threads (save them with the page's own data).`));
  layer.addEventListener('bmxMention', e => (out.textContent = `Notify ${e.detail.people.map(p => p.name).join(', ')}.`));
</script>
<bmx-review-layer id="review" user='{"id":"ana","name":"Ana Silva"}'>
  <main> ... the content being reviewed ... </main>
</bmx-review-layer>
<script>
  review.people = await api.people();
  review.threads = await api.loadReview(docId);
  review.addEventListener('bmxChange', e => api.saveReview(docId, e.detail.threads));
  review.addEventListener('bmxMention', e => api.notify(e.detail.people, e.detail.thread));
</script>

Nothing is stored or sent: threads in, bmxChange out, as plain JSON (exportJson, importJson). Every change can be refused by cancelling its event (bmxThreadCreate, bmxCommentAdd, bmxCommentEdit, bmxCommentDelete, bmxThreadStatus, bmxThreadDelete).

A page names what can be reviewed with data-review-anchor="name" on an element: pins made inside it stay with it even when its insides change. An element that shows a document page by page can mark each page this way, so a pin on a page stays on that page.

Keyboard: Ctrl+Alt+M (Command+Option+M on a Mac) comments on whatever has focus, or turns commenting on. Pins are one tab stop, arrows move between them, Enter opens one. In a comment box Ctrl+Enter sends, @ opens the people list (arrows, Enter or Tab to choose), Escape closes.

Properties

PropertyAttributeTypeDefaultDescription
active active boolean false Commenting is on: a click on the content starts a thread there. Mutable.
hidePins hide-pins boolean false Hide every pin (the list still works).
hideToolbar hide-toolbar boolean false Leave out the bar above the content.
hotkey hotkey string 'mod+alt+m' The keys that comment on what has focus. Empty for none.
locale locale string — The language for times ("5 minutes ago"). Default: the page's.
moderator moderator boolean false Let the user resolve, reopen and delete anyone's threads and comments, not only their own. Resolving and reopening are open to everyone regardless.
panel panel boolean false The list of every thread is showing. Mutable.
people people BmxReviewPerson[] | string [] The people who can be
readonly readonly boolean false Read the threads only: no new threads, replies or changes.
searchPeople property only BmxReviewPeopleSearch — Looks people up as the reader types an
showResolved show-resolved boolean false Show resolved threads' pins too. Mutable.
sort sort BmxReviewSort 'number' How the list is ordered.
strings strings Partial<BmxReviewLayerStrings> | string — Words to show instead of the English ones, as an object or JSON.
threads threads BmxReviewThread[] | string [] The threads, as an array, a { version, threads } document, or JSON. Mutable: every change writes it back.
user user BmxReviewPerson | string — Who is commenting: { id, name, avatar, color } or JSON. Without it the threads can be read but not added to.

Events

EventDetailDescription
bmxActiveChange { active: boolean; } Fired when commenting is turned on or off.
bmxChange BmxReviewChangeDetail Fired after any change, with every thread: save these.
bmxCommentAdd BmxReviewCommentDetail Fired before a reply is added. Cancel it to refuse.
bmxCommentDelete BmxReviewCommentDetail Fired before a comment is deleted. Cancel it to refuse.
bmxCommentEdit BmxReviewCommentDetail Fired before a comment is changed. Cancel it to refuse.
bmxMention BmxReviewMentionDetail Fired when a new or edited comment mentions people it did not before: notify them.
bmxThreadCreate BmxReviewThreadDetail Fired before a thread is started. Cancel it to refuse.
bmxThreadDelete BmxReviewThreadDetail Fired before a thread is deleted. Cancel it to refuse.
bmxThreadSelect { thread: BmxReviewThread | null; } Fired when a thread is opened or closed.
bmxThreadStatus BmxReviewThreadDetail & { status: BmxReviewStatus; } Fired before a thread is resolved or reopened. Cancel it to refuse.

Methods

MethodSignatureDescription
addThread addThread(target: Element | string, text: string, x?: number, y?: number) => Promise<BmxReviewThread | null> Start a thread on an element inside the layer (an element or a selector), at a point in it (fractions; default the middle), with its first comment. Resolves the thread, or null if it was refused.
closeThread closeThread() => Promise<void> Close the open thread's card.
deleteThread deleteThread(id: string) => Promise<boolean> Delete a thread. Resolves whether it was deleted.
exportJson exportJson() => Promise<string> Every thread as JSON ({ version: 1, threads }).
getThreads getThreads() => Promise<BmxReviewThread[]> Every thread.
importJson importJson(json: string | unknown, mode?: "replace" | "merge") => Promise<number> Load threads from JSON or an array: replace (default) or merge (by id; incoming wins). Resolves how many were read.
openThread openThread(id: string) => Promise<boolean> Open a thread's card, scrolling its element into view. Resolves whether it was found.
refreshPins refreshPins() => Promise<void> Re-measure where every pin goes (after the page changed in a way the layer could not see).
reopenThread reopenThread(id: string) => Promise<boolean> Reopen a thread. Resolves whether it changed.
resolveThread resolveThread(id: string) => Promise<boolean> Resolve a thread. Resolves whether it changed.

Slots

SlotDescription
(default) The content being reviewed.

CSS shadow parts

PartDescription
card The thread card next to a pin.
comment One comment in the card.
composer A comment box.
content The box around the content, which the pins sit over.
mentions The people list under a comment box.
panel The list of every thread.
pin A pin; also pin-resolved, pin-selected and pin-new.
pin-new
thread One thread in the list.
toolbar The bar above the content.

CSS custom properties

PropertyDescription
--bmx-review-layer-card-width The width of a thread card.
--bmx-review-layer-mention The background of a mention in a comment.
--bmx-review-layer-panel-width The width of the list of threads.
--bmx-review-layer-pin The colour of an open pin (a person's own colour wins).
--bmx-review-layer-pin-size The size of a pin.