Skip to main content

A lightweight native webview widget for PySide6.

Project description

SideView

A lightweight native webview widget for PySide6 applications that need the operating system browser stack without bundling Qt WebEngine.

The library exposes a normal QWidget subclass in Python and delegates rendering to:

  • Windows: Microsoft Edge WebView2, hosted inside the widget's native HWND.
  • macOS: WKWebView, hosted inside the widget's native NSView.
  • Linux: WebKitGTK 4.1, embedded into the Qt widget through an X11 foreign window.

This is intentionally not a full browser. It is a small embedded view for in-app navigation, OAuth/help pages, web dashboards, and video playback using codecs available through the operating system browser stack.

It also gives the host application control over browser-adjacent behavior that should not be left to the embedded engine by default:

  • Downloads are intercepted and blocked unless your Python whitelist callback allows them.
  • Links that request a new window are intercepted and surfaced as a signal, so the app can open an internal tab instead of letting the engine spawn a separate native window.
  • JavaScript can be injected at document creation, and page scripts can send messages back to Python.
  • Native context menus and devtools can be disabled so the host app can provide its own controlled menu.
  • The visible webview can be captured as PNG/JPEG bytes, either as a full frame, viewport-relative region, or a continuous native frame stream where supported.
  • Browser zoom can be read, changed, and observed through a platform-neutral API.

Why not fork pywebview?

pywebview is a window abstraction. Its backends are designed to own top-level windows and integrate with their own GUI loops. This library uses the opposite contract: PySide6 owns the application, and the native webview is a child of a Qt widget. That keeps focus, resizing, lifecycle, and application shutdown under Qt's control.

Current status

SideView is currently an alpha API. The repository provides:

  • Focused Python NativeWebView API.
  • Native library loader with clear platform errors.
  • Windows C++ backend using WebView2.
  • macOS Objective-C++ backend using WKWebView.
  • Linux C++ backend using WebKitGTK and GTK 3.
  • CMake build files for native libraries.
  • A tabbed PySide6 browser example with back, forward, reload, new tab, close tab, URL/search bar, download policy hooks, and new-window routing.
  • Script bridge hooks for custom overlays, page-to-Python messages, and controlled context menus.
  • Asynchronous native capture hooks for tab-live previews, region projection, and high-FPS frame streaming on WebView2.

Installation

Windows x64 and macOS universal2 wheels contain the native backend:

python -m pip install sideview

Linux is distributed as source because WebKitGTK is a system library. Install the WebKitGTK 4.1 and GTK 3 development packages for your distribution before running the same pip install command. For Ubuntu 22.04 and newer:

sudo apt-get install libgtk-3-dev libwebkit2gtk-4.1-dev pkg-config
python -m pip install sideview

Linux embedding requires Qt's XCB platform. Native X11 works directly; Wayland sessions can run through XWayland by setting QT_QPA_PLATFORM=xcb before the QApplication is created.

Python usage

from PySide6.QtWidgets import QApplication, QMainWindow
from sideview import NativeWebView

app = QApplication([])

window = QMainWindow()
view = NativeWebView()
window.setCentralWidget(view)
window.resize(1200, 800)
window.show()

view.navigate("https://www.youtube.com")

app.exec()

Downloads and new windows

Downloads are denied by default. Register a callback with set_download_policy() to allow only the URLs your app trusts. The callback should return True to let the native webview continue the download, or False to cancel it so your app can handle the URL itself.

from urllib.parse import urlparse

ALLOWED_DOWNLOAD_HOSTS = {"example.com", "cdn.example.com"}


def allow_download(url: str) -> bool:
    return (urlparse(url).hostname or "").lower() in ALLOWED_DOWNLOAD_HOSTS


view.set_download_policy(allow_download)
view.downloadRequested.connect(lambda url: print("Download requested:", url))

Links that try to open a new tab or popup are not opened as separate native windows. Instead, the widget emits newWindowRequested(url).

view.newWindowRequested.connect(lambda url: open_internal_tab(url))

JavaScript bridge and context menus

Use add_document_script() for persistent JavaScript that should run at document creation on every future navigation. Use eval_js() for one-shot JavaScript on the currently loaded page.

view.add_document_script("""
window.addEventListener("DOMContentLoaded", () => {
  document.documentElement.dataset.hostApp = "solin";
});
""")

install_script_bridge() exposes window.nativeWebView.postMessage(message) to page scripts. Messages arrive in Python through scriptMessageReceived.

view.install_script_bridge()
view.scriptMessageReceived.connect(lambda message: print("JS:", message))

view.eval_js("""
window.nativeWebView.postMessage({
  type: "overlay-ready",
  href: location.href
});
""")

For a controlled right-click menu, call install_context_menu_bridge(). It disables the native browser context menu, prevents default page context menus, and emits contextMenuRequested(dict) with coordinates and basic target metadata.

def show_context_menu(payload: dict) -> None:
    print(payload["x"], payload["y"], payload.get("href"), payload.get("src"))


view.set_devtools_enabled(False)
view.install_context_menu_bridge()
view.contextMenuRequested.connect(show_context_menu)

Native capture

capture_frame() and capture_region() return immediately with a request id. The PNG bytes arrive later through captureCompleted(request_id, data). capture_frame_jpeg() follows the same request/response model, but returns JPEG bytes for lighter live-preview snapshots. Failures arrive through captureFailed(request_id, error).

def save_capture(request_id: int, data: bytes) -> None:
    Path(f"capture-{request_id}.png").write_bytes(data)


view.captureCompleted.connect(save_capture)
view.captureFailed.connect(lambda request_id, error: print("Capture failed:", error))

full_request_id = view.capture_frame()
region_request_id = view.capture_region(120, 80, 640, 360)
jpeg_request_id = view.capture_frame_jpeg()

The capture API is intentionally native and asynchronous:

  • Windows uses WebView2 CapturePreview and crops regions with Windows Imaging Component.
  • macOS uses WKWebView snapshots with WKSnapshotConfiguration.
  • Linux uses WebKitGTK visible-region snapshots and GdkPixbuf encoding.
  • Coordinates use widget pixels with a top-left origin.

For live projection on Windows, prefer start_frame_stream(). It uses WebView2 DevTools screencast frames and emits JPEG bytes through frameStreamFrame(data). Unsupported platforms return False, so production apps should fall back to capture_frame_jpeg().

def show_live_frame(data: bytes) -> None:
    pixmap = QPixmap()
    if pixmap.loadFromData(data):
        update_projection(pixmap)


view.frameStreamFrame.connect(show_live_frame)
view.frameStreamFailed.connect(lambda error: print("Frame stream failed:", error))

if not view.start_frame_stream(quality=75, max_width=1280, max_height=720):
    start_timer_based_jpeg_capture()

# Later, when projection stops:
view.stop_frame_stream()

Sessions and cookies

Use session_id to isolate browser state per application profile. Views created with the same session_id share cookies/cache; views created with different IDs get separate sessions.

profile_id = "default"  # for example, your active app profile id

view = NativeWebView(
    session_id=f"solin_session_{profile_id}",
    session_data_root="path/to/user/data/webview",
)

On Windows, the session maps to a WebView2 userDataFolder. On macOS, the session maps to a persistent WKWebsiteDataStore identifier when the platform supports it. On Linux, it maps to a WebKitGTK website-data directory with persistent cookie storage. You can still pass user_data_folder directly when you need full control over the storage path.

Cookies can be set before or after the native view is ready. If the view is not ready yet, the cookie operation is queued and applied before the first pending navigation.

view.set_cookie(
    name="session",
    value="abc123",
    domain=".example.com",
    path="/",
    secure=True,
    http_only=True,
    same_site="lax",
)

To clear the current session cookies:

view.clear_cookies()
view.reload()

Zoom

set_zoom_factor() applies native page zoom. User changes, including Ctrl + scroll in WebView2, emit zoomFactorChanged, allowing a host with multiple views to keep one shared zoom preference.

view.set_zoom_factor(1.25)
view.zoomFactorChanged.connect(lambda factor: print("Zoom:", factor))
print(view.zoom_factor())

Building from source

GitHub Actions

Every pull request runs tests and builds the release distributions through .github/workflows/build-distributions.yml. Release builds produce:

  • a Windows x64 py3-none-win_amd64 wheel;
  • a macOS py3-none-macosx_*_universal2 wheel;
  • one source distribution used to build the Linux backend on the target machine.

The workflow can also be dispatched manually for a branch, tag, or commit.

Windows

Install the WebView2 Runtime and the WebView2 SDK headers/libraries. The CMake project expects WEBVIEW2_SDK_DIR to point at the SDK root that contains build/native/include/WebView2.h and the matching loader library.

.\scripts\build_windows.ps1 -WebView2SdkDir C:\path\to\Microsoft.Web.WebView2

The script uses vswhere to locate Visual Studio Build Tools, configures MSVC through vcvars64.bat, builds the DLL, and copies it next to the Python package. If you build manually, copy sideview_native.dll next to src/sideview/ or set:

$env:SIDEVIEW_NATIVE_LIB="C:\path\to\sideview_native.dll"

macOS

cmake -S native -B build/native -DCMAKE_BUILD_TYPE=Release
cmake --build build/native

Copy libsideview_native.dylib next to src/sideview/ or set:

export SIDEVIEW_NATIVE_LIB=/path/to/libsideview_native.dylib

Linux

The Linux backend uses the GTK 3 build of WebKitGTK 4.1. On Ubuntu 22.04 or newer Debian-based distributions, install the build dependencies and build the library with:

sudo apt-get install libgtk-3-dev libwebkit2gtk-4.1-dev pkg-config
bash scripts/build_linux.sh

The build script copies libsideview_native.so next to the Python package. A custom location can be selected with:

export SIDEVIEW_NATIVE_LIB=/path/to/libsideview_native.so

Embedding GTK inside Qt requires the X11/XCB window model. It works on native X11 desktops and through XWayland. On a Wayland session, launch the application with the Qt XCB backend before creating QApplication:

QT_QPA_PLATFORM=xcb python examples/simple_browser.py

WebKitGTK and GTK remain dynamically linked system dependencies.

Design constraints

  • The native webview is a real native child view. It draws outside Qt's paint engine, so it should not be overlapped by translucent Qt widgets.
  • Because rendering happens in a native child view, Qt QWidget.grab() is not a reliable production-grade way to capture tab-live thumbnails or crop projected page regions. Use the native capture API instead.
  • Keep one webview per visible widget unless the product truly needs more; native browser views are heavier than normal widgets.
  • Navigation policy, custom context menus, downloads, permission prompts, and devtools should be added deliberately as product requirements, not by default.
  • Linux embedding requires Qt to use the XCB platform because GTK 3 foreign-window embedding is X11-specific. Native Wayland cross-toolkit embedding is not available.

Example

Run:

python examples/simple_browser.py

The example opens Google in a new tab by default. The address bar goes directly to valid URLs and searches Google when the input looks like plain text. Its download whitelist is intentionally empty:

DOWNLOAD_HOST_WHITELIST: set[str] = set()

Replace is_download_allowed(url) in examples/simple_browser.py with your application policy when you decide how downloads should be handled.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sideview-0.4.0.tar.gz (49.4 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

sideview-0.4.0-py3-none-win_amd64.whl (54.5 kB view details)

Uploaded Python 3Windows x86-64

sideview-0.4.0-py3-none-macosx_12_0_universal2.whl (52.9 kB view details)

Uploaded Python 3macOS 12.0+ universal2 (ARM64, x86-64)

File details

Details for the file sideview-0.4.0.tar.gz.

File metadata

  • Download URL: sideview-0.4.0.tar.gz
  • Upload date:
  • Size: 49.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for sideview-0.4.0.tar.gz
Algorithm Hash digest
SHA256 ccadcb9304e83e88c4da202dd4940827592c77b48e74184d87dd17248afb6db0
MD5 1f23d897275de30c962524afbf53f57b
BLAKE2b-256 6a7f7fdc75bf943cb08b868fbefc88b1d3231526e8b229d934a9b89e2122df34

See more details on using hashes here.

Provenance

The following attestation bundles were made for sideview-0.4.0.tar.gz:

Publisher: release.yml on AlexsanderMe/SideView

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sideview-0.4.0-py3-none-win_amd64.whl.

File metadata

  • Download URL: sideview-0.4.0-py3-none-win_amd64.whl
  • Upload date:
  • Size: 54.5 kB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for sideview-0.4.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 81b738130f0c200937b4ee620b8e533854e0790430167a95521e69f0a37b05cc
MD5 c88cbdff44b5ebfefbe5efba8eefd29e
BLAKE2b-256 d33647589e7ef02e13e9078d61a8f4760e36a3ff1f1afb0dd5a5aaab970c5750

See more details on using hashes here.

Provenance

The following attestation bundles were made for sideview-0.4.0-py3-none-win_amd64.whl:

Publisher: release.yml on AlexsanderMe/SideView

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sideview-0.4.0-py3-none-macosx_12_0_universal2.whl.

File metadata

File hashes

Hashes for sideview-0.4.0-py3-none-macosx_12_0_universal2.whl
Algorithm Hash digest
SHA256 0f74a25c13c9171475d89311491f251f3ffca91f4d56b60de180491cf94620f3
MD5 d7a76c9c3f144af8bdeee97349728641
BLAKE2b-256 49caaae6ad29d4a09cc287e675a47e5831d650028ba14199c7a4b71d343a5607

See more details on using hashes here.

Provenance

The following attestation bundles were made for sideview-0.4.0-py3-none-macosx_12_0_universal2.whl:

Publisher: release.yml on AlexsanderMe/SideView

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page