diff --git a/libs/webview/include/webview.h b/libs/webview/include/webview.h
index 62ac14b55528..25c2a8d168ce 100644
--- a/libs/webview/include/webview.h
+++ b/libs/webview/include/webview.h
@@ -257,6 +257,12 @@ WEBVIEW_API void webview_set_title(webview_t w, const char *title);
 /**
  * Updates the size of the native window.
  *
+ * Remarks:
+ * - Using WEBVIEW_HINT_MAX for setting the maximum window size is not
+ *   supported with GTK 4 because X11-specific functions such as
+ *   gtk_window_set_geometry_hints were removed. This option has no effect
+ *   when using GTK 4.
+ *
  * @param w The webview instance.
  * @param width New width.
  * @param height New height.
@@ -1095,23 +1101,39 @@ inline std::string json_parse(const std::string &s, const std::string &key,
 //
 // ====================================================================
 //
-// This implementation uses webkit2gtk backend. It requires gtk+3.0 and
-// webkit2gtk-4.0 libraries. Proper compiler flags can be retrieved via:
+// This implementation uses webkit2gtk backend. It requires GTK and
+// WebKitGTK libraries. Proper compiler flags can be retrieved via:
 //
+//   pkg-config --cflags --libs gtk4 webkitgtk-6.0
+//   pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.1
 //   pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0
 //
 // ====================================================================
 //
 #include <cstdlib>
 
-#include <JavaScriptCore/JavaScript.h>
 #include <gtk/gtk.h>
+
+#if GTK_MAJOR_VERSION >= 4
+
+#include <jsc/jsc.h>
+#include <webkit/webkit.h>
+
+#ifdef GDK_WINDOWING_X11
+#include <gdk/x11/gdkx.h>
+#endif
+
+#elif GTK_MAJOR_VERSION >= 3
+
+#include <JavaScriptCore/JavaScript.h>
 #include <webkit2/webkit2.h>
 
 #ifdef GDK_WINDOWING_X11
 #include <gdk/gdkx.h>
 #endif
 
+#endif
+
 #include <fcntl.h>
 #include <sys/stat.h>
 
@@ -1166,9 +1188,8 @@ static inline bool is_wayland_display() {
 // See: https://docs.gtk.org/gdk3/class.DisplayManager.html
 static inline bool is_gdk_x11_backend() {
 #ifdef GDK_WINDOWING_X11
-  auto *manager = gdk_display_manager_get();
-  auto *display = gdk_display_manager_get_default_display(manager);
-  return GDK_IS_X11_DISPLAY(display); // NOLINT(misc-const-correctness)
+  auto *gdk_display = gdk_display_get_default();
+  return GDK_IS_X11_DISPLAY(gdk_display); // NOLINT(misc-const-correctness)
 #else
   return false;
 #endif
@@ -1215,65 +1236,192 @@ static inline void apply_webkit_dmabuf_workaround() {
 }
 } // namespace webkit_dmabuf
 
-namespace webkit_symbols {
-using webkit_web_view_evaluate_javascript_t =
-    void (*)(WebKitWebView *, const char *, gssize, const char *, const char *,
-             GCancellable *, GAsyncReadyCallback, gpointer);
+/**
+ * GTK compatibility helper class.
+ */
+class gtk_compat {
+public:
+  static gboolean init_check() {
+#if GTK_MAJOR_VERSION >= 4
+    return gtk_init_check();
+#else
+    return gtk_init_check(nullptr, nullptr);
+#endif
+  }
+
+  static GtkWidget *window_new() {
+#if GTK_MAJOR_VERSION >= 4
+    return gtk_window_new();
+#else
+    return gtk_window_new(GTK_WINDOW_TOPLEVEL);
+#endif
+  }
+
+  static void window_set_child(GtkWindow *window, GtkWidget *widget) {
+#if GTK_MAJOR_VERSION >= 4
+    gtk_window_set_child(window, widget);
+#else
+    gtk_container_add(GTK_CONTAINER(window), widget);
+#endif
+  }
+
+  static void window_remove_child(GtkWindow *window, GtkWidget *widget) {
+#if GTK_MAJOR_VERSION >= 4
+    if (gtk_window_get_child(window) == widget) {
+      gtk_window_set_child(window, nullptr);
+    }
+#else
+    gtk_container_remove(GTK_CONTAINER(window), widget);
+#endif
+  }
+
+  static void widget_set_visible(GtkWidget *widget, bool visible) {
+#if GTK_MAJOR_VERSION >= 4
+    gtk_widget_set_visible(widget, visible ? TRUE : FALSE);
+#else
+    if (visible) {
+      gtk_widget_show(widget);
+    } else {
+      gtk_widget_hide(widget);
+    }
+#endif
+  }
+
+  static void window_set_size(GtkWindow *window, int width, int height) {
+#if GTK_MAJOR_VERSION >= 4
+    gtk_window_set_default_size(window, width, height);
+#else
+    gtk_window_resize(window, width, height);
+#endif
+  }
+
+  static void window_set_max_size(GtkWindow *window, int width, int height) {
+// X11-specific features are available in GTK 3 but not GTK 4
+#if GTK_MAJOR_VERSION < 4
+    GdkGeometry g{};
+    g.max_width = width;
+    g.max_height = height;
+    GdkWindowHints h = GDK_HINT_MAX_SIZE;
+    gtk_window_set_geometry_hints(GTK_WINDOW(window), nullptr, &g, h);
+#else
+    // Avoid "unused parameter" warnings
+    (void)window;
+    (void)width;
+    (void)height;
+#endif
+  }
+};
+
+/**
+ * WebKitGTK compatibility helper class.
+ */
+class webkitgtk_compat {
+public:
+#if GTK_MAJOR_VERSION >= 4
+  using wk_handler_js_value_t = JSCValue;
+#else
+  using wk_handler_js_value_t = WebKitJavascriptResult;
+#endif
+
+  using on_script_message_received_t =
+      std::function<void(WebKitUserContentManager *, const std::string &)>;
+  static void
+  connect_script_message_received(WebKitUserContentManager *manager,
+                                  const std::string &handler_name,
+                                  on_script_message_received_t handler) {
+    std::string signal_name = "script-message-received::";
+    signal_name += handler_name;
+
+    auto callback = +[](WebKitUserContentManager *manager,
+                        wk_handler_js_value_t *r, gpointer arg) {
+      auto *handler = static_cast<on_script_message_received_t *>(arg);
+      (*handler)(manager, get_string_from_js_result(r));
+    };
+
+    auto deleter = +[](gpointer data, GClosure *) {
+      delete static_cast<on_script_message_received_t *>(data);
+    };
+
+    g_signal_connect_data(manager, signal_name.c_str(), G_CALLBACK(callback),
+                          new on_script_message_received_t{handler}, deleter,
+                          static_cast<GConnectFlags>(0) /*G_CONNECT_DEFAULT*/);
+  }
 
-using webkit_web_view_run_javascript_t = void (*)(WebKitWebView *,
-                                                  const gchar *, GCancellable *,
-                                                  GAsyncReadyCallback,
-                                                  gpointer);
+  static std::string get_string_from_js_result(JSCValue *r) {
+    char *cs = jsc_value_to_string(r);
+    std::string s{cs};
+    g_free(cs);
+    return s;
+  }
+
+#if GTK_MAJOR_VERSION < 4
+  static std::string get_string_from_js_result(WebKitJavascriptResult *r) {
+#if (WEBKIT_MAJOR_VERSION == 2 && WEBKIT_MINOR_VERSION >= 22) ||               \
+    WEBKIT_MAJOR_VERSION > 2
+    JSCValue *value = webkit_javascript_result_get_js_value(r);
+    return get_string_from_js_result(value);
+#else
+    JSGlobalContextRef ctx = webkit_javascript_result_get_global_context(r);
+    JSValueRef value = webkit_javascript_result_get_value(r);
+    JSStringRef js = JSValueToStringCopy(ctx, value, nullptr);
+    size_t n = JSStringGetMaximumUTF8CStringSize(js);
+    char *cs = g_new(char, n);
+    JSStringGetUTF8CString(js, cs, n);
+    JSStringRelease(js);
+    std::string s{cs};
+    g_free(cs);
+    return s;
+#endif
+  }
+#endif
 
-constexpr auto webkit_web_view_evaluate_javascript =
-    library_symbol<webkit_web_view_evaluate_javascript_t>(
-        "webkit_web_view_evaluate_javascript");
-constexpr auto webkit_web_view_run_javascript =
-    library_symbol<webkit_web_view_run_javascript_t>(
-        "webkit_web_view_run_javascript");
-} // namespace webkit_symbols
+  static void user_content_manager_register_script_message_handler(
+      WebKitUserContentManager *manager, const gchar *name) {
+#if GTK_MAJOR_VERSION >= 4
+    webkit_user_content_manager_register_script_message_handler(manager, name,
+                                                                nullptr);
+#else
+    webkit_user_content_manager_register_script_message_handler(manager, name);
+#endif
+  }
+};
 
 class gtk_webkit_engine : public engine_base {
 public:
   gtk_webkit_engine(bool debug, void *window)
       : m_owns_window{!window}, m_window(static_cast<GtkWidget *>(window)) {
     if (m_owns_window) {
-      if (gtk_init_check(nullptr, nullptr) == FALSE) {
+      if (!gtk_compat::init_check()) {
         return;
       }
-      m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+      m_window = gtk_compat::window_new();
       on_window_created();
-      g_signal_connect(G_OBJECT(m_window), "destroy",
-                       G_CALLBACK(+[](GtkWidget *, gpointer arg) {
-                         auto *w = static_cast<gtk_webkit_engine *>(arg);
-                         // Widget destroyed along with window.
-                         w->m_webview = nullptr;
-                         w->m_window = nullptr;
-                         w->on_window_destroyed();
-                       }),
-                       this);
+      auto on_window_destroy = +[](GtkWidget *, gpointer arg) {
+        auto *w = static_cast<gtk_webkit_engine *>(arg);
+        w->m_window = nullptr;
+        w->on_window_destroyed();
+      };
+       g_signal_connect(G_OBJECT(m_window), "destroy",
+                        G_CALLBACK(on_window_destroy), this);
     }
     webkit_dmabuf::apply_webkit_dmabuf_workaround();
     // Initialize webview widget
     m_webview = webkit_web_view_new();
+    g_object_ref_sink(m_webview);
     WebKitUserContentManager *manager =
         webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_webview));
-    g_signal_connect(manager, "script-message-received::external",
-                     G_CALLBACK(+[](WebKitUserContentManager *,
-                                    WebKitJavascriptResult *r, gpointer arg) {
-                       auto *w = static_cast<gtk_webkit_engine *>(arg);
-                       char *s = get_string_from_js_result(r);
-                       w->on_message(s);
-                       g_free(s);
-                     }),
-                     this);
-    webkit_user_content_manager_register_script_message_handler(manager,
-                                                                "external");
+    webkitgtk_compat::connect_script_message_received(
+        manager, "external",
+        [this](WebKitUserContentManager *, const std::string &r) {
+          on_message(r);
+        });
+    webkitgtk_compat::user_content_manager_register_script_message_handler(
+        manager, "external");
     init("window.external={invoke:function(s){window.webkit.messageHandlers."
          "external.postMessage(s);}}");
 
-    gtk_container_add(GTK_CONTAINER(m_window), GTK_WIDGET(m_webview));
-    gtk_widget_show(GTK_WIDGET(m_webview));
+    gtk_compat::window_set_child(GTK_WINDOW(m_window), GTK_WIDGET(m_webview));
+    gtk_compat::widget_set_visible(GTK_WIDGET(m_webview), true);
 
     WebKitSettings *settings =
         webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_webview));
@@ -1286,7 +1434,7 @@ public:
 
     if (m_owns_window) {
       gtk_widget_grab_focus(GTK_WIDGET(m_webview));
-      gtk_widget_show_all(m_window);
+      gtk_compat::widget_set_visible(GTK_WIDGET(m_window), true);
     }
   }
 
@@ -1296,18 +1444,19 @@ public:
   gtk_webkit_engine &operator=(gtk_webkit_engine &&) = delete;
 
   virtual ~gtk_webkit_engine() {
-    if (m_webview) {
-      gtk_widget_destroy(GTK_WIDGET(m_webview));
-      m_webview = nullptr;
-    }
     if (m_window) {
       if (m_owns_window) {
         // Disconnect handlers to avoid callbacks invoked during destruction.
         g_signal_handlers_disconnect_by_data(GTK_WINDOW(m_window), this);
         gtk_window_close(GTK_WINDOW(m_window));
         on_window_destroyed(true);
+      } else {
+        gtk_compat::window_remove_child(GTK_WINDOW(m_window),
+                                        GTK_WIDGET(m_webview));
       }
-      m_window = nullptr;
+    }
+    if (m_webview) {
+      g_object_unref(m_webview);
     }
     if (m_owns_window) {
       // Needed for the window to close immediately.
@@ -1318,9 +1467,14 @@ public:
   void *window_impl() override { return (void *)m_window; }
   void *widget_impl() override { return (void *)m_webview; }
   void *browser_controller_impl() override { return (void *)m_webview; };
-  void run_impl() override { gtk_main(); }
+  void run_impl() override {
+    m_stop_run_loop = false;
+    while (!m_stop_run_loop) {
+      g_main_context_iteration(nullptr, TRUE);
+    }
+  }
   void terminate_impl() override {
-    dispatch_impl([] { gtk_main_quit(); });
+    dispatch_impl([&] { m_stop_run_loop = true; });
   }
   void dispatch_impl(std::function<void()> f) override {
     g_idle_add_full(G_PRIORITY_HIGH_IDLE, (GSourceFunc)([](void *f) -> int {
@@ -1338,17 +1492,11 @@ public:
   void set_size_impl(int width, int height, webview_hint_t hints) override {
     gtk_window_set_resizable(GTK_WINDOW(m_window), hints != WEBVIEW_HINT_FIXED);
     if (hints == WEBVIEW_HINT_NONE) {
-      gtk_window_resize(GTK_WINDOW(m_window), width, height);
-    } else if (hints == WEBVIEW_HINT_FIXED) {
+      gtk_compat::window_set_size(GTK_WINDOW(m_window), width, height);
+    } else if (hints == WEBVIEW_HINT_FIXED || hints == WEBVIEW_HINT_MIN) {
       gtk_widget_set_size_request(m_window, width, height);
-    } else {
-      GdkGeometry g;
-      g.min_width = g.max_width = width;
-      g.min_height = g.max_height = height;
-      GdkWindowHints h =
-          (hints == WEBVIEW_HINT_MIN ? GDK_HINT_MIN_SIZE : GDK_HINT_MAX_SIZE);
-      // This defines either MIN_SIZE, or MAX_SIZE, but not both:
-      gtk_window_set_geometry_hints(GTK_WINDOW(m_window), nullptr, &g, h);
+    } else if (hints == WEBVIEW_HINT_MAX) {
+      gtk_compat::window_set_max_size(GTK_WINDOW(m_window), width, height);
     }
   }
 
@@ -1372,24 +1520,24 @@ public:
   }
 
   void eval_impl(const std::string &js) override {
-    auto &lib = get_webkit_library();
-    auto wkmajor = webkit_get_major_version();
-    auto wkminor = webkit_get_minor_version();
-    if ((wkmajor == 2 && wkminor >= 40) || wkmajor > 2) {
-      if (auto fn =
-              lib.get(webkit_symbols::webkit_web_view_evaluate_javascript)) {
-        fn(WEBKIT_WEB_VIEW(m_webview), js.c_str(),
-           static_cast<gssize>(js.size()), nullptr, nullptr, nullptr, nullptr,
-           nullptr);
-      }
-    } else if (auto fn =
-                   lib.get(webkit_symbols::webkit_web_view_run_javascript)) {
-      fn(WEBKIT_WEB_VIEW(m_webview), js.c_str(), nullptr, nullptr, nullptr);
-    }
+#if (WEBKIT_MAJOR_VERSION == 2 && WEBKIT_MINOR_VERSION >= 40) ||               \
+    WEBKIT_MAJOR_VERSION > 2
+    webkit_web_view_evaluate_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(),
+                                        static_cast<gssize>(js.size()), nullptr,
+                                        nullptr, nullptr, nullptr, nullptr);
+#else
+    webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(m_webview), js.c_str(),
+                                   nullptr, nullptr, nullptr);
+#endif
   }
 
 private:
-  static char *get_string_from_js_result(WebKitJavascriptResult *r) {
+#if GTK_MAJOR_VERSION >= 4
+  static char *get_string_from_js_result(JSCValue *r) {
+    return jsc_value_to_string(r);
+  }
+#else
+    static char *get_string_from_js_result(WebKitJavascriptResult *r) {
     char *s;
 #if (WEBKIT_MAJOR_VERSION == 2 && WEBKIT_MINOR_VERSION >= 22) ||               \
     WEBKIT_MAJOR_VERSION > 2
@@ -1406,48 +1554,21 @@ private:
 #endif
     return s;
   }
-
-  static const native_library &get_webkit_library() {
-    static const native_library non_loaded_lib;
-    static native_library loaded_lib;
-
-    if (loaded_lib.is_loaded()) {
-      return loaded_lib;
-    }
-
-    constexpr std::array<const char *, 2> lib_names{"libwebkit2gtk-4.1.so",
-                                                    "libwebkit2gtk-4.0.so"};
-    auto found =
-        std::find_if(lib_names.begin(), lib_names.end(), [](const char *name) {
-          return native_library::is_loaded(name);
-        });
-
-    if (found == lib_names.end()) {
-      return non_loaded_lib;
-    }
-
-    loaded_lib = native_library(*found);
-
-    auto loaded = loaded_lib.is_loaded();
-    if (!loaded) {
-      return non_loaded_lib;
-    }
-
-    return loaded_lib;
-  }
+#endif
 
   // Blocks while depleting the run loop of events.
   void deplete_run_loop_event_queue() {
     bool done{};
     dispatch([&] { done = true; });
     while (!done) {
-      gtk_main_iteration();
+      g_main_context_iteration(nullptr, TRUE);
     }
   }
 
   bool m_owns_window{};
   GtkWidget *m_window{};
   GtkWidget *m_webview{};
+  bool m_stop_run_loop{};
 };
 
 } // namespace detail
diff --git a/webview.go b/webview.go
index 7cf0d9fa79fe..615ab7d421bf 100644
--- a/webview.go
+++ b/webview.go
@@ -6,7 +6,7 @@ package webview
 
 #cgo linux openbsd freebsd netbsd CXXFLAGS: -DWEBVIEW_GTK -std=c++11
 #cgo linux openbsd freebsd netbsd LDFLAGS: -ldl
-#cgo linux openbsd freebsd netbsd pkg-config: gtk+-3.0 webkit2gtk-4.0
+#cgo linux openbsd freebsd netbsd pkg-config: gtk4 webkitgtk-6.0
 
 #cgo darwin CXXFLAGS: -DWEBVIEW_COCOA -std=c++11
 #cgo darwin LDFLAGS: -framework WebKit -ldl
