The original work records include a plan selector that worked at the top but not at the bottom. Repeated button appearance did not ensure a shared connection and selection state. I extracted a pattern from that problem: several operating positions read one explicit material ID.

This is new code, not a reconstruction of the original customer slider. It connects declared material panels and controls and keeps their selected state consistent. The synthetic example lets you compare upper and lower controls with the visible material. The download includes the new source, two language demos, styling and isolated checks.

Keep one ID for the selected material

createSelection receives a material-ID list and initial ID and exposes current and select. A stable ID can preserve meaning when a label or control order changes. Re-selecting the same material reports no change; an unknown ID fails without changing the selection. The caller’s array is left unchanged.

IDs are unique nonempty strings. This implementation allows at most 200 IDs, each at most 120 characters. It does not infer material identity from button position or image content. The caller must declare the relationship between actual material and IDs. A very large catalog may require another navigation arrangement rather than simply adding more controls.

Connect repeated controls to one panel state

attachSharedPicker reads data-panel-id panels and data-pick-id buttons within one exclusive root. Each panel has a unique ID, while several control groups may repeat those IDs. Selection shows the chosen panel and updates every pressed flag from the same state. Every panel needs a matching control, and unknown control IDs are rejected before connection.

Controls are native HTML buttons. Enter and Space use browser button behavior; the module does not claim a complete arrow-key ARIA tab interaction. The narrow example hides the lower group without deleting material or its selected ID. Control count and material state are treated as different responsibilities.

Inspect selection state using new independent code and synthetic material.

Source and package contents

The JavaScript below is the same shared_picker.mjs included in the ZIP. It separates the state core from its DOM connection and includes no external slider, customer assets, contact settings or server call. A changed selection emits shared-picker:change with detail.id. Connecting that event to a real website feature is a separate decision and test.

The six package files contain the source, CSS, Korean and English synthetic demos, Node checks and guide. Run node checks.mjs for the included checks. The guide explains serving the folder over local HTTP to open the ES-module demos. A file double-click and an environment that serves a JavaScript module are different invocation paths.

Explore the implementation

New independent JavaScript · one material ID, several controls

// New independent selection state and DOM connector. No storage or network.
const mounted = new WeakMap();

export function createSelection(ids, initialId = ids?.[0]) {
  if (!Array.isArray(ids) || !ids.length || ids.length > 200 ||
      ids.some(id => typeof id !== 'string' || !id.trim() || id.length > 120) ||
      new Set(ids).size !== ids.length) throw new TypeError('Invalid material IDs');
  const known = new Set(ids);
  if (!known.has(initialId)) throw new TypeError('Unknown initial material');
  let current = initialId;
  return Object.freeze({
    get current() { return current; },
    select(id) {
      if (!known.has(id)) throw new TypeError('Unknown material');
      const changed = current !== id;
      current = id;
      return changed;
    }
  });
}

export function attachSharedPicker(root, {initialId} = {}) {
  if (!root?.querySelectorAll || !root.addEventListener) throw new TypeError('Root required');
  const panels = [...root.querySelectorAll('[data-panel-id]')];
  const buttons = [...root.querySelectorAll('button[data-pick-id]')];
  const ids = panels.map(panel => panel.dataset.panelId);
  const model = createSelection(ids, initialId ?? ids[0]);
  const known = new Set(ids);
  if (!buttons.length || buttons.some(button => !known.has(button.dataset.pickId)) ||
      ids.some(id => !buttons.some(button => button.dataset.pickId === id))) {
    throw new TypeError('Controls and material IDs disagree');
  }
  // Invalid new markup/initial selection leaves an existing connector intact.
  mounted.get(root)?.destroy();
  const priorPanels = panels.map(panel => panel.hidden);
  const priorPressed = buttons.map(button => button.getAttribute('aria-pressed'));
  const ownedButtons = new Set(buttons);
  let active = true;
  const render = () => {
    panels.forEach(panel => { panel.hidden = panel.dataset.panelId !== model.current; });
    buttons.forEach(button => button.setAttribute('aria-pressed', String(button.dataset.pickId === model.current)));
  };
  const select = id => {
    if (!active) throw new Error('Picker is detached');
    const changed = model.select(id);
    render();
    if (changed) root.dispatchEvent(new root.ownerDocument.defaultView.CustomEvent(
      'shared-picker:change', {bubbles: true, detail: {id: model.current}}
    ));
    return changed;
  };
  const click = event => {
    const button = event.target.closest?.('button[data-pick-id]');
    if (ownedButtons.has(button) && !button.disabled) select(button.dataset.pickId);
  };
  const api = Object.freeze({
    get selectedId() { return model.current; }, select,
    destroy() {
      if (!active) return;
      active = false; root.removeEventListener('click', click);
      panels.forEach((panel, i) => { panel.hidden = priorPanels[i]; });
      buttons.forEach((button, i) => {
        if (priorPressed[i] === null) button.removeAttribute('aria-pressed');
        else button.setAttribute('aria-pressed', priorPressed[i]);
      });
      if (mounted.get(root) === api) mounted.delete(root);
    }
  });
  root.addEventListener('click', click); render(); mounted.set(root, api);
  return api;
}
Source, CSS, two demos, 22 checks and guide ZIP

Include remount and cleanup in the contract

A valid connection on the same root removes the old click listener. To preserve the selected material, pass the previous selectedId as initialId, as the example does. Omitting it starts with the first material. After replacing panel or control nodes, inspect the new declarations and remount. The module does not automatically observe asynchronous DOM changes.

destroy removes its listener and restores the original hidden and aria-pressed values. A detached object rejects later selection calls. An invalid new configuration is rejected before removing a working connector. This makes the end of the responsibility explicit when an application redraws its interface, rather than silently accumulating handlers or breaking an existing feature.

Keep synthetic checks separate from actual application checks

The twenty-two isolated checks cover state changes, unknown IDs, duplicates, repeated-control synchronization, change events, disabled controls, remount and attribute restoration using a synthetic DOM fixture. They test the bounded new contract, not an original customer site, real plan material or booking submission. The visible example contains no factual project material either.

Application needs inspection of real keyboard behavior, layout, focus inside a panel that becomes hidden and existing event connections. An external slider is not connected merely by attaching this module. It needs an adapter matching material IDs to that program’s actual state. I have scoped reuse to the consistency of one selection rather than promise an entire website integration.

What another website can adapt

Material IDs, panel contents, button labels and design can be adapted to the site. The narrow-screen hiding threshold is a demo starting point. Important information must not exist only in the hidden group, and necessary material and controls need inspection in the actual visitor flow. Independent selections should not be merged merely because their buttons look similar.

This implementation neither persists values nor sends them elsewhere. IDs remain unchanged in the emitted event, so it is not a privacy scrubber. Use generic identifiers in public examples and inspect the conditions for real customer material separately. The purpose is a small function a reader can attach, inspect and adapt while keeping its connection boundaries understandable.