<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
£9 a month
£29 a month
Talk to us
Prices exclude VAT. Cancel any time.
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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
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
| Event | Detail | Description |
|---|---|---|
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
| Method | Signature | Description |
|---|---|---|
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
| Slot | Description |
|---|---|
(default) |
The content being reviewed. |
CSS shadow parts
| Part | Description |
|---|---|
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
| Property | Description |
|---|---|
--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. |