v2.0.0

<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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

PartDescription
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

PropertyDescription
--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.