Building Accessible and High-Performance Frontend Components

Table of Contents

  1. Keyboard Navigation with tabindex
  2. Inline Editing with contenteditable
  3. Array Transforms & Hidden O(n²) Trap
  4. Dynamic Code Eval (Function vs eval)
  5. Case Study: Drag‑and‑Drop Text Selection
  6. Accessibility & Performance Pitfalls in Common Components
  7. Testing & Validation Toolbox
  8. Secure Sandbox Runner (Next.js + Cross‑Origin iframe)
  9. Local & Production Origin Setup
  10. Troubleshooting iframe Messaging & CSP
  11. Conclusion
  12. Further Reading

1 · Keyboard Navigation with tabindex

Browsers place only native controls (links, buttons, form fields) in the tab order.
Use tabindex sparingly:

<!-- semantic button — needs no tabindex -->
<button onclick="save()">Save</button>

<!-- custom widget behaves like a button -->
<div role="button" tabindex="0" onclick="save()">Save</div>

| Value | Meaning | |-------|---------| | 0 | Add element to natural tab order | | -1 | Programmatically focusable, not tabbable | | 1… | Avoid – breaks logical navigation |

Best practices

  • Prefer semantic elements
  • For custom widgets add role + tabindex="0"
  • Keep DOM order logical
  • Audit with Lighthouse / aXe

2 · Inline Editing with contenteditable

<div contenteditable="true" role="textbox" aria-label="Notes" tabindex="0">
  Rich text here…
</div>

Pros: zero‑effort rich text.
Cons: cross‑browser quirks, screen‑reader announcement issues, XSS risk.

Guidelines:

  • Use only for rich‑text; prefer <textarea> for plain text
  • Add role="textbox", use libraries (Slate/Quill) for production
  • Sanitize HTML with DOMPurify

3 · Array Transforms – Hidden O(n²) Trap

// ❌ quadratic
const map = items.reduce((a, x) => ({ ...a, [x.id]: x.val }), {});

Copying the accumulator each turn → O(n²) slowdown.

Linear alternatives

const map = Object.fromEntries(items.map(i => [i.id, i.val]));

4 · Dynamic Code Eval (Function vs eval)

Both execute arbitrary strings; both blocked by CSP unless 'unsafe-eval'.
Function() cannot see local scope but still accesses globalThis.

Safer alternatives: JSON.parse, templates, sandboxed interpreters, strict CSP.


5 · Case Study: Drag‑and‑Drop Text Selection

Read‑only contenteditable wrapper stops accidental drag when user selects text.

<div draggable="true" class="card">
  <div contenteditable="true" role="document" aria-readonly="true"
       tabindex="-1" spellcheck="false"
       onkeydown="return false" onpaste="return false">
    <RichTextComponent/>
  </div>
</div>

6 · Accessibility & Performance Pitfalls in Common Components

| Component | Pitfall | Fix | |-----------|---------|-----| | Forms | Placeholder as label | Use real <label> / aria-label | | Modals | Focus not trapped | Focus container, add aria-modal | | Tables | No <th>/<caption> | Add headers + caption | | Tooltips | title only | Real tooltip + aria-describedby | | Dropdowns | No keyboard nav | Up/Down/Esc, toggle aria-expanded | | DnD | No keyboard alt | Provide buttons + live‑region |


7 · Testing & Validation Toolbox

  • Lighthouse, aXe, WAVE
  • NVDA, VoiceOver, TalkBack
  • Chrome DevTools Performance
  • CI: aXe‑core / pa11y

8 · Secure Sandbox Runner (Next.js + Cross‑Origin iframe)

8.1 Architecture

Host page ──postMessage──▶ sandbox iframe (sandbox="allow-scripts")
             ◀──result────

Iframe lives on different origin (sandbox.yourapp.com) with CSP allowing 'unsafe-eval'.

8.2 Minimal sandbox (run.html)

<!doctype html>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self';
               script-src 'self' 'unsafe-eval';">
<script src="./run.js" defer></script>

run.js

self.onmessage = ({ data }) => {
  const { uid, code, input } = data;
  let result, error = false;
  try {
    // eslint-disable-next-line no-new-func
    const fn = new Function('input', `"use strict";\n${code}`);
    result = fn(input);
  } catch (e) {
    error = true;
    result = e instanceof Error ? e.message : String(e);
  }
  parent.postMessage({ uid, result, error }, '*');
};

8.3 Host page (client component)

'use client';
const iframeRef = useRef<HTMLIFrameElement|null>(null);

useEffect(() => {
  const ifr = document.createElement('iframe');
  ifr.sandbox.add('allow-scripts');          // opaque origin
  ifr.src = `${SANDBOX_ORIGIN}/run.html`;
  document.body.appendChild(ifr);
  iframeRef.current = ifr;
}, []);

function runUserCode(code: string, input: any) {
  const uid = crypto.randomUUID();
  iframeRef.current?.contentWindow?.postMessage({ uid, code, input }, '*'); // '*' needed
}

Use e.source === iframe.contentWindow to validate replies.


9 · Local & Production Origin Setup

9.1 Local Dev

  1. /etc/hosts
    127.0.0.1 sandbox.localhost
    
  2. Next dev: next dev -p 3000 --hostname 0.0.0.0
  3. Iframe URL: http://sandbox.localhost:3000/sandbox/run.html

9.2 Production on Vercel

| Project | Root Dir | Domain | |---------|----------|--------| | main‑app | / | yourapp.com | | sandbox  | /public/sandbox | sandbox.yourapp.com |

sandbox project vercel.json

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "default-src 'self'; script-src 'self' 'unsafe-eval';"
        }
      ]
    }
  ]
}

10 · Troubleshooting iframe Messaging & CSP

| Symptom | Cause | Fix | |---------|-------|-----| | onmessage never fires | Used specific origin but iframe has opaque origin (sandbox="allow-scripts") | Post with '*' or add allow-same-origin | | ERR_SSL_PROTOCOL_ERROR | Using https:// but local server is HTTP | Switch to http:// or create self‑signed HTTPS (mkcert) | | Refused to evaluate ... 'unsafe-eval' | Sandbox CSP missing 'unsafe-eval' | Add in sandbox CSP (not main app) | | No logs visible | DevTools context on top frame | Right‑click › Inspect inside iframe or open run.html in new tab |


11 · Conclusion

By combining solid accessibility patterns, performance‑first coding, and a hardened sandbox for untrusted code:

  • Apps stay inclusive (WCAG‑compliant)
  • Critical paths remain fast (no hidden O(n²))
  • Untrusted user code runs safely out‑of‑origin

12 · Further Reading

  • WAI‑ARIA Authoring Practices
  • WCAG 2.2
  • MDN: tabindex, contenteditable, CSP
  • DOMPurify
  • vm2 (Node) / SES (browser)