Webview

Discord Source API Docs Issues Releases
Discord GitHub Javadoc GitHub



Avaje Webview wraps native platform webview engines to provide a clean interface for building modern cross-platform GUIs. It uses Java 25 FFM (Foreign Function & Memory) to call native libraries directly.

Platform requirements


Platform Engine Requirement
Linux WebKitGTK 6.0 libgtk-4, libwebkitgtk-6.0, libjavascriptcoregtk-6.0 must be installed
macOS WKWebView Built into macOS
Windows WebView2 (Edge) Pre-installed on Windows 10+

Quick Start


1. Add avaje-webview dependency.

<dependency>
  <groupId>io.avaje.webview</groupId>
  <artifactId>avaje-webview</artifactId>
  <version>${avaje.webview.version}</version>
</dependency>

2. JVM flags

On macOS, the first webview must be created on the OS main thread. Pass -XstartOnFirstThread to the JVM:

java -XstartOnFirstThread --enable-native-access=ALL-UNNAMED -jar myapp.jar

3. Build a Webview

Webview webview = Webview.builder()
    .enableDeveloperTools(true)
    .title("My App")
    .html("<h1>Hello World</h1>")
    .build();

webview.run();

Or navigate to a URL:

Webview webview = Webview.builder()
    .title("My App")
    .navigate("https://avaje.io")
    .build();

webview.run();

Options


Window Properties

Webview webview = Webview.builder()
    .title("Configurable Window")
    .width(1200)
    .height(800)
    .enableDeveloperTools(true) // Enable right-click > Inspect
    .resizable(false)           // Lock to width/height, no user resize
    .maximizable(false)         // Hide/disable the maximize button (ignored on Linux)
    .maximize(true)             // Start maximized (ignored if fullscreen(true))
    .fullscreen(true)           // Start fullscreen, takes precedence over maximize
    .minSize(600, 400)          // Minimum size the user can resize to
    .maxSize(1920, 1080)        // Maximum size the user can resize to
    .build();

// Set window constraints after creation
webview.setMinSize(600, 400);
webview.setMaxSize(1920, 1080);

// Or lock to a fixed size
webview.setFixedSize(800, 600);

// Maximize or fullscreen
webview.maximizeWindow();
webview.fullscreen();

// Dark mode
webview.setDarkAppearance(true);

Borderless Windows

Webview webview = Webview.builder()
    .borderless(true) // No title bar, borders, or minimize/maximize/close buttons
    .build();

Combine with webview.startWindowDrag() to implement a custom draggable title bar, since a borderless window has no native title bar for the user to grab.

Keep the native outline (drop shadow and thin border) while still removing the title bar:

Webview webview = Webview.builder()
    .borderless(true, true) // outline=true keeps the native border/shadow
    .build();

Transparent Windows

Webview webview = Webview.builder()
    .borderless(true)
    .transparent(true) // Window background is see-through wherever the page doesn't paint
    .html("<body style='background:transparent'>...</body>")
    .build();

Combine with a page that only paints part of its area (e.g. background: transparent plus a backdrop-filter: blur(...) card) to get a native-looking translucent window. Usually paired with borderless(true) so there's no opaque title bar left behind.

Child Windows

Webview parent = Webview.builder().title("Parent").build();

Webview child = Webview.builder()
    .title("Child")
    .parent(parent) // Blocks the parent's input until this window closes
    .build();

The parent window is disabled (blocked from mouse/keyboard input) as soon as the child is built, and re-enabled automatically when the child closes.

Pass true as a second argument to parent(...) to keep the parent locked to the child's position while dragging (Windows and macOS only currently):

Webview child = Webview.builder()
    .parent(parent, true) // parent moves with the child when dragged
    .build();

Set Window Icon

// From file path
webview.setIcon(Path.of("icon.ico"));

// From classpath resource
webview.setIcon(getClass().getResource("/icon.ico").toURI());

Note: GTK4 dropped support for arbitrary file-based window icons. On Linux, the app icon is set via the .desktop file and icon theme. setIcon is a no-op on Linux.

Java-JavaScript Bridge


Executing JavaScript from Java

Webview webview = Webview.builder()
    .html("<html><body><h1 id='title'>Original</h1></body></html>")
    .build();

// Execute JavaScript in the webview
webview.eval("document.getElementById('title').textContent = 'Updated!';");

webview.run();

Calling Java from JavaScript

Bind Java functions as async window globals that return Promises:

Webview webview = Webview.builder()
    .title("Java Bridge Example")
    .html("""
        <!DOCTYPE html>
        <html>
        <body>
           <button onclick="callJava()">Call Java</button>
           <div id="result"></div>
           <script>
               async function callJava() {
                   try {
                       const result = await greet('World');
                       document.getElementById('result').textContent = result;
                   } catch (error) {
                       console.error('Java error:', error);
                   }
               }
           </script>
        </body>
        </html>
    """)
    .build();

// Bind Java method to JavaScript — args arrive as a JSON array string
webview.bind("greet", (String jsonArgs) -> {
    return "\"Hello, " + jsonArgs + "!\""; // return value must be valid JSON
});

webview.run();

Binding callbacks receive arguments as a JSON array string and must return a valid JSON string. The JavaScript side receives the return value as the resolved Promise value.

Complex Data Exchange

record User(String name, int age) {}
record UserRequest(String action, String userId) {}

Webview webview = Webview.builder()
    .html("""
        <script>
            async function getUser() {
                const user = await fetchUser({ action: 'get', userId: '123' });
                console.log(user.name, user.age);
            }
        </script>
    """)
    .build();

webview.bind("fetchUser", (String jsonArgs) -> {
    UserRequest request = Jsonb.instance().type(UserRequest.class).list().fromJson(jsonArgs).getFirst();
    User user = new User("Alice", 30);
    return Jsonb.instance().toJson(user);
});

webview.eval("getUser();");
webview.run();

Multiple Windows


Multiple windows are supported on all platforms. Additional windows can be created from any thread once the first window's event loop is running.

try (var w1 = Webview.create(false)) {
    w1.bind("openSecond", _ -> {
        Thread.ofPlatform().start(() -> {
            try (var w2 = Webview.create(false)) {
                w2.setHTML("<h1>Window 2</h1>");
                w2.run();
            }
        });
        return "null";
    });
    w1.setHTML("<button onclick='openSecond()'>Open second window</button>");
    w1.run();
}

macOS constraint: the first Webview must be created on the OS main thread (-XstartOnFirstThread). Additional windows may be created from background threads.

Linux constraint: the first Webview must be created on the thread that will call run(). All subsequent windows dispatch their init to that thread automatically.

Console output


Webview webview = Webview.builder()
    .redirectConsole(true) // Defaults to false
    .build();

With redirectConsole(true), console.log, console.warn, console.error, etc. are forwarded to java.lang.System.Logger under the name io.avaje.webview. Configure your logging framework (e.g. Simple Logger) to see webview JS console output.