
Godot Platform Web
- 165 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-platform-web for development tasks
About
godot-platform-web: A skill for development. This provides functionality for development workflows.
- godot-platform-web
Godot Platform Web by the numbers
- 165 all-time installs (skills.sh)
- +18 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,345 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-platform-webAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 165 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-platform-web for development tasks
Files
Platform: Web
Browser API integration, LocalStorage persistence, and size optimization define web deployment.
NEVER Do (Expert Web Rules)
Persistence & Storage
- NEVER use FileAccess for persistent saves — Browsers sandbox the filesystem. Standard
FileAccesstouser://is unreliable. Always useJavaScriptBridgeforlocalStorageorIndexedDB. - NEVER assume localStorage is permanent — Browsers may purge local storage if space is low. Always implement a cloud-save fallback for production titles.
Rendering & Logic
- NEVER use the Forward+ renderer — Forward+ requires Vulkan features that are unstable in browsers. Use the Compatibility (WebGL 2.0) renderer for consistent 60 FPS.
- NEVER block the browser event loop — Long-running sync logic will cause the browser to prompt the user to "Kill the Page." Use
awaitand background tasks. - NEVER ignore the COOP/COEP header requirement — Multi-threading and
SharedArrayBufferwill fail on many hosts unless cross-origin isolation is configured server-side.
UX & Security
- NEVER forget to handle tab focus loss — Audio playing in a hidden background tab is poor UX. Use
visibilitychangeto pause audio. - NEVER trigger Fullscreen/Mouse Lock without click — Browsers block security-sensitive requests unless they are inside a direct user interaction event.
- NEVER use absolute paths in HTML shells — Use relative paths to ensure the game works when hosted in sub-directories.
---
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
web_javascript_bridge_callback.gd
Expert two-way JS-to-GD communication using create_callback.
web_local_storage_wrapper.gd
Robust localStorage handler with JSON serialization and quota error prevention.
web_responsive_canvas_adaptor.gd
Dynamic canvas resizing to match browser window dimensions via JS.
web_browser_input_guard.gd
Preventing browser default behaviors (Right-click menu, Spacebar scroll).
web_resource_lazy_loader.gd
Lazy loading of remote PCKs and resources using browser-fetch logic.
web_clipboard_interface.gd
Asynchronous clipboard (Copy/Paste) integration via the Navigator API.
web_visibility_auto_pause.gd
Visibility API integration to auto-pause engine and audio on tab hide.
web_navigation_guard.gd
Navigation guard using beforeunload to prevent closing on unsaved progress.
web_external_url_opener.gd
Expert URL opening using window.open with noopener security flags.
web_performance_profiler.gd
Browser performance tracking (VRAM, Draw calls) logged to JS console.
---
<!-- index.html custom loading -->
<div id="loading-screen">
<div class="progress-bar">
<div id="progress" style="width: 0%"></div>
</div>
<p id="status-text">Loading...</p>
</div>
<script>
const engine = new Engine(CONFIG);
engine.startGame({
onProgress: function(current, total) {
const percent = Math.floor((current / total) * 100);
document.getElementById('progress').style.width = percent + '%';
document.getElementById('status-text').innerText = `Loading ${percent}%`;
}
}).then(() => {
document.getElementById('loading-screen').style.display = 'none';
});
</script>Browser Integration
# Check if running in browser
if OS.has_feature("web"):
# Web-specific code
JavaScriptBridge.eval("console.log('Running in browser')")LocalStorage Save
func save_to_browser() -> void:
if not OS.has_feature("web"):
return
var data := JSON.stringify(get_save_data())
JavaScriptBridge.eval("localStorage.setItem('savegame', '%s')" % data)
func load_from_browser() -> Dictionary:
if not OS.has_feature("web"):
return {}
var data_str := JavaScriptBridge.eval("localStorage.getItem('savegame')")
if data_str:
return JSON.parse_string(data_str)
return {}Size Optimization
# Minimize build size
[rendering]
textures/vram_compression/import_s3tc_bptc=false
textures/vram_compression/import_etc2_astc=true
# Exclude unnecessary exports
[export_preset]
exclude_filter="*.md,*.txt,docs/*"Performance
- Target 60 FPS on mid-range browsers
- Limit godot-particles - WebGL has lower limits
- Reduce draw calls
- Avoid large textures
Best Practices
1. Loading Screen - Users expect feedback 2. File Size - Keep under 50MB 3. Mobile Web - Test on phones 4. HTTPS - Required for many APIs
1. PWA Update Lifecycle
Godot supports Progressive Web Apps (PWA) natively. Use the pwa_update_available signal to notify players of new versions and force a live reload using pwa_update().
func _ready() -> void:
if OS.has_feature("web"):
# Detect new PWA version waiting to be activated.
JavaScriptBridge.pwa_update_available.connect(_on_pwa_update)
func _on_pwa_update() -> void:
if JavaScriptBridge.pwa_needs_update():
# Force the new version to install and reload all browser tabs.
JavaScriptBridge.pwa_update()2. WebGPU Beta-Feature Status
Godot 4.x targets WebGL 2.0 via the Compatibility renderer. While WebGPU is the future of web rendering, it is currently unsupported in Godot 4.x. To maximize performance, use the Compatibility renderer and enable VRAM texture compression (S3TC/BPTC for desktop browsers).
3. JSON-RPC Bridge (Browser Communication)
Create a structured, bidirectional communication bridge between Godot and the browser using the JSONRPC and JavaScriptBridge classes.
class_name WebRPCBridge extends Node
## Facilitates JSON-RPC communication between Godot and the Browser.
var _json_rpc: JSONRPC = JSONRPC.new()
var _js_callback: JavaScriptObject
func _ready() -> void:
if not OS.has_feature("web"): return
_json_rpc.set_method("update_score", _on_score_update)
# Wrap GDScript callable for JS. Reference must be kept in class scope.
_js_callback = JavaScriptBridge.create_callback(_on_js_message)
# Inject into global window object.
var window := JavaScriptBridge.get_interface("window")
window.sendToGodot = _js_callback
func _on_js_message(args: Array) -> void:
var json_payload: String = args[0]
var parsed: Variant = JSON.parse_string(json_payload)
if parsed is Dictionary:
var response = _json_rpc.process_action(parsed)
if response: _send_to_browser(JSON.stringify(response))
func _on_score_update(params: Variant) -> Variant:
# Handle logic...
return {"status": "ok"}
func _send_to_browser(json_str: String) -> void:
var js := "if (window.onGodotMessage) { window.onGodotMessage('%s'); }" % json_str.json_escape()
JavaScriptBridge.eval(js)Reference
- Related:
godot-export-builds,godot-platform-mobile
Related
- Master Skill: godot-master
# platform_web_patterns.gd
extends Node
# 1. Checking Web Browser Context
# EXPERT NOTE: Simple check to apply web-specific optimizations.
func is_web_runtime() -> bool:
return OS.has_feature(&"web")
# 2. Safe Fullscreen Request
# EXPERT NOTE: Browsers block fullscreen/mouse-lock unless initiated by a direct user input event.
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed(&"toggle_fullscreen"):
# This occurs within an input call, satisfying the user-activation requirement.
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
# 3. Asynchronous HTTP Data Fetching
# EXPERT NOTE: Low-level TCP is forbidden in browsers. HTTPRequest is the safeFetch abstraction.
func fetch_api_data(url: String) -> void:
var http := HTTPRequest.new()
add_child(http)
http.request_completed.connect(func(r, c, h, b):
print("Data received: ", b.get_string_from_utf8())
http.queue_free()
)
http.request(url)
# 4. Checking WebXR Availability
# EXPERT NOTE: Not all desktop browsers support WebXR yet. Check supports signal.
func check_web_xr_support() -> void:
var webxr := XRServer.find_interface("WebXR") as WebXRInterface
if webxr:
webxr.session_supported.connect(_on_webxr_supported)
webxr.is_session_supported("immersive-vr")
func _on_webxr_supported(session_type: String, supported: bool) -> void:
print("XR Session ", session_type, " supported: ", supported)
# 5. Handling WebXR Input Modalities
# EXPERT NOTE: Identify if input is gaze-based or a tracked pointer.
func process_webxr_input(source_id: int) -> void:
var webxr := XRServer.find_interface("WebXR") as WebXRInterface
if webxr:
var mode := webxr.get_input_source_target_ray_mode(source_id)
if mode == WebXRInterface.TARGET_RAY_MODE_TRACKED_POINTER:
print("Tracked controller active.")
# 6. Lowering Visuals Dynamically for WebGL 2.0
# EXPERT NOTE: Disable expensive TAA/MSAA if the browser environment is struggling.
func optimize_visuals_for_web() -> void:
if OS.has_feature(&"web"):
get_viewport().use_taa = false
get_viewport().msaa_3d = Viewport.MSAA_DISABLED
# 7. Safe Async Clipboard Usage
# EXPERT NOTE: Requires secure context (HTTPS). Check feature flag before use.
func copy_to_web_clipboard(text: String) -> void:
if OS.has_feature(&"web") and DisplayServer.has_feature(DisplayServer.FEATURE_CLIPBOARD):
DisplayServer.clipboard_set(text)
# 8. Handling Apple Web (iOS Safari)
# EXPERT NOTE: iOS Safari has specific quirks (like audio context locks).
func is_safari_ios() -> bool:
return OS.has_feature(&"web_ios")
# 9. Establishing WebSocket Connectivity
# EXPERT NOTE: The ONLY viable real-time multiplayer protocol for standard web exports.
func connect_via_websocket(url: String) -> void:
var peer := WebSocketMultiplayerPeer.new()
if peer.create_client(url) == OK:
multiplayer.multiplayer_peer = peer
# 10. Programmatic Canvas Resize Policy
# EXPERT NOTE: Control how the HTML canvas stretches within the browser container.
func set_web_canvas_policy() -> void:
# 0 = Proportional, 1 = Full window, 2 = Programmatic
ProjectSettings.set_setting("html/canvas_resize_policy", 2)
# skills/platform-web/scripts/web_bridge_sync.gd
extends Node
## Web Bridge Sync Expert Pattern
## JavaScriptBridge helpers for browser API integration (fullscreen, persistence, analytics).
class_name WebBridgeSync
# Persistence
static func save_to_local_storage(key: String, data: Dictionary) -> void:
if not OS.has_feature("web"):
return
var json_str = JSON.stringify(data)
# Encode to base64 to avoid character issues in JS string
var b64_str = Marshalls.utf8_to_base64(json_str)
var js_code = "localStorage.setItem('%s', '%s');" % [key, b64_str]
JavaScriptBridge.eval(js_code)
static func load_from_local_storage(key: String) -> Dictionary:
if not OS.has_feature("web"):
return {}
# JavaScriptBridge.eval returns the value directly
var b64_str = JavaScriptBridge.eval("localStorage.getItem('%s');" % key)
if b64_str and b64_str is String:
var json_str = Marshalls.base64_to_utf8(b64_str)
var result = JSON.parse_string(json_str)
if result:
return result
return {}
# Browser Interaction
static func set_tab_title(title: String) -> void:
if OS.has_feature("web"):
JavaScriptBridge.eval("document.title = '%s';" % title)
# Analytics Hook (e.g. Google Analytics)
static func send_analytics_event(event_name: String, params: Dictionary = {}) -> void:
if OS.has_feature("web"):
# Ensure gtag is defined in index.html, safeguard against missing window.gtag
# We construct a JS function call dynamically
var json_params = JSON.stringify(params)
var js = "if(typeof gtag !== 'undefined') { gtag('event', '%s', %s); }" % [event_name, json_params]
JavaScriptBridge.eval(js)
## EXPERT USAGE:
## if OS.has_feature("web"): WebBridgeSync.save_to_local_storage("save1", data)
class_name WebBrowserInputGuard
extends Node
## Expert input guard to prevent browser default behaviors.
## Disables context menu (right-click) and spacebar scrolling.
func _ready() -> void:
if not OS.has_feature("web"): return
JavaScriptBridge.eval("""
window.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('keydown', function(e) {
if([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) {
e.preventDefault();
}
}, false);
""")
## Rule: Only disable defaults if your game fully handles these inputs.
class_name WebClipboardInterface
extends Node
## Expert browser clipboard integration via Navigator API.
## Handles asynchronous Copy and Paste operations.
func copy_text(text: String) -> void:
if not OS.has_feature("web"): return
JavaScriptBridge.eval("""
navigator.clipboard.writeText('%s').then(() => {
console.log('Web: Text copied to clipboard');
});
""" % text)
func paste_text_async(callback_obj: Object, callback_method: String) -> void:
# Paste requires explicit user permission check in browsers
var js_callback := JavaScriptBridge.create_callback(func(args):
callback_obj.call(callback_method, args[0])
)
var window := JavaScriptBridge.get_interface("window")
window.pasteToGodot(js_callback)
## Rule: 'navigator.clipboard' requires a secure (HTTPS) context.
class_name WebExternalURLOpener
extends Node
## Expert utility to open external URLs from a web export.
## Ensures 'noopener' and 'noreferrer' are used for security.
func open_url(url: String, new_tab: bool = true) -> void:
if not OS.has_feature("web"):
OS.shell_open(url)
return
var target := "_blank" if new_tab else "_self"
JavaScriptBridge.eval("window.open('%s', '%s', 'noopener,noreferrer');" % [url, target])
## Rule: Most browsers block window.open unless triggered by a click/keypress.
class_name WebJavaScriptBridgeCallback
extends Node
## Expert two-way communication between GDScript and JavaScript.
## Demonstrates create_callback for receiving async data from browser APIs.
func _ready() -> void:
if not OS.has_feature("web"): return
# Create a persistent callback to JS
var js_callback := JavaScriptBridge.create_callback(_on_js_called)
# Pass the callback to a JS function (e.g., a custom analytic or login hook)
var window := JavaScriptBridge.get_interface("window")
if window:
window.registerGodotCallback(js_callback)
func _on_js_called(args: Array) -> void:
var message = args[0]
print("Web: Received message from JavaScript: ", message)
## Rule: Always keep a reference to 'js_callback' to prevent garbage collection.
class_name WebLocalStorageWrapper
extends Node
## Expert wrapper for browser localStorage.
## Features JSON serialization, error handling, and quota checks.
func save_data(key: String, value: Variant) -> bool:
if not OS.has_feature("web"): return false
var json_str := JSON.stringify(value)
var storage := JavaScriptBridge.get_interface("localStorage")
try:
storage.setItem(key, json_str)
return true
except: # Handles QuotaExceededError
push_error("WebLocalStorage: Storage quota exceeded or blocked.")
return false
func load_data(key: String) -> Variant:
var storage := JavaScriptBridge.get_interface("localStorage")
var data = storage.getItem(key)
if data:
return JSON.parse_string(data)
return null
## Rule: Browsers may wipe localStorage; never use it for mission-critical core data.
class_name WebNavigationGuard
extends Node
## Expert navigation guard for web games with unsaved state.
## Triggers a browser confirmation dialog if the user tries to close the tab.
func set_unsaved_changes(has_changes: bool) -> void:
if not OS.has_feature("web"): return
if has_changes:
JavaScriptBridge.eval("""
window.onbeforeunload = function() {
return "You have unsaved changes. Are you sure you want to leave?";
};
""")
else:
JavaScriptBridge.eval("window.onbeforeunload = null;")
## Rule: Only enable this during active gameplay or editing sessions.
class_name WebPerformanceProfiler
extends Node
## Expert performance profiler for WebGL/WebGPU.
## Logs memory and draw calls to the browser console.
func log_profle_data() -> void:
if not OS.has_feature("web"): return
var draw_calls := Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
var memory := OS.get_static_memory_usage() / 1024 / 1024
JavaScriptBridge.eval("console.log('Godot Performance | Draw Calls: %d | Memory: %dMB');" % [draw_calls, memory])
## Tip: Keep draw calls under 500 for stable 60FPS on mid-range mobile browsers.
class_name WebResourceLazyLoader
extends Node
## Expert lazy loading of remote Godot resources/PCKs in the browser.
## Uses HTTPRequest but with browser cache awareness.
func load_remote_pck(url: String) -> void:
var http := HTTPRequest.new()
add_child(http)
http.request_completed.connect(_on_pck_downloaded)
http.request(url)
func _on_pck_downloaded(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
if result == HTTPRequest.RESULT_SUCCESS and code == 200:
# ProjectSettings.load_resource_pack is expert for post-launch content
ProjectSettings.load_resource_pack(body)
print("Web: Remote PCK loaded successfully.")
class_name WebResponsiveCanvasAdaptor
extends Node
## Expert canvas resize management for responsive web games.
## Dynamically updates the HTML canvas size via JavaScriptBridge.
func _ready() -> void:
get_window().size_changed.connect(_update_canvas_size)
func _update_canvas_size() -> void:
if not OS.has_feature("web"): return
# Force browser canvas to match window inner dimensions
JavaScriptBridge.eval("""
var canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
""")
## Tip: Set 'Stretch Mode' to 'canvas_items' and 'Aspect' to 'expand' in Project Settings.
class_name WebVisibilityAutoPause
extends Node
## Expert tab visibility manager for Web exports.
## Automatically pauses the engine and audio when the tab is hidden.
func _ready() -> void:
if not OS.has_feature("web"): return
var js_callback := JavaScriptBridge.create_callback(_on_visibility_changed)
JavaScriptBridge.eval("""
document.addEventListener('visibilitychange', function() {
window.onVisibilityChange(document.hidden);
});
""")
var window := JavaScriptBridge.get_interface("window")
window.onVisibilityChange = js_callback
func _on_visibility_changed(args: Array) -> void:
var is_hidden: bool = args[0]
get_tree().paused = is_hidden
AudioServer.set_bus_mute(0, is_hidden)
## Rule: Always pause audio on visibility change to respect browser user experience.