# Browser support

> Supported browsers and fallbacks.

## Supported browsers

Baseline support matches modern evergreen browsers:

| Browser          | Minimum version | Notes                      |
| ---------------- | --------------- | -------------------------- |
| Chrome / Edge    | 100             | Released April 2022        |
| Firefox          | 100             | Released May 2022          |
| Safari           | 15              | macOS Big Sur + / iOS 15 + |
| Opera            | 86              | Chromium-based             |
| Samsung Internet | 17              | Android                    |

Chromium-based browsers (Brave, Vivaldi, Arc, DuckDuckGo Browser)
work if their underlying Chromium version is ≥ 100.

## Explicitly not supported

- **Internet Explorer** (any version).
- **Legacy Edge** (pre-Chromium, EdgeHTML).
- **Opera Mini** (proxy-rendered, no JS).
- **UC Browser** (ancient WebKit fork).

If you need to support IE or similar, the widget won't render. Detect
and show a fallback contact form:

```js
const supportsWidget = 'attachShadow' in Element.prototype && 'fetch' in window && typeof ReadableStream === 'function';

if (!supportsWidget) {
  document.getElementById('legacy-contact').style.display = 'block';
}
```

---

## Features used

The widget relies on:

| API                            | Feature detection                              | Why                                                               |
| ------------------------------ | ---------------------------------------------- | ----------------------------------------------------------------- |
| Shadow DOM (`attachShadow`)    | `'attachShadow' in Element.prototype`          | Widget rendering isolation                                        |
| Custom Elements                | `typeof customElements`                        | `<web-ai-chat>` element                                           |
| Constructable Stylesheets      | `'adoptedStyleSheets' in ShadowRoot.prototype` | Efficient CSS adoption (falls back to `<style>`)                  |
| ES Modules                     | Native                                         | NPM + web-component sub-entry                                     |
| `fetch`                        | Native                                         | API calls                                                         |
| `ReadableStream`               | Native                                         | Streaming responses                                               |
| `TextDecoder` / `TextEncoder`  | Native                                         | SSE frame parsing                                                 |
| Async generators (`for await`) | Native                                         | Stream consumers                                                  |
| CSS custom properties          | `:root { --var: ... }`                         | Theming                                                           |
| CSS `contain: size`            | Supported everywhere we support                | Inline-mode layout containment                                    |
| `IntersectionObserver`         | Native                                         | Auto-scroll behaviour                                             |
| `matchMedia`                   | Native                                         | `prefers-color-scheme`, `prefers-reduced-motion`, touch detection |
| `MediaRecorder`                | Feature-detected                               | Voice input (server mode)                                         |
| Web Speech API                 | Feature-detected                               | Voice input (local mode)                                          |
| `navigator.vibrate`            | Feature-detected                               | Haptic feedback (Android)                                         |
| `AudioContext`                 | Feature-detected                               | Sound effects                                                     |

Features marked "Feature-detected" gracefully degrade when unavailable
— e.g., on iOS 17.3 and below, `navigator.vibrate` is missing, so
haptics silently no-op.

---

## Mobile specifics

### iOS Safari

- **Full support** on iOS 15+.
- **Haptics** — WebKit doesn't expose `navigator.vibrate`. We use a
  programmatic `<input type="checkbox" switch>` click on iOS 17.4+ to
  trigger a single haptic tick. Custom patterns flatten to one tick.
- **Voice (local)** — `SpeechRecognition` is available but quality
  varies. Recommend `features.voice: "server"` for production.
- **Keyboard avoidance** — when the on-screen keyboard appears, the
  panel auto-adjusts to stay visible.
- **Text-entry focus** — the composer, Help search, and form/HITL fields keep a
  minimum 16px computed font on touch devices so focusing them does not trigger
  iOS page zoom. Each uses its existing border as the single focus indicator.
- **Viewport units** — we use `dvh` where supported (iOS 16+) and
  fall back to `vh` below.

### Android Chrome

- **Full support**.
- **Haptics** — `navigator.vibrate` supports both single and pattern
  vibrations.
- **Voice (local)** — Web Speech works well.

### Samsung Internet + other Android browsers

- **Samsung Internet 17+** — Chromium-based, full support.
- **Firefox Android** — tested and working.
- **Brave / DuckDuckGo** — Chromium-based, work identically to Chrome.

---

## Accessibility

### Keyboard navigation

- **Tab** rotates through all interactive elements (launcher, header
  buttons, menu items, textarea, send, history rows).
- **Shift+Tab** reverses.
- **Enter / Space** on the launcher opens the panel.
- **Escape** closes the panel (or exits fullscreen in inline mode).
- **Arrow Up / Down** navigates within popover menus.
- **Home / End** jump to first / last menu item.
- **Enter** on a menu item selects.

### Screen readers

- Every icon-only button has an `aria-label`.
- Popover menus use `role="menu"` + `role="menuitem"` with proper
  `aria-haspopup` + `aria-expanded`.
- Switch-style actions carry `aria-pressed`.
- Active language / theme options use `aria-current="true"`.
- The panel is a `role="dialog"` with `aria-modal="false"`
  (non-blocking) and focus trap.

### `prefers-reduced-motion`

When the user has motion reduced:

- Launcher callout animations disabled.
- Panel open/close animations shortened.
- Haptic feedback disabled.
- Sound volume halved.

### `prefers-color-scheme`

The default theme mode is `"auto"` — follows the OS preference.
Users can override via the in-widget theme picker.

### Touch vs pointer

The widget detects touch capability via
`matchMedia("(hover: none) and (pointer: coarse)")` and enables
haptics only on touch devices. Desktop users with mice never get
vibration prompts.

---

## Feature-detection code

If you want to check for widget compatibility before installing:

```js
function helpaiSupported() {
  if (typeof window === 'undefined') return false;
  if (!('customElements' in window)) return false;
  if (!('attachShadow' in Element.prototype)) return false;
  if (!('fetch' in window)) return false;
  if (typeof ReadableStream !== 'function') return false;
  if (typeof Promise !== 'function') return false;
  return true;
}

if (helpaiSupported()) {
  // Load the widget
  const s = document.createElement('script');
  s.src = 'https://cdn.help.ai/sdk/js/elements/latest/elements.js';
  s.setAttribute('data-public-key', 'agent_pk_...');
  s.setAttribute('data-ai-agent-deployment-id', '01900000-0000-7000-8000-000000000000');
  s.defer = true;
  document.head.appendChild(s);
} else {
  // Show fallback
  document.getElementById('legacy-chat-fallback').style.display = 'block';
}
```

---

## Polyfills

We don't ship any — the feature set is chosen so that every supported
browser has native implementations.

If you need to support an older browser via your own polyfills
(`core-js`, `whatwg-fetch`, etc.), the widget should work, but we
don't test this path. File an issue if something breaks and we'll
look.

---

## Performance targets

On a mid-tier laptop (M1 MacBook Air, 2020):

| Metric                               | Target   | Measured (current) |
| ------------------------------------ | -------- | ------------------ |
| Bundle download (gzipped)            | < 110 KB | ~107 KB            |
| First byte to first bubble painted   | < 200ms  | ~150ms             |
| Time to interactive (first open)     | < 1s     | ~600ms             |
| Per-token stream render latency      | < 5ms    | ~2ms               |
| Panel close → FAB visible transition | < 200ms  | ~180ms             |

On a 2019 Android mid-range (Galaxy A30):

| Metric                             | Target  | Measured |
| ---------------------------------- | ------- | -------- |
| First byte to first bubble painted | < 500ms | ~350ms   |
| Time to interactive (first open)   | < 2s    | ~1.1s    |

We run Lighthouse audits on every release; the widget never regresses
the host page's performance score by more than 2 points.

---

## Degraded modes

### Offline

- If the bundle is cached, the widget renders.
- Attempting to send fires an `error` event. Listen to provide a
  fallback:
  ```ts
  init({
    onError: (err) => {
      if (!navigator.onLine) {
        alert('You appear to be offline. Please try again later.');
      }
    },
  });
  ```
- Cached chats remain viewable.
- When the connection returns, the widget continues working
  automatically.

### Slow networks

- The streaming pipeline is resilient to per-frame delays.
- If the stream gets stuck, the user sees a "thinking…" indicator.
- User can click Stop to abort and retry.

### No localStorage (private mode on some browsers)

- The widget falls back to a no-op storage adapter (writes succeed
  but don't persist).
- Chat history doesn't persist across reloads.
- Everything else works.

### Old browsers

- The widget shows nothing (custom elements / shadow DOM aren't
  available). Your page renders normally; the widget is just absent.
- Feature-detect if you want to show a fallback.

---

## Device form factors

Tested on:

- **Desktop** (1280×720 minimum — smaller works but cramped in
  floating mode).
- **Tablet** (iPad landscape/portrait, Android tablets).
- **Mobile** (360×640 minimum — iPhone SE, Android Pixel).
- **Foldable** (Galaxy Z Fold — adapts via `matchMedia`).

Responsive behavior:

- `initialSize: "auto"` expands the panel on wide viewports
  (`≥ autoSizeBreakpoint`, default 900px).
- Below the breakpoint, the panel defaults to a phone-friendly
  compact size.

---

## Known limitations

- **iOS Safari viewport units** — we use `dvh` when supported (iOS
  16+). On older iOS, use `100vh` fallback may over-expand by the
  browser chrome height.
- **Android haptics vary by device** — OEM customizations (Samsung,
  Xiaomi) sometimes override or weaken `navigator.vibrate`. Not
  much we can do.
- **Firefox streaming markdown** — Firefox's `streaming-markdown`
  runs ~5% slower than Chromium on long streams. Imperceptible unless
  streaming tens of thousands of tokens.
- **Dark reader extensions** — "Dark Reader" (the popular dark-mode
  browser extension) can invert the widget's colors even in dark
  mode. Workaround: users disable Dark Reader for your site.

---

## Reporting compatibility issues

Found a browser where the widget misbehaves? Please file an issue
with:

- Browser name + version.
- OS + version.
- Device type (desktop / mobile / tablet).
- Steps to reproduce.
- Screenshot or screen recording.
- Console output.

[github.com/help-ai/elements/issues](https://github.com/help-ai/elements/issues)
