#1830·puck

0.21.0: sidebar text field renders its <input> twice with the same id (duplicate DOM ids, breaks label association)

Author: noobomancerCreated Sep 6, 2026Updated Sep 17, 2026
Labelsreadytype: bug 🐛

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:

  1. page.getByLabel("title", { exact: true }) → "strict mode violation: resolved to 2 elements".
  2. 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 text field (title), default fields sidebar, Puck Render/Puck editor 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"]').length2 (expected 1). document.querySelectorAll('[id]').length !== new Set([...document.querySelectorAll('[id]')].map(e => e.id)).size also 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.