Web compatibility
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.
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.
The text below comes from the project’s compatibility tracker, COMPATIBILITY.md. Extension APIs are covered in Extension compatibility.
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)
- 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 (
requestPortwith USB vendor and product filters,getPorts,openwith baud rate, data bits, stop bits, parity and hardware flow control,readable/writablestreams,setSignals,getSignals,forget) over a native bridge to the serial devices IOKit lists.requestPortshows 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
connectevent when a device is plugged in (disconnectfires when it goes); cross-origin iframes get nonavigator.serial; reads are not flow-controlled by the page.
EyeDropper API (window.EyeDropper)
- 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 withAbortError.
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)
- Status: Not implemented, on purpose
- WebKit support: None on macOS -
'registerProtocolHandler' in navigatoris false in a WKWebView (checked on macOS 27); Chrome lets a site handle a scheme, such as Gmail formailto: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
- Status: Partial - a click runs the worker’s
notificationclick, andclient.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
- Status: needs the
com.apple.developer.web-browser.public-key-credentialentitlement, which Apple grants per app ID. Without it, WebKit’sgetCanCurrentProcessAccessPasskeysForRelyingPartycheck fails:PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()is false andnavigator.credentials.create()/.get()reject at once withNotAllowedError(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 withNotAllowedError, 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 onnavigator.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; ad-hoc and Debug builds never do. - Not pursued: A custom
ASAuthorizationPlatformPublicKeyCredentialProviderintegration 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)
- FairPlay Streaming (
com.apple.fps,1_0,2_0,3_0): supported, no configuration needed. Measured in a WKWebView configured like Lilo’s tabs:requestMediaKeySystemAccessis granted forcenc,sinfandskdinit data with H.264 and 10-bit HEVC, andcreateMediaKeysworks. The legacywindow.WebKitMediaKeysis 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;libwidevinecdmplugs 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
Lilotoken), so services serve their Safari/FairPlay variant (see 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)
- 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;requestWindowmakes the tab’s nextwindow.opena same-origin popup thatDocumentPiPControllerhosts: 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 theenterpictureinpictureMedia 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 aNotAllowedError. - Google Meet: Meet offers its Document PiP only to Chrome, read from
navigator.userAgent(it never readsuserAgentData), and fixes at load the identity its WebRTC stack follows. Lilo shows a Chrome user agent onmeet.google.comonly in the microtask after a read ofdocumentPictureInPicture: Meet’s PiP checks pass, its call stays on Safari’s media path. A full Chrome identity breaks camera and microphone (Chromium-onlycreateEncodedStreamsand audioMediaStreamTrackGenerator).defaults write <bundle id> LiloMeetDocumentPiPDisabled -bool trueturns the Meet part off (new tabs).
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
getDisplayMediasurface), so Lilo interceptsgetDisplayMediain the page world: a chrome picker lists the open tabs, and picking one returns a realMediaStreambuilt 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
getDisplayMediaand 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). A desktop pick uses the systemSCContentSharingPickerdirectly, and anSCStreamfeeds 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
- 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)
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’sallowed_origins. Its parent must either match a fixed list of known browsers’ signing identifier and team identifier pairs, or hold thecom.apple.developer.web-browser.public-key-credentialentitlement. 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)
- 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, which published releases do not carry yet: there, no request WebKit handles works.
Apple Pay on the web
- Symptom:
window.PaymentRequestis undefined andApplePaySession, 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
_applePayEnabledsetting, off on macOS outside Safari. Turned on,canMakePayments()is true but the system sheet closes at once: PassKit refuses a host without Safari’s privatecom.apple.private.in-app-paymentsandcom.apple.payment.all-accessentitlements (“not entitled for merchant session”), which Apple grants no third-party app.com.apple.developer.in-app-paymentscovers an app’s own merchant IDs, not a website’s. - Lilo’s handling: the setting stays off: a native
ApplePaySessionwould take the place of the SDK’s stand-in and then fail.
Web Push
- Symptom:
PushManagerandregistration.pushManagerare 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
- Symptom: A page’s scripts that run before
DOMContentLoadedmay readNotification.permissionas'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
DOMContentLoadedthe 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
- 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
noiseSuppressionnorautoGainControlapart, and its capture withechoCancellation: falsecomes 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
- 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
- Symptom: “Download Image” or “Download Linked File” from a frame sandboxed without
allow-popupssends 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 underunsafe-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-popupscannot 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
- Symptom: a certificate error on a site that sent a
Strict-Transport-Securityheader 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.appand.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
_WKWebsiteDataTypeHSTSCachewebsite data records, names each by registrable domain (example.comforwww.example.com), without the host orincludeSubDomains, 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.