S-button in <s-text-field> accessory slot is hidden via inline display: none when rendered by React

Environment

  • App Home / embedded admin app, React 18
  • Polaris web components loaded via (unversioned) + app-bridge.js

What I’m doing - the documented accessory slot:

<s-text-field label="Recipients" placeholder="email@example.com">
  <s-button slot="accessory" onClick={onAddEmail}>Add</s-button>
</s-text-field>

Expected: the button renders in the field’s accessory area (as in the TextField docs, and as itdoes in a plain-HTML CodePen with identical markup).

Actual: the button never appears. In dev tools it’s correctly assigned to the slot (shows the
slot badge, shadow root present), but the host element has an inline style=“display: none;”
that’s never cleared:

<s-text-field>
  <s-button slot="accessory" style="display: none;">   <!-- hidden -->
    #shadow-root (open)
      <button class="button size-base tone-auto variant-auto" type="button">Add</button>
  </s-button>
</s-text-field>

Identical markup works in plain HTML (CodePen) - only React rendering fails. It looks like the
field hides the slotted accessory initially and the reveal doesn’t run for React-managed
children.

Question: Is the accessory slot supported when the child is rendered by React, or is it
harvested/revealed only at element-upgrade time (which React’s reconciliation misses)?
Recommended pattern/workaround?

Hey @Luke - thanks for flagging this, and for the detailed repro context.

Just confirming the intended behaviour on our end: the accessory slot itself is supported for s-text-field, and using a button/clickable action there should be a valid pattern. That said, the inline style="display: none" being left on the slotted s-button does seem a bit weird to me for sure.

I’m going to look into the expected behaviour for this on our end here, especially around React-rendered children in that accessory slot, and I’ll loop back once I have more info.

Just to add here - I believe the click events for the nested button is also broken (in React App Home), but works in admin UI extensions.

Hi @Luke I’ve tried reproducing this in a few different environments including a react 18 playground but to no avail. Can you create a sandbox I can take a better look at?

Following up with more detail, since the ask was a reproduction.

I cannot reproduce it standalone. A plain page loading the same https://cdn.shopify.com/shopifycloud/polaris.js renders the accessory button correctly in every configuration I tried:

  • plain HTML, no framework
  • React 19, mounted on first paint
  • React 19, field mounted 1s later (so the element upgrades well after paint)
  • React 19, field rendered first and the accessory slotted in 1s later
  • React 19 in StrictMode (double mount)
  • React 19 in StrictMode with an error state on the field
  • all of the above with app-bridge.js also loaded

It still fails inside our embedded app, and inspecting the live DOM there gives a different picture to my original post. The button is not simply keeping a stale inline style. It is never assigned to the named slot:

const f = [...document.querySelectorAll("s-text-field")].find(el => el.querySelector('[slot="accessory"]'));
const b = f?.querySelector('[slot="accessory"]');
console.log({
  found: !!b,
  inlineStyle: b?.getAttribute("style"),
  computed: b && getComputedStyle(b).display,
  assigned: b?.assignedSlot?.name ?? null,
  polaris: customElements.get("s-text-field") ? "upgraded" : "NOT upgraded",
});
{
  "found": true,
  "inlineStyle": "display: none;",
  "computed": "none",
  "assigned": "",
  "polaris": "upgraded"
}

So the attribute is present (the [slot="accessory"] selector matches it), the element is upgraded, but assignedSlot.name is "" rather than "accessory". The button has been assigned to the default slot. The display: none looks like the component’s own pre-placement style, and it never clears because the button never lands in the accessory slot.

That would explain why a static sandbox never shows it: the question is not whether React clears a style, it is what determines whether a <slot name="accessory"> exists in that shadow root at the moment children are assigned.

Is there any state in which s-text-field renders its shadow content without the named accessory slot? Anything that would make it fall back to a default slot only, and is there something on our side that could trigger it?