# Agent guide - input, focus, and native HTML

## Decide who owns interaction

For an HTML site, native HTML should own links, form submission, text entry, focus outlines, and disabled state. Put decorative Pibbl pixels behind it with `aria-hidden="true"` and `pointer-events: none`. The [native button](/guides/native-button/) demonstrates this direction with pointer stirring, native click activation, fallback, and cleanup.

For a Pibbl-centered surface, use the runtime's event targets and focus policies. Read [focus and keyboard](/guides/focus-and-keyboard/) and the [event types](/reference/types/events/). Keyboard support on Canvas does not create native semantic elements or an accessibility tree. Provide meaningful HTML alternatives for information and controls where needed.

## Event and coordinate rules

Source traversal is paint order; coordinate targeting walks it in reverse and chooses at most one topmost logical target. Decorative paint should opt out. Do not dispatch a second native click from a Pibbl click handler unless the host explicitly requires that separate action. Native default behavior and Pibbl synthetic routing are different ownership domains.

Pibbl pointer coordinates are root-logical. Do not treat `event.x` and `event.y` as local to a transformed Group or a physics world. Convert explicitly at the system boundary. Pointer capture belongs to a pointer session: acquire it at the start, handle move, and release/reset on up, cancel, and teardown. Touch must not rely on hover.

## Focus and HTML allocation

`FocusManagement` and `KeyboardNavigation` are transparent policy components, not drawing or layout nodes. Each accepts one element after empty nodes are removed. Directional navigation requires focus management on the same root. Enrollment and navigation bounds come from logical targets; painted labels alone do not create targets.

Use [HtmlBox](/reference/components/html-box/) when Pibbl should allocate native HTML. Configure an empty HTML overlay immediately after the canvas in a shared positioned wrapper; their content rectangles must match. Mount returns an optional synchronous update function. The supplied AbortSignal is the native resource lifetime: use it for listeners and observer cleanup. Native events can write signals; mount/update lifecycle callbacks cannot.

HTML hosts remain above all Canvas paint, cannot participate in Canvas filters, and do not determine intrinsic Pibbl layout. Moving a keyed HtmlBox preserves nodes but is not a promise to reorder accessibility reading order. Shadow DOM/iframe focus internals and top-layer portals are outside the ordinary light-DOM guarantee.

## Complete example and ownership

The source below is the existing HTML Box lesson, including the overlay host setup, Canvas mounting, and teardown. [Open HTML Box](/playground/#/examples/composition/html-box). Compile its TSX using `@pibbl/core` and call the default mount on an attached canvas. Its returned controller is the owner's cleanup handle. No private helper or external asset is required.

Do not transplant that overlay setup into an HTML-owned button recipe: CSS sibling composition is sufficient there. Keep the two integration directions explicit in an agent's implementation plan.

## Verification

Test pointer, Enter, Space, Tab, Shift+Tab, disabled state, and focus restoration after removal. Confirm each action fires once. For a form, assert the original native submit/validation behavior still occurs. Test IME/editing in the native input rather than replacing it with Canvas text.

At two sizes and transforms, assert the native host and intended Pibbl allocation agree. Use a full-page screenshot to inspect HTML plus Canvas; exporting only Canvas excludes the HTML region. Remove and remount repeatedly, verifying the mount AbortSignal aborts and listeners no longer respond. Test decoration absent and reduced motion separately from full animation.

## Complete source

Host setup for this source: use a canvas with width 720 and height 480, compile with `jsx: "react-jsx"` and `jsxImportSource: "@pibbl/core"`, import its default `mount`, and call `const controller = mount(canvas)` after attaching the canvas. Call `controller.dispose()` before removing it.

### html-box.example.tsx

```tsx
import { pibbl, HtmlBox, Group, Clip, FocusManagement, Rectangle, Text, PathGeometry, useSignal, useConst, type PibblController, type HtmlMountCallback } from '@pibbl/core';

const mountEditor: HtmlMountCallback<string> = (element, title, signal) => {
  element.style.cssText += ';padding:22px;font:15px system-ui;color:#163a40;background:#e7f6ef';
  const label = document.createElement('label');
  label.style.cssText = 'display:grid;gap:10px;font-weight:650';
  const caption = document.createElement('span'); caption.textContent = title;
  const input = document.createElement('input'); input.placeholder = 'Type here, then move the box';
  input.style.cssText = 'width:100%;padding:10px;border:1px solid #87b2a3;border-radius:5px;font:inherit;box-sizing:border-box';
  label.append(caption, input);
  const button = document.createElement('button'); button.textContent = 'Native clicks: 0';
  button.style.cssText = 'margin-top:15px;padding:10px 16px;background:#164c45;color:white;border:0;border-radius:5px;font:inherit;cursor:pointer';
  let clicks = 0;
  button.addEventListener('click', () => { button.textContent = `Native clicks: ${++clicks}`; }, { signal });
  element.append(label, button);
  return next => { caption.textContent = next; };
};

function HtmlBoxLesson() {
  const angle = useSignal(0); const position = useSignal(0); const scale = useSignal(1); const after = useSignal(0);
  const clip = useConst(() => { const path = new PathGeometry(); path.roundRect(0, 0, 340, 190, 24); return path; });
  const controls = [
    { text: 'ROTATE', action: () => angle.update(value => value === 20 ? -20 : value + 10) },
    { text: 'MOVE', action: () => position.update(value => value === 60 ? 0 : value + 20) },
    { text: 'SCALE', action: () => scale.update(value => value === 1 ? 0.8 : 1) },
  ];
  return <FocusManagement><Group>
    <Rectangle style={{ width: 720, height: 480, fill: '#fffdf6' }} />
    <Text style={{ left: 32, top: 45, font: '700 28px system-ui', fill: '#163a40' }}>HTML, inside your canvas app</Text>
    <Text style={{ left: 32, top: 75, font: '14px system-ui', fill: '#42655f' }}>Native editing. Pibbl placement, transforms, clipping, and focus.</Text>
    {controls.map((control, index) => <Group key={control.text}>
      <Rectangle style={{ left: 32 + index * 135, top: 96, width: 120, height: 40, fill: '#164c45', cursor: 'pointer' }} onClick={control.action} />
      <Text pointerEvents="none" style={{ left: 92 + index * 135, top: 122, textAlign: 'center', font: '700 12px system-ui', fill: 'white' }}>{control.text}</Text>
    </Group>)}
    <Group style={{ translateX: 145 + position.get(), translateY: 174 }}>
      <Group style={{ rotationDegrees: angle.get(), rotationOrigin: [170, 95], scaleX: scale.get(), scaleY: scale.get() }}>
        <Clip style={{ d: clip }}>
          <HtmlBox mount={mountEditor} data="Your note" style={{ width: 340, height: 190 }} />
        </Clip>
      </Group>
    </Group>
    <Rectangle style={{ left: 32, top: 404, width: 190, height: 40, fill: '#164c45', cursor: 'pointer' }} onClick={() => after.update(n => n + 1)} />
    <Text pointerEvents="none" style={{ left: 127, top: 430, textAlign: 'center', font: '700 12px system-ui', fill: 'white' }}>{`CANVAS CLICKS: ${after.get()}`}</Text>
    <Text style={{ left: 242, top: 429, font: '13px system-ui', fill: '#42655f' }}>Tab between Canvas and HTML. Your text stays put.</Text>
  </Group></FocusManagement>;
}

export default function mount(canvas: HTMLCanvasElement): PibblController {
  const placeholder = document.createComment('HtmlBox lesson canvas');
  canvas.before(placeholder);
  const wrapper = document.createElement('div');
  wrapper.style.cssText = 'position:relative;width:100%;height:100%;isolation:isolate';
  const overlay = document.createElement('div');
  overlay.style.cssText = 'position:absolute;inset:0;z-index:1;pointer-events:none';
  const previousStyle = canvas.getAttribute('style');
  canvas.style.cssText = 'display:block;width:100%;height:100%;border:0;padding:0';
  const ownedStyle = canvas.getAttribute('style');
  placeholder.after(wrapper); wrapper.append(canvas, overlay);
  let controller: PibblController;
  const restore = () => {
    if (canvas.parentElement === wrapper) placeholder.after(canvas);
    if (canvas.getAttribute('style') === ownedStyle) {
      if (previousStyle === null) canvas.removeAttribute('style'); else canvas.setAttribute('style', previousStyle);
    }
    wrapper.remove(); placeholder.remove();
  };
  try { controller = pibbl(canvas, <HtmlBoxLesson />, { viewport: { width: 720, height: 480 }, htmlOverlay: overlay }); }
  catch (error) { restore(); throw error; }
  let disposed = false;
  return { dispose() { if (disposed) return; disposed = true; try { controller.dispose(); } finally { restore(); } } };
}

```

## Documentation version

Documentation built with @pibbl/core 0.0.2, revision 380919b. ALPHA — NOT FOR PRODUCTION USE.
