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 nativeNSView. - 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
NativeWebViewAPI. - 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
CapturePreviewand 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_amd64wheel; - a macOS
py3-none-macosx_*_universal2wheel; - 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ccadcb9304e83e88c4da202dd4940827592c77b48e74184d87dd17248afb6db0
|
|
| MD5 |
1f23d897275de30c962524afbf53f57b
|
|
| BLAKE2b-256 |
6a7f7fdc75bf943cb08b868fbefc88b1d3231526e8b229d934a9b89e2122df34
|
Provenance
The following attestation bundles were made for sideview-0.4.0.tar.gz:
Publisher:
release.yml on AlexsanderMe/SideView
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sideview-0.4.0.tar.gz -
Subject digest:
ccadcb9304e83e88c4da202dd4940827592c77b48e74184d87dd17248afb6db0 - Sigstore transparency entry: 2255374272
- Sigstore integration time:
-
Permalink:
AlexsanderMe/SideView@ccb28fa5933942f49ea3a79feccc4f7777ed9fb7 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/AlexsanderMe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ccb28fa5933942f49ea3a79feccc4f7777ed9fb7 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81b738130f0c200937b4ee620b8e533854e0790430167a95521e69f0a37b05cc
|
|
| MD5 |
c88cbdff44b5ebfefbe5efba8eefd29e
|
|
| BLAKE2b-256 |
d33647589e7ef02e13e9078d61a8f4760e36a3ff1f1afb0dd5a5aaab970c5750
|
Provenance
The following attestation bundles were made for sideview-0.4.0-py3-none-win_amd64.whl:
Publisher:
release.yml on AlexsanderMe/SideView
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sideview-0.4.0-py3-none-win_amd64.whl -
Subject digest:
81b738130f0c200937b4ee620b8e533854e0790430167a95521e69f0a37b05cc - Sigstore transparency entry: 2255374289
- Sigstore integration time:
-
Permalink:
AlexsanderMe/SideView@ccb28fa5933942f49ea3a79feccc4f7777ed9fb7 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/AlexsanderMe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ccb28fa5933942f49ea3a79feccc4f7777ed9fb7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sideview-0.4.0-py3-none-macosx_12_0_universal2.whl.
File metadata
- Download URL: sideview-0.4.0-py3-none-macosx_12_0_universal2.whl
- Upload date:
- Size: 52.9 kB
- Tags: Python 3, macOS 12.0+ universal2 (ARM64, x86-64)
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0f74a25c13c9171475d89311491f251f3ffca91f4d56b60de180491cf94620f3
|
|
| MD5 |
d7a76c9c3f144af8bdeee97349728641
|
|
| BLAKE2b-256 |
49caaae6ad29d4a09cc287e675a47e5831d650028ba14199c7a4b71d343a5607
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sideview-0.4.0-py3-none-macosx_12_0_universal2.whl -
Subject digest:
0f74a25c13c9171475d89311491f251f3ffca91f4d56b60de180491cf94620f3 - Sigstore transparency entry: 2255374280
- Sigstore integration time:
-
Permalink:
AlexsanderMe/SideView@ccb28fa5933942f49ea3a79feccc4f7777ed9fb7 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/AlexsanderMe
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ccb28fa5933942f49ea3a79feccc4f7777ed9fb7 -
Trigger Event:
push
-
Statement type: