Keep Tkinter — give it the WebView it never had.
Embed a real system WebView (wry) inside your Frame: modern HTML, JS, and IPC in the same layout as your buttons and tabs — one mainloop, no floating overlay.
Alpha — Early preview (see PyPI badge for the current version). APIs and behavior may change without notice. Not recommended for production use yet.
Tkinter is still a solid GUI shell — it just had no first-class way to host modern web content inside a widget. Overlay-style WebViews drift out of sync when you move, resize, or switch tabs.
tkwry fills that missing piece:
- True child embedding —
build_as_childvia HWND, NSView, or X11 window ID - One event loop — Tk
mainlooponly; no separate app runtime - IPC bridge — JavaScript → Python callbacks without freezing the UI
- Layout-aware — tracks
pack/grid/place, tabs, andPanedWindow
Pre-built abi3 wheels ship for Windows (x86_64, arm64) and macOS (Apple Silicon + Intel) — these are the primary release targets. Linux is best-effort: build from source (sdist / git); timing and headless behavior are not guaranteed in v0.0.x.
- Python 3.10+
- Tkinter (included with most Python builds)
- Building from source (git clone,
pip install git+…, or Linux) — Rust toolchain (stable);pipuses maturin as the build backend - Windows (x86_64, arm64)
- WebView2 Runtime must be installed (pre-installed on many Windows 10/11 systems).
- Without WebView2, tkwry is not supported on Windows — there is no fallback engine.
- macOS — 11 (Big Sur) or later; Apple Silicon (arm64) or Intel (x86_64); system WKWebView (no extra runtime)
- Linux — WebKitGTK 4.1 + GTK 3 dev packages; X11 or XWayland (
$DISPLAY); build the extension from source (see below)
pip install tkwryCloning the repo and installing locally compiles the Rust extension on your machine. You need a Rust toolchain (rustup) and platform runtimes from Requirements above (WebView2 on Windows, etc.). pip pulls in maturin automatically as the build backend.
git clone https://github.com/mashu3/tkwry.git
cd tkwry
pip install -e .Use this for development and for running the examples from the tree.
pip install git+https://github.com/mashu3/tkwry.gitThis also builds from source (sdist via git), not a pre-built wheel. It requires Rust and will fail on machines without a working toolchain — same as pip install . / pip install -e .. Prefer the PyPI wheel on Windows and macOS when you do not need bleeding-edge changes.
Linux builds from source and runs for many apps, but v0.0.x does not treat Linux stability as a release requirement — focus is on Windows and macOS wheels. Install system dependencies, then:
# Debian / Ubuntu
sudo apt install \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libglib2.0-dev
# Runtime (for end users of your app)
# sudo apt install libwebkit2gtk-4.1-0 libgtk-3-0
pip install maturin
git clone https://github.com/mashu3/tkwry.git
cd tkwry
pip install .GTK events are pumped automatically on a Tk timer while your app runs.
import tkinter as tk
from tkwry import WebView
root = tk.Tk()
root.geometry("900x600")
frame = tk.Frame(root, bg="#222")
frame.pack(fill="both", expand=True, padx=8, pady=8)
web = WebView(frame, url="https://github.com")
root.mainloop()def on_message(msg: str) -> None:
print("from JS:", msg)
web = WebView(
frame,
html='<button onclick="window.ipc.postMessage(\'hi\')">send</button>',
ipc_handler=on_message,
)web.load_html("<h1>Hello</h1>")
web.eval_js("document.title = 'Hi'") # fire-and-forget (Tk idle, no return value)
web.eval_js("bad()", on_error=lambda exc: print("eval failed:", exc))
web.eval_js_with_callback("document.title", print) # async; callback on Tk main thread
web.load_url("https://example.com")
web.reload()
print(web.url)
web.focus()
web.open_devtools()Rapid load_url / load_html calls are coalesced (last-wins) — load(A); load(B); load(C) loads C only.
eval_js does not return a result (not synchronous). Use eval_js_with_callback when you need the JavaScript return value as a str. Pass on_error= to handle evaluation failures on the Tk main thread; otherwise the traceback is printed to stderr (EvalErrorHandler).
Bounds sync runs automatically on <Configure>, <Map>, and <Unmap>. Call sync_bounds() manually after custom layout changes so the WebView reflows (e.g. centered images):
web.sync_bounds()from tkwry import NewWindowResponse, PageLoadEvent
web = WebView(
frame,
url="https://example.com",
on_page_load=lambda evt, url: print(evt, url),
on_title_changed=lambda title: root.title(title),
on_navigation=lambda url: url.startswith("https://"),
on_new_window=lambda url: NewWindowResponse.Deny,
)on_page_load fires PageLoadEvent.Started and PageLoadEvent.Finished for every navigation. Events that occurred before a handler was registered are discarded when you call set_on_page_load (or pass on_page_load in the constructor from the start).
Callback threads: on_page_load and on_title_changed run on the Tk main thread (queued). on_navigation and on_new_window run synchronously on the WebKit thread — wry needs an immediate return value, so they cannot be queued. Keep those handlers fast and avoid Tk widget calls; defer work with root.after if needed.
Callback exceptions are printed to stderr and do not stop event delivery.
File drops from Finder / Explorer are handled by the OS WebView. Your handler runs on the Tk main thread (tkwry queues events from WebKit automatically). The handler is notify-only (-> None); drops are always accepted and cannot be denied from Python.
from tkwry import DragDropEvent
def on_drop(event, paths, position):
if event == DragDropEvent.Drop:
print("files:", paths)
web = WebView(frame, html="...", drag_drop_handler=on_drop)See examples/dnd_demo.py.
web.destroy() # release native webview; host Frame is kept
# or destroy the host Frame — both tear down the webview| Category | Members |
|---|---|
| Content | load_url, load_html, reload, url |
| JavaScript | eval_js (on_error), eval_js_with_callback |
| IPC | set_ipc_handler |
| Callbacks | set_on_navigation, set_on_page_load, set_on_title_changed, set_on_new_window, set_drag_drop_handler |
| Appearance | set_background_color, focus, focus_parent, open_devtools, close_devtools, is_devtools_open |
| Layout | pack, grid, place, sync_bounds (delegate to host Frame except sync_bounds) |
| Lifecycle | destroy, destroyed, native |
Constructor options: url, html, ipc_handler, devtools, background_color,
user_agent, initialization_script, focused, plus the callback hooks above.
Enums: PageLoadEvent, NewWindowResponse, DragDropEvent.
Type aliases: IpcHandler, NavigationHandler, PageLoadHandler, TitleChangedHandler, NewWindowHandler, DragDropHandler, EvalCallback, EvalErrorHandler.
- Alpha — APIs may change; not recommended for production yet
- Windows — WebView2 Runtime required; systems without it are unsupported
- Linux — source install only (no PyPI wheel); best-effort in v0.0.x — headless CI and event timing are not release blockers
- DevTools — macOS uses private APIs; avoid in Mac App Store release builds
- macOS input — Tk text widgets and the WebView share one window; tkwry routes focus automatically (see macOS embedding). IME and other advanced input may still differ from a standalone browser
- Drag & drop — drop target is the WebView area only (not arbitrary Tk widgets; use tkinterdnd2 for those)
See CHANGELOG.md for release history.
| OS | Arch | Parent handle | Engine | Notes |
|---|---|---|---|---|
| Windows | x86_64, arm64 | Frame.winfo_id() → HWND |
WebView2 | WebView2 Runtime required |
| macOS | arm64, x86_64 | Toplevel content NSView |
WKWebView | See macOS embedding below |
| Linux | — | winfo_id() → X11 window ID |
WebKitGTK | Source install; best-effort (not a wheel release target) |
On macOS, Tk child Frames usually do not get their own NSView — that is a property of the Tk Aqua backend, not something tkwry can turn into per-frame native views without upstream Tk changes.
tkwry works around this by:
- Attaching the WebView to the toplevel content view
- Positioning it with
set_boundsto match yourFrame(<Configure>) - Hiding it with
set_visible(False)when the frame is unmapped (<Unmap>) — e.g. anotherttk.Notebooktab is selected
Keyboard focus (macOS): tkwry routes input between Tk widgets (Entry, Text, …) and the WebView automatically. Rust hit-tests clicks at the NSEvent layer and switches first responder; Python drains focus signals on the Tk main thread so keystrokes reach the correct target. Use web.focus() and web.focus_parent() when you need explicit control — see examples/url_demo.py. IME and other advanced input may still behave differently than in a standalone browser.
You do not need extra code for tabs or panes — see examples/multi_demo.py. IPC, page-load, title, eval, and drag-and-drop handlers are dispatched on the Tk main thread via an internal queue (avoids WebKit deadlocks). on_navigation and on_new_window are synchronous WebKit-thread hooks — see Navigation / lifecycle callbacks.
Tkinter apps already have a window and a layout. The web belongs inside a Frame — same mainloop, same tabs and panes — not in a separate top-level webview that floats beside your UI. tkwry wraps wry's build_as_child against the native surface Tk gives your widgets.
- Child-window embedding — WebView is a native child of your Tk window surface, not a floating overlay
- Bounds & visibility sync — follows
<Configure>,<Map>, and<Unmap>(tabs /Notebookwork out of the box on macOS) - Deferred callbacks — IPC, page load, title, eval results, and DnD queue to Tk (avoids macOS deadlocks)
- URL safety — normalizes and validates URLs before navigation
- DevTools —
open_devtools()/devtools=Truefor debugging - Native drag & drop — OS-level file drops into the WebView (no tkinterdnd2)
- Navigation hooks —
on_page_load,on_title_changed(Tk thread);on_navigation,on_new_window(WebKit thread, synchronous) - Multiple layouts — works with
pack,grid,place,Notebook, andPanedWindow(see examples) - Plotly-ready — load HTML +
eval_jsfor interactive charts - Folium-ready — embed Leaflet maps from Folium HTML (right-click to pin)
- Markdown-ready — Monaco editor + live preview in a
PanedWindow(seeexamples/markdown_demo.py; CDN required) - Alpha, but tested — CI runs
pytest tests/on Windows (x86_64 + arm64), macOS, and Linux (Xvfb + WebKitGTK); many timing-sensitive integration tests are skipped on Linux CI (best-effort, not a release blocker)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .| Script | Description |
|---|---|
examples/url_demo.py |
URL bar + embedded page |
examples/ipc_demo.py |
JavaScript ↔ Tkinter IPC |
examples/multi_demo.py |
Multiple WebViews, tabs, panes |
examples/plotly_demo.py |
Plotly charts (pip install plotly) |
examples/folium_demo.py |
Folium maps (pip install folium) |
examples/markdown_demo.py |
Monaco markdown editor + live preview (CDN) |
examples/dnd_demo.py |
Native file drag & drop into WebView |
python examples/url_demo.py
python examples/ipc_demo.py
python examples/multi_demo.py
python examples/plotly_demo.py
python examples/folium_demo.py
python examples/markdown_demo.py
python examples/dnd_demo.pyThis project is licensed under the MIT License. See LICENSE.
This project links against wry, which is dual-licensed (Apache-2.0 or MIT). tkwry uses wry under MIT; see NOTICE for attribution.