---
title: "Web compatibility"
description: "The web platform features that work differently in Lilo than in other browsers, and the known limitations to keep in mind when a site misbehaves."
sidebar:
  order: 6
---

Lilo renders every page with WebKit, the engine of Safari. This page lists the web features where Lilo differs, how Lilo handles each, and the limitations of WebKit and macOS that Lilo cannot work around. When a site misbehaves, check here first, then see [Troubleshooting](/guides/troubleshooting).

The text below comes from the project's compatibility tracker, [COMPATIBILITY.md](https://github.com/Alexays/Lilo/blob/main/COMPATIBILITY.md). Extension APIs are covered in [Extension compatibility](/reference/extension-compatibility).

## Web APIs [#web-apis]

`navigator.serial`, `window.EyeDropper` and `window.documentPictureInPicture` exist only in secure
contexts, as in Chrome, but Safari has none of them: a page testing for them can tell a Lilo tab from
Safari, whose user agent Lilo shows.

### Web Serial API (`navigator.serial`) [#web-serial-api-navigatorserial]
- **Status:** Implemented (`Lilo/Engine/SerialBridge.swift`)
- **WebKit support:** None - Chrome/Edge only; Lilo polyfills it
- **How:** a page-world polyfill for secure top-level documents (`requestPort` with USB vendor and
  product filters, `getPorts`, `open` with baud rate, data bits, stop bits, parity and hardware flow
  control, `readable`/`writable` streams, `setSignals`, `getSignals`, `forget`) over a native bridge
  to the serial devices IOKit lists. `requestPort` shows a native picker; a port reaches only the
  origin the user picked it for, for the session. A reloaded or closed page releases its ports.
- **Limits:** no `connect` event when a device is plugged in (`disconnect` fires when it goes);
  cross-origin iframes get no `navigator.serial`; reads are not flow-controlled by the page.

### EyeDropper API (`window.EyeDropper`) [#eyedropper-api-windoweyedropper]

- **Status:** Implemented (`Lilo/Engine/EyeDropperBridge.swift`)
- **WebKit support:** None - Chrome/Edge only; Lilo polyfills it in secure contexts
- **How:** `open()` shows the system color sampler (the loupe of the macOS color panel) and resolves
  with `{ sRGBHex }`; Escape rejects with `AbortError`.

### File System Access API (`showOpenFilePicker`, `showSaveFilePicker`, `showDirectoryPicker`) [#file-system-access-api-showopenfilepicker-showsavefilepicker-showdirectorypicker]

- **Status:** Not implemented, on purpose for now
- **WebKit support:** None - Chrome/Edge only (WebKit ships only the origin-private file system)
- **Impact:** apps feature-detect it and fall back to file inputs and downloads, which work. A partial
  polyfill would switch those apps onto a path with missing pieces (handles that do not survive
  IndexedDB, directory writes), which is worse than the fallback.

### Protocol handlers (`navigator.registerProtocolHandler`) [#protocol-handlers-navigatorregisterprotocolhandler]

- **Status:** Not implemented, on purpose
- **WebKit support:** None on macOS - `'registerProtocolHandler' in navigator` is false in a WKWebView
  (checked on macOS 27); Chrome lets a site handle a scheme, such as Gmail for `mailto:` links
- **Impact:** sites feature-detect it and skip the offer. A `mailto:` link the user follows opens the
  app macOS assigns to the scheme at once; any other non-web link opens it after Lilo asks. Pages see
  Lilo as Safari, which lacks the API too: adding it would tell Lilo apart from Safari, and a
  page-visible trace flags Lilo as a bot.

### Service worker notification clicks [#service-worker-notification-clicks]

- **Status:** Partial - a click runs the worker's `notificationclick`, and `client.focus()` brings back a page it has open
- **Impact:** a notification clicked after Lilo relaunches reaches no worker: the click only brings Lilo forward.
  `clients.openWindow()` opens its tab in a Space of the worker's profile, the one on screen when it has that profile.
  A page's own notifications (`new Notification`) bring their tab back as in Chrome.

### Passkeys / WebAuthn [#passkeys--webauthn]

- **Status:** needs the `com.apple.developer.web-browser.public-key-credential` entitlement, which
  Apple grants per app ID. Without it, WebKit's `getCanCurrentProcessAccessPasskeysForRelyingParty`
  check fails: `PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()` is false and
  `navigator.credentials.create()` / `.get()` reject at once with `NotAllowedError` (checked on
  macOS 27). Apple has not granted it yet, so published releases do not carry it: WebKit's own WebAuthn does not
  work in them, passkeys and USB/NFC security keys alike. A request WebKit handles fails at once with `NotAllowedError`,
  with no prompt, and reads like a user cancel; an account whose only second factor is a hardware
  key cannot sign in. A password manager extension that answers passkey requests itself, through its hook on
  `navigator.credentials`, does not go through WebKit's check, and Lilo keeps that hook in place (`PageWorld`);
  whether a given manager's passkeys work in Lilo was not tested. Once granted, release builds carry it through a provisioning profile, see
  [docs/RELEASING.md](https://github.com/Alexays/Lilo/blob/main/docs/RELEASING.md#browser-passkey-entitlement); ad-hoc and Debug builds never do.
- **Not pursued:** A custom `ASAuthorizationPlatformPublicKeyCredentialProvider` integration would
  only duplicate WebKit. `com.apple.developer.associated-domains` (`webcredentials:`) governs the
  app's *own* domains and cannot scope arbitrary websites - it is not how a browser hosts WebAuthn.

### DRM - FairPlay (works) vs Widevine/PlayReady (impossible) [#drm---fairplay-works-vs-widevineplayready-impossible]

- **FairPlay Streaming (`com.apple.fps`, `1_0`, `2_0`, `3_0`): supported, no configuration needed.**
  Measured in a WKWebView configured like Lilo's tabs: `requestMediaKeySystemAccess` is granted for
  `cenc`, `sinf` and `skd` init data with H.264 and 10-bit HEVC, and `createMediaKeys` works. The
  legacy `window.WebKitMediaKeys` is also present. ClearKey is granted.
- **No persistent-license sessions:** offline downloads in streaming web apps are not available.
- **Widevine / PlayReady: rejected (`NotSupportedError`), and not fixable.** WebKit on Apple
  platforms implements EME only against FairPlay and exposes no API to register another CDM;
  `libwidevinecdm` plugs into Chromium's CDM host only. Getting Widevine would mean embedding a
  second engine for DRM origins, a deliberate non-goal.
- **Codecs (MSE and ManagedMediaSource):** H.264, HEVC Main/Main10, Dolby Vision, VP9 profiles 0
  and 2 (10-bit), AV1, Opus and Vorbis in WebM, AAC, E-AC-3. 4K HDR decoding is smooth.
- **User agent:** a plain Safari identity (no `Lilo` token), so services serve their Safari/FairPlay
  variant (see [User agent and the Chrome Web Store](/reference/extension-compatibility#user-agent-and-the-chrome-web-store)).
- **Streaming services** (from their public support pages for Safari, not yet hand-tested in Lilo):
  - *Expected to work:* Netflix (4K unverified), Prime Video, Disney+, Max, Apple TV+, Spotify web
    player, Apple Music web, Crunchyroll, YouTube (4K HDR), YouTube Music, Twitch.
  - *Reduced quality:* Canal+ (720p reported in Safari), Tidal (no lossless, which needs Widevine).
  - *Unknown:* Deezer, YouTube purchased and rented movies.

### Picture-in-Picture - `<video>` PiP and Document PiP (Lilo shim) [#picture-in-picture---video-pip-and-document-pip-lilo-shim]

- **Single-`<video>` Picture-in-Picture: supported.** WebKit's element PiP works
  (`video.webkitSetPresentationMode('picture-in-picture')` / `requestPictureInPicture()`), and Lilo
  auto-enters it when a tab with active media is backgrounded (`PiPController`). This pops *one* video
  out into the system PiP window (no page controls) - fine for YouTube/Netflix-style single-stream PiP.
- **Document Picture-in-Picture (`window.documentPictureInPicture`): supported via a Lilo shim.**
  WebKit ships no implementation. A page-world module (`DocumentPiPBridge`) defines the API in
  secure top-level documents of regular tabs; `requestWindow` makes the tab's next `window.open` a
  same-origin popup that `DocumentPiPController` hosts: docked at its own size in the sidebar while
  Lilo is in front and its tab is not shown, floating otherwise (always on top, every Space, out of
  screen sharing, non-activating, snapped to a corner remembered per screen, back and close buttons
  on hover). The window closes when its page navigates or its tab closes. A call tab whose page
  registers the `enterpictureinpicture` Media Session action gets its PiP window when the tab is
  left (closed again on coming back); with no window within 1.5 s, the whole page goes to the dock or
  floating panel as before. Agent tabs get a `NotAllowedError`.
- **Google Meet:** Meet offers its Document PiP only to Chrome, read from `navigator.userAgent` (it
  never reads `userAgentData`), and fixes at load the identity its WebRTC stack follows. Lilo shows a
  Chrome user agent on `meet.google.com` only in the microtask after a read of
  `documentPictureInPicture`: Meet's PiP checks pass, its call stays on Safari's media path. A full
  Chrome identity breaks camera and microphone (Chromium-only `createEncodedStreams` and audio
  `MediaStreamTrackGenerator`). `defaults write <bundle id> LiloMeetDocumentPiPDisabled -bool true`
  turns the Meet part off (new tabs).

### Screen sharing - tab capture (Lilo frame ferry) [#screen-sharing---tab-capture-lilo-frame-ferry]

- **Tab sharing is supported via a Lilo shim.** WebKit's capture pipeline only enumerates screens
  and OS windows (tab capture is a Chromium-only `getDisplayMedia` surface), so Lilo intercepts
  `getDisplayMedia` in the page world: a chrome picker lists the open tabs, and picking one
  returns a real `MediaStream` built page-side from a canvas fed by a native frame ferry
  (`TabShareSessionController`: 30 fps snapshots of the shared tab's engine, H.264-encoded in
  hardware and decoded page-side with WebCodecs; identical frames are skipped, so static content
  costs almost nothing). A backgrounded
  shared tab stays attached under the active view at full content size (the inspector-retention
  pattern), so it keeps its desktop layout and keeps rendering; the sidebar capture indicator
  marks it as shared.
- **Caveats vs Chrome:** the ferried stream is video-only (no tab audio: WebKit exposes no
  per-page audio tap) and capped at 30 fps / 1920 pixels wide.
- **Picking "share a screen or window instead"** runs WebKit's own `getDisplayMedia` and its
  system picker (the pick itself is the consent; no Screen Recording permission), invoked by
  native with a fresh user gesture since the async tab picker has left the page's one.
- **Extensions** (`chrome.tabCapture`, `chrome.desktopCapture`) ride the same ferry (see
  tabCapture and desktopCapture under [API differences](/reference/extension-compatibility#api-differences)). A desktop pick uses the
  system `SCContentSharingPicker` directly, and an `SCStream` feeds the ferry while the stream lives.
- **Note:** utilities that keep a transparent full-screen overlay window (LanguageTool for
  Desktop, some capture/window-manager tools) swallow the system window picker's hit-testing:
  the picker opens but no window can be selected, in every browser including Safari. Quit the
  overlay app while sharing.

### Idle tab freezing [#idle-tab-freezing]

- **Spared as in Chrome's freezing policy:** tabs playing or capturing media, loading, allowed to notify, holding a
  connected `RTCPeerConnection` (data channels alone included) or a Web Lock.
- **Caveat:** WebKit reports neither peers nor locks to the app, so Lilo tracks peers in the top document's page world
  and asks the top document for its locks when the tab is due to freeze: a peer or a lock held by a frame or a worker
  does not keep its tab from freezing.

## Known Limitations (WebKit/macOS level - no fix possible in Lilo) [#known-limitations-webkitmacos-level---no-fix-possible-in-lilo]

### iCloud Passwords extension [#icloud-passwords-extension]

- **Symptom:** The "iCloud Passwords" Chrome extension fails to connect to its native helper in Lilo.
- **Root cause:** The native-messaging host `com.apple.passwordmanager`
  (`/System/Cryptexes/App/System/Library/CoreServices/PasswordManagerBrowserExtensionHelper.app`)
  carries **parent launch constraints** in its code signature, independent of the manifest's
  `allowed_origins`. Its parent must either match a fixed list of known browsers' signing
  identifier and team identifier pairs, or hold the
  `com.apple.developer.web-browser.public-key-credential` entitlement. Lilo has neither, so the
  extension cannot reach its helper.
- **Fix:** Not possible in Lilo code: it needs that entitlement, requested from Apple ("macOS
  Browsers Passkeys" request).

### Passkey conditional mediation (autofill) [#passkey-conditional-mediation-autofill]

- **Symptom:** `navigator.credentials.get({ mediation: "conditional" })` /
  `PublicKeyCredential.isConditionalMediationAvailable()` (passkey suggestions in username fields)
  may report unavailable from a third-party WKWebView host.
- **Root cause:** WebKit's conditional-mediation support outside Safari. There is no app-side remedy.
  Explicit (non-conditional) passkey flows also need the entitlement described in
  [Passkeys / WebAuthn](#passkeys--webauthn), which published releases do not carry yet: there, no
  request WebKit handles works.

### Apple Pay on the web [#apple-pay-on-the-web]

- **Symptom:** `window.PaymentRequest` is undefined and `ApplePaySession`, when present, is the
  Apple Pay JS SDK's stand-in, which pays through a code scanned with an iPhone (checked on macOS 27).
- **Root cause:** WebKit exposes both only with the private `_applePayEnabled` setting, off on macOS
  outside Safari. Turned on, `canMakePayments()` is true but the system sheet closes at once:
  PassKit refuses a host without Safari's private `com.apple.private.in-app-payments` and
  `com.apple.payment.all-access` entitlements ("not entitled for merchant session"), which Apple
  grants no third-party app. `com.apple.developer.in-app-payments` covers an app's own merchant IDs,
  not a website's.
- **Lilo's handling:** the setting stays off: a native `ApplePaySession` would take the place of the
  SDK's stand-in and then fail.

### Web Push [#web-push]

- **Symptom:** `PushManager` and `registration.pushManager` are undefined, so a site cannot notify while none of its pages is open. Notifications a page or its service worker shows (`new Notification`, `registration.showNotification`) work.
- **Root cause:** WebKit turns Web Push on only for an app holding Apple's web push entitlement, which Lilo does not have.
- **Lilo's handling:** None.

### Early `Notification.permission` [#early-notificationpermission]

- **Symptom:** A page's scripts that run before `DOMContentLoaded` may read `Notification.permission` as `'default'` on a site you allowed or blocked.
- **Root cause:** WebKit gives a web process the data store's decisions only when it launches it for that store; a prewarmed process, which new tabs and cross-site navigations often take, starts with none. It also reads a blocked site as `'default'` until the site asks, against fingerprinting.
- **Lilo's handling:** at `DOMContentLoaded` the page learns the stored decision. A process launched for its store, and every service worker, knows it from the first script.

### Microphone quality during calls [#microphone-quality-during-calls]
- **Symptom:** A call's microphone sounds duller and more processed than in Chrome: nothing above 12 kHz reaches the page.
- **Root cause:** With echo cancellation on (the default, and what call sites ask for), WebKit runs the microphone through Apple's voice processing, which caps it at 12 kHz; Chrome cancels echo, suppresses noise and sets the gain in software on the raw signal. WebKit exposes neither `noiseSuppression` nor `autoGainControl` apart, and its capture with `echoCancellation: false` comes out 14 to 26 dB below the device's level with dropouts, so turning it off is no remedy. A Bluetooth headset's link caps its microphone at 12 kHz in every browser.
- **Lilo's handling:** None; matching Chrome would take an audio processing stack of Lilo's own.

### Favicons a page changes by script [#favicons-a-page-changes-by-script]

- **Symptom:** a tab keeps the icon its page loaded with when the page then swaps its `<link rel=icon>` (an unread badge, a build status).
- **Root cause:** WebKit offers a document's link icons to the app once, when the document loads, and never again for a `<link rel=icon>` added or changed afterwards (checked on macOS 27). Chrome follows those changes.
- **Lilo's handling:** None; following them would take a script watching every page's `<head>` in an isolated world, an icon load of Lilo's own outside WebKit's, and a per-tab icon apart from the per-site one Lilo caches.

### Referrer of a context-menu download [#referrer-of-a-context-menu-download]

- **Symptom:** "Download Image" or "Download Linked File" from a frame sandboxed without `allow-popups` sends the frame's origin (none from https to http) whatever the frame's referrer policy; with "Prevent cross-site tracking" on, or in a private space, a download to another site sends at most the page's origin even under `unsafe-url`.
- **Root cause:** WebKit's own download items send no Referer, so Lilo has WebKit open the address in a new window from the clicked frame, as a link would, and takes the Referer from that request's policy decision before cancelling it; a sandboxed frame without `allow-popups` cannot open one. WebKit's tracking prevention trims every cross-site Referer to its origin in the network process, a download's included. Chrome applies the frame's policy in both cases.
- **Lilo's handling:** the sandboxed frame gets strict-origin-when-cross-origin from its address; the cross-site trim is WebKit's, as for every request.

### Certificate errors on sites that set HSTS themselves [#certificate-errors-on-sites-that-set-hsts-themselves]

- **Symptom:** a certificate error on a site that sent a `Strict-Transport-Security` header on an earlier visit still offers "Visit Anyway", and the exception loads the page. Chrome offers no way past it. Hosts on the HSTS preload list (all of `.app` and `.dev`, many large sites) get no "Visit Anyway", as in Chrome.
- **Root cause:** the HSTS hosts a profile's pages record live in WebKit's network process. The only view WebKit gives the app, its `_WKWebsiteDataTypeHSTSCache` website data records, names each by registrable domain (`example.com` for `www.example.com`), without the host or `includeSubDomains`, so it cannot tell an HSTS host from a sibling without HSTS (checked on macOS 27). CFNetwork does honor the exception on an HSTS host.
- **Lilo's handling:** the preload list comes from CFNetwork's lookup in Lilo's own process; a per-site rule would also lock users out of a sibling host with a self-signed certificate (a home server under a personal domain), so Lilo keeps offering the exception there.
