0.21.0: sidebar text field renders its <input> twice with the same id (duplicate DOM ids, breaks label association)
Summary
With @puckeditor/[email protected] (React 19.2, Next 16.2, App Router), a text field in the right-hand fields sidebar renders its <input> as two real DOM nodes sharing one id (e.g. id="help-v1-1-SectionHeader_text_title"). Duplicate ids are invalid HTML and break label association for assistive tech; they also break strict-mode test locators.
How we found it
Playwright strict-mode violations list distinct DOM elements. Two independent locators hit the same 2-match violation on the same id:
page.getByLabel("title", { exact: true })→ "strict mode violation: resolved to 2 elements".- A plain id selector with no accessible-name matching at all,
page.locator('[id$="_text_title"]')→ the same 2-match violation on the same id.
(2) falsifies the first theory we had (dual accessible-name sources from DefaultField's title={label || name} attribute plus a wrapping <label>): an id selector cannot match one element twice, so there are two nodes. Filling either node updates the same value, so they appear to be wired to the same store key — most likely the visible sidebar field plus a hidden/portal copy.
Under 0.20.2 the same field resolved to exactly one node (FieldLabelInternal rendered <label>{title}<input/></label>).
Repro
- Config: a component with a single
textfield (title), default fields sidebar, PuckRender/Puckeditor in a Next 16 App Router page. - Select the component on the canvas so its fields render in the sidebar.
- In the browser console:
document.querySelectorAll('[id$="_text_title"]').length→ 2 (expected 1).document.querySelectorAll('[id]').length !== new Set([...document.querySelectorAll('[id]')].map(e => e.id)).sizealso shows the duplicate.
Versions
@puckeditor/core 0.21.0 (also re-checked the changelogs of 0.21.1–0.21.3, 0.22.0–0.22.4, 0.23.0 — nothing mentions duplicate field-input DOM/ids; we are running an empirical probe against 0.23.x and will update this issue with the result). React 19.2.0, Next 16.2.x, Chromium via Playwright.
Workaround we ship
page.locator('[id$="_text_title"]:visible').first() — filter to the visible copy, break the tie deterministically.
Expected
One <input> per field, one id per document.
Source: puckeditor/puck