<bmx-property-grid>
Inspect and edit any object: its properties in two columns, name and value, grouped by category or A to Z, with a search, a description under the grid, and an editor for each kind of value - text, numbers with units, switches, lists, flags, dates and times, colours, nested objects and arrays, JSON, or an editor of the page's own.
10 properties · 3 events · 9 methods · 8 parts
Example
Three buttons in a toolbar. Choose one, or all three to edit them together.
Edit a value: the buttons follow.
Show markup
<div class="row" style="display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 16px; align-items: start">
<div style="display: grid; gap: 8px">
<p class="note" style="margin: 0">Three buttons in a toolbar. Choose one, or all three to edit them together.</p>
<div id="ex-pg-pick" role="radiogroup" aria-label="Edit" style="display: flex; gap: 8px; flex-wrap: wrap">
<label><input type="radio" name="ex-pg-pick" value="0" checked /> Save</label>
<label><input type="radio" name="ex-pg-pick" value="1" /> Cancel</label>
<label><input type="radio" name="ex-pg-pick" value="2" /> Help</label>
<label><input type="radio" name="ex-pg-pick" value="all" /> All three</label>
</div>
<div id="ex-pg-stage" style="display: flex; gap: 12px; flex-wrap: wrap; align-items: center; padding: 24px; border: 1px dashed var(--bmx-border); border-radius: 12px; min-block-size: 120px"></div>
<p class="note" id="ex-pg-out" role="status" style="margin: 0">Edit a value: the buttons follow.</p>
</div>
<bmx-property-grid id="ex-pg" label="Button properties" style="--bmx-property-grid-height: 34rem"></bmx-property-grid>
</div>
<script type="module">
await customElements.whenDefined('bmx-property-grid');
const grid = document.getElementById('ex-pg');
const stage = document.getElementById('ex-pg-stage');
const out = document.getElementById('ex-pg-out');
const buttons = [
{ text: 'Save', variant: 'solid', tone: 'primary', width: 120, disabled: false, tooltip: 'Save the document', radius: 8, colour: '#1a73e8', icon: { name: 'save', position: 'start' }, shortcuts: ['Ctrl+S'], style: ['bold'], released: '2026-10-08' },
{ text: 'Cancel', variant: 'outline', tone: 'neutral', width: 120, disabled: false, tooltip: 'Close without saving', radius: 8, colour: '#5f6368', icon: { name: 'x', position: 'start' }, shortcuts: ['Esc'], style: [], released: '2026-10-08' },
{ text: 'Help', variant: 'ghost', tone: 'neutral', width: 90, disabled: true, tooltip: '', radius: 8, colour: '#5f6368', icon: { name: 'help', position: 'end' }, shortcuts: ['F1'], style: ['italic'], released: '2026-09-01' },
];
grid.schema = [
{ name: 'text', category: 'Content', description: 'The caption on the button.', required: true, maxLength: 40 },
{ name: 'tooltip', category: 'Content', description: 'Shown on hover and read by screen readers as the description.' },
{ name: 'shortcuts', category: 'Content', description: 'Keys that press the button.', items: { type: 'text', pattern: '[A-Za-z0-9+]+' }, newItem: () => 'Ctrl+K' },
{ name: 'variant', category: 'Appearance', options: ['solid', 'soft', 'outline', 'ghost', 'link'], defaultValue: 'solid', description: 'How strongly the button is drawn.' },
{ name: 'tone', category: 'Appearance', options: [{ value: 'primary', label: 'Primary' }, { value: 'neutral', label: 'Neutral' }, { value: 'danger', label: 'Danger' }], defaultValue: 'primary' },
{ name: 'colour', category: 'Appearance', type: 'color', description: 'The accent colour.' },
{ name: 'style', category: 'Appearance', type: 'flags', options: ['bold', 'italic', 'underline'], description: 'Text styles.' },
{ name: 'radius', category: 'Appearance', type: 'range', min: 0, max: 24, unit: 'px', defaultValue: 8, description: 'Corner radius.' },
{ name: 'icon', category: 'Appearance', description: 'An icon beside the caption.', properties: [{ name: 'name', options: ['save', 'x', 'help', 'plus'] }, { name: 'position', options: ['start', 'end'], defaultValue: 'start' }] },
{ name: 'width', category: 'Layout', type: 'integer', min: 40, max: 400, step: 10, unit: 'px', defaultValue: 120, description: 'Width in pixels, 40 to 400.' },
{ name: 'disabled', category: 'Behaviour', defaultValue: false, description: 'A disabled button cannot be pressed.' },
{ name: 'released', category: 'Behaviour', type: 'date', readonly: true, description: 'When this button was added. Read only.' },
];
const draw = () => {
stage.replaceChildren(
...buttons.map(b => {
const el = document.createElement('bmx-button');
el.textContent = b.text;
el.variant = b.variant;
el.tone = b.tone;
el.disabled = b.disabled;
el.title = b.tooltip;
el.style.inlineSize = `${b.width}px`;
el.style.setProperty('--bmx-button-radius', `${b.radius}px`);
el.style.fontWeight = b.style.includes('bold') ? '700' : '';
el.style.fontStyle = b.style.includes('italic') ? 'italic' : '';
el.style.textDecoration = b.style.includes('underline') ? 'underline' : '';
return el;
}),
);
};
const pick = value => {
if (value === 'all') grid.objects = buttons;
else {
grid.objects = undefined;
grid.object = buttons[Number(value)];
}
};
document.getElementById('ex-pg-pick').addEventListener('change', e => pick(e.target.value));
grid.addEventListener('bmxPropertyChanged', e => {
out.textContent = `${e.detail.path} is now ${JSON.stringify(e.detail.value)}.`;
draw();
});
pick('0');
draw();
</script>
<bmx-property-grid id="props"></bmx-property-grid>
<script>
props.schema = [
{ name: 'text', category: 'Content', description: 'The caption.' },
{ name: 'width', category: 'Layout', type: 'integer', min: 0, unit: 'px', defaultValue: 100 },
{ name: 'variant', category: 'Appearance', options: ['solid', 'outline', 'ghost'] },
];
props.object = selectedButton; // or props.objects = [a, b, c]
props.addEventListener('bmxPropertyChanged', e => redraw(e.detail.path));
</script>
Without a schema the properties are read from the object itself. With
several objects, a value they share is shown and one they do not is
"(mixed)"; an edit sets it on every one. Edits go straight into the
objects (as a property grid does), after the descriptor's checks - required,
range, pattern, length, options, the page's own validate - and the
cancelable bmxPropertyChange. A value that differs from its defaultValue
is bold and can be reset. Ctrl+Z and Ctrl+Y undo and redo.
The grid is a treegrid: Up and Down move between rows, Right and Left open
and close categories, objects and arrays, Enter or F2 goes into the value's
editor (typing starts one too), Enter there commits, Escape cancels, and Tab
moves to the next property's editor. Space toggles a switch. The editor is
named by its property, an error is tied to it, and the description pane says
what the focused property is for.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
hideDescription |
hide-description |
boolean |
false |
Leave out the description pane. |
hideToolbar |
hide-toolbar |
boolean |
false |
Leave out the toolbar. |
label |
label |
string |
— | The grid's accessible name. Default: "Properties". |
object |
property only | unknown |
— | The object to edit. Ignored when objects is set. |
objects |
property only | unknown[] |
— | Several objects to edit together. |
readonly |
readonly |
boolean |
false |
No editing. |
schema |
schema |
BmxPropertyDescriptor[] | string |
[] |
The properties, as descriptors or JSON. Read from the object when empty. |
showAdvanced |
show-advanced |
boolean |
false |
Show properties marked advanced. |
strings |
strings |
Partial<BmxPropertyGridStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
view |
view |
'categorized' | 'alphabetical' |
'categorized' |
Grouped by category, or one list A to Z. Mutable: the toolbar switches it. |
Events
| Event | Detail | Description |
|---|---|---|
bmxPropertyChange |
BmxPropertyChangeDetail |
Fired before a value changes. Cancel it to refuse the change. |
bmxPropertyChanged |
BmxPropertyChangeDetail |
Fired after a value has changed in the objects (edits, reset, undo, redo). |
bmxPropertyFocus |
BmxPropertyFocusDetail |
Fired when another property is focused. |
Methods
| Method | Signature | Description |
|---|---|---|
collapseAll |
collapseAll() => Promise<void> |
Closes every category, object and array. |
expandAll |
expandAll() => Promise<void> |
Opens every category, object and array. |
focusProperty |
focusProperty(path: string) => Promise<boolean> |
Focuses a property's row by dotted path, opening what holds it. Resolves whether it was found. |
getValue |
getValue(path: string) => Promise<unknown> |
The value at a dotted path; undefined when the objects disagree. |
redo |
redo() => Promise<boolean> |
Redoes the last undone change. Resolves whether there was one. |
refresh |
refresh() => Promise<void> |
Re-reads the objects, after they were changed elsewhere. |
setValue |
setValue(path: string, value: unknown) => Promise<boolean> |
Sets a value at a dotted path, through the checks and events. Resolves whether it was taken. |
undo |
undo() => Promise<boolean> |
Undoes the last change. Resolves whether there was one. |
validate |
validate() => Promise<BmxPropertyProblem[]> |
Every value that breaks its descriptor. |
CSS shadow parts
| Part | Description |
|---|---|
category |
A category row. |
description |
The pane under the grid. |
editor |
The editor in a value cell. |
grid |
The rows. |
name |
A property's name. |
row |
A property row. |
toolbar |
The view switch and search. |
value |
A property's value cell. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-property-grid-category |
The background of a category row. |
--bmx-property-grid-height |
The grid's height; the rows scroll inside it. |
--bmx-property-grid-indent |
How far each nested level is indented. |
--bmx-property-grid-name-width |
The width of the name column. |
--bmx-property-grid-row-height |
The least height of a row. |