HTML5 Features

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, Jakarta Servlet 6 on the server side, and Elemental2 1.3.2, checked on 2026-10-08. The classes below are client code, compiled to JavaScript and running in the browser, with one exception: the typed-array interfaces live in a shared package and run on the JVM too, so a binary format written once can be encoded in the browser and decoded on the server (or the other way round). There is nothing special to configure in the build: the com.google.gwt.user.User module already inherits the storage, geolocation, media and canvas modules, and (through the XMLHttpRequest module) the typed-array module. Every example was compiled with the GWT compiler and run in headless Google Chrome 155 against a scratch Bookshelf application, and the output blocks are what that run printed. The section "What was run and what was not" says exactly which parts could not be exercised (a real GPS, a permission prompt, other browsers).

What GWT ships for HTML5

The Developer Guide dates its HTML5 support to GWT 2.3, and its HTML5 page lists four families: client-side storage, canvas, audio and video. The 2.13.1 jar contains two more, geolocation and typed arrays, which have Javadoc but no Developer Guide page. Each family is a small, separate GWT module with its own detection logic, because a browser may lack any of them.

Table 1. HTML5 APIs in gwt-user 2.13.1
Feature Classes Module Detection

Web Storage

com.google.gwt.storage.client: Storage, StorageMap, StorageEvent, StorageEvent.Handler

com.google.gwt.storage.Storage (part of User)

Storage.isLocalStorageSupported(), isSessionStorageSupported(), isSupported(), or a null from getLocalStorageIfSupported() / getSessionStorageIfSupported()

Canvas

com.google.gwt.canvas.client.Canvas (the widget); com.google.gwt.canvas.dom.client: Context2d, CssColor, ImageData, CanvasGradient, CanvasPattern, TextMetrics

com.google.gwt.canvas.Canvas (part of UI, so of User)

Canvas.isSupported(), Canvas.createIfSupported(); compile-time property canvasElementSupport

Audio and video

com.google.gwt.media.client: Audio, Video, MediaBase; com.google.gwt.dom.client: AudioElement, VideoElement, MediaElement

com.google.gwt.media.Media (part of User)

Audio.isSupported(), Video.isSupported(), createIfSupported(); compile-time properties audioElementSupport, videoElementSupport

Typed arrays

com.google.gwt.typedarrays.shared: TypedArrays, ArrayBuffer, ArrayBufferView, DataView, DataViewStream, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array

com.google.gwt.typedarrays.TypedArrays (inherited by the XMLHttpRequest module, hence by User)

TypedArrays.isSupported()

Geolocation

com.google.gwt.geolocation.client: Geolocation, Geolocation.PositionOptions, Position, Position.Coordinates, PositionError

com.google.gwt.geolocation.Geolocation (part of User)

Geolocation.isSupported(), Geolocation.getIfSupported()

The Javadoc of Storage, StorageMap, StorageEvent, Geolocation, Audio, Video and MediaBase still carries the warning "Experimental API: This API is still under development and is subject to change" in the 2.13.1 sources. Treat these wrappers as stable but frozen: the pieces they lack (see "When to use Elemental2 instead") are filled by other libraries, not by new GWT classes.

Two things are not in GWT’s HTML5 set and need Elemental2 or a hand-written overlay: WebGL (Canvas.getContext("webgl") returns a generic Context with no methods to call; there is no WebGL class in gwt-user) and anything newer than the classes above (IndexedDB, Web Workers, fetch, the Clipboard and Notification APIs, MediaRecorder, BroadcastChannel).

Feature detection

A GWT application is compiled once and runs on browsers it has never seen, so every HTML5 class answers the question "is this available?" before it is used. GWT does this in two layers, and it is worth knowing which layer a given class uses.

flowchart TD call["Canvas.createIfSupported()"] --> ct{"Compile-time property
canvasElementSupport"} ct -- "no (set in module XML)" --> nul["returns null
drawing code is pruned"] ct -- "maybe (default)" --> rt{"Runtime probe
on a fresh canvas element"} rt -- "getContext missing" --> nul2["returns null"] rt -- "present" --> ok["returns a Canvas widget"] nul --> fb["caller shows a fallback
table or image"] nul2 --> fb
Table 2. Detection layers per feature
Feature Compile-time layer Runtime layer

Canvas

property canvasElementSupport (maybe or no, default maybe), consumed through a deferred-binding replacement of an internal detector class

createIfSupported() and isSupported() create a canvas element and test it; Canvas.wrap(CanvasElement) returns null when the browser has no canvas

Audio

property audioElementSupport (maybe or no, default maybe)

!!element.canPlayType on a fresh AudioElement

Video

property videoElementSupport (maybe or no, default maybe)

!!element.canPlayType on a fresh VideoElement

Storage

none

tries setItem and removeItem of a dummy key on window.localStorage or window.sessionStorage inside a try/catch; the result is cached when Storage is first loaded

Geolocation

none

!!navigator.geolocation, computed once

Typed arrays

none; the module always replaces the internal NativeImpl with NativeImplFull, whose Javadoc says it is for browsers with full native implementations

TypedArrays.isSupported() combines mightBeSupported() and runtimeSupportCheck(); on the JVM the pure-Java implementation is always available

The Javadoc of @PartialSupport states the convention: a class carrying the annotation provides a static isSupported() and a static createIfSupported() that returns the feature or null. Canvas, Audio, Video, Storage and Geolocation follow it, with the getXxxIfSupported spelling for storage and geolocation. The Storage check is a real probe rather than a property test because a browser can have localStorage and still refuse to use it: a document in a sandboxed frame without allow-same-origin, or a profile that blocks site data, makes the property getter throw a SecurityError (measured for the sandboxed case: "Failed to read the 'localStorage' property from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.").

The first example is the support report the lab prints, and the two sets of values a run produced.

Reporting what the browser supports (Html5Lab.java)
private void support() {
  log("Storage.isLocalStorageSupported=" + Storage.isLocalStorageSupported());
  log("Storage.isSessionStorageSupported=" + Storage.isSessionStorageSupported());
  log("Storage.isSupported=" + Storage.isSupported());
  log("Canvas.isSupported=" + Canvas.isSupported());
  log("Audio.isSupported=" + Audio.isSupported());
  log("Video.isSupported=" + Video.isSupported());
  log("Geolocation.isSupported=" + Geolocation.isSupported());
  log("TypedArrays.isSupported=" + TypedArrays.isSupported());
}
Output in Chrome 155, normal run
Storage.isLocalStorageSupported=true
Storage.isSessionStorageSupported=true
Storage.isSupported=true
Canvas.isSupported=true
Audio.isSupported=true
Video.isSupported=true
Geolocation.isSupported=true
TypedArrays.isSupported=true

Storage.isSupported() is true only when both local and session storage work. To simulate a browser that blocks storage, a script injected before the page defined window.localStorage and window.sessionStorage as getters that throw a SecurityError (the error a browser raises when site data is blocked). This is a simulation of the failure, not a real "cookies blocked" profile:

Output with storage access made to throw a SecurityError
Storage.isLocalStorageSupported=false
Storage.isSessionStorageSupported=false
Storage.isSupported=false
Canvas.isSupported=true
...
storage: unavailable, reading list disabled

Switching a feature off at compile time

The three element-support properties can be set in a module XML file. A module that inherits the lab and turns canvas, audio and video off:

Html5LabLite.gwt.xml
<module rename-to="html5lite">
  <inherits name="com.example.bookshelf.html5.Html5Lab" />
  <set-property name="canvasElementSupport" value="no" />
  <set-property name="audioElementSupport" value="no" />
  <set-property name="videoElementSupport" value="no" />
</module>

Compiled with the same flags, the same entry point, and the same two permutations (gecko1_8, safari), the lite module produced a smaller script:

Table 3. Compiled permutation sizes (OBF style, bytes, uncompressed)
Module Permutation 1 Permutation 2

Html5Lab (all features)

78,745

79,236

Html5LabLite (canvas, audio and video set to no)

70,397

69,913

That is roughly 8.4 KB (about 10 per cent) less for a lab that still contains the code that calls Canvas.createIfSupported(), Audio.createIfSupported() and Video.createIfSupported(): the difference suggests that the compiler dropped the drawing and player code behind the three factory methods. The numbers are for this small lab, not a prediction. Run in Chrome, which does support all three, the lite build reported:

Canvas.isSupported=false
Audio.isSupported=false
Video.isSupported=false
Geolocation.isSupported=true
TypedArrays.isSupported=true

The property overrides the browser: use it for a build that deliberately has no canvas or media (a kiosk with a text-only profile, or a size-constrained widget), never as a way to "turn canvas on".

Web Storage: Storage and StorageMap

com.google.gwt.storage.client.Storage is a final class that wraps one of the two areas of the HTML Web Storage standard. It stores strings under string keys, and nothing else: numbers, booleans and objects must be converted to strings first, and getItem returns null for a key that is not there. The two areas differ in lifetime and sharing.

Table 4. localStorage and sessionStorage
Storage.getLocalStorageIfSupported() Storage.getSessionStorageIfSupported()

Lifetime

stays until the user or the application deletes it

ends when the tab or window that created it is closed (it survives a reload)

Shared with

every tab and window of the same origin in the same browser profile

only the tab that created it

Typical Bookshelf use

the reader’s reading list, a cached catalogue page, a language choice

a half-written review, the current filter of the catalogue

The API of Storage is getItem(key), setItem(key, value), removeItem(key), clear(), key(index) and getLength(). key(index) is how you enumerate; there is no iterator, and the order of keys is browser defined. StorageMap is the answer to that: new StorageMap(storage) exposes a storage area as a java.util.Map<String, String> (it extends AbstractMap), so containsKey, containsValue, keySet(), entrySet() and iterator remove() work, and put returns the previous value. It is a live view, not a copy: it reads and writes the real storage on every call. The Javadoc states two restrictions: neither keys nor values may be null, and all are strings.

A reading list in localStorage

The Bookshelf reading list keeps the titles a reader has saved under a key prefix, bookshelf.reading., followed by the book id. The prefix matters: localStorage belongs to the whole origin, so a prefix keeps the application’s keys apart from every other script’s, and lets the list delete only its own entries.

ReadingList.java
import com.google.gwt.storage.client.Storage;
import com.google.gwt.storage.client.StorageMap;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/** The reader's reading list, kept in localStorage under the key prefix {@code bookshelf.reading.}. */
public class ReadingList {
  static final String PREFIX = "bookshelf.reading.";

  private final Storage store;
  private final StorageMap map;

  private ReadingList(Storage store) {
    this.store = store;
    this.map = new StorageMap(store);
  }

  /** Returns the list, or {@code null} when localStorage is missing or blocked. */
  public static ReadingList create() {
    Storage local = Storage.getLocalStorageIfSupported();
    return local == null ? null : new ReadingList(local);
  }

  public void add(long bookId, String title) {
    store.setItem(PREFIX + bookId, title);
  }

  public boolean contains(long bookId) {
    return map.containsKey(PREFIX + bookId);
  }

  public List<String> titles() {
    List<String> titles = new ArrayList<>();
    for (int i = 0; i < store.getLength(); i++) {
      String key = store.key(i);
      if (key.startsWith(PREFIX)) {
        titles.add(store.getItem(key));
      }
    }
    return titles;
  }

  /** Removes only this application's keys; Storage.clear() would remove every key of the origin. */
  public int clear() {
    int removed = 0;
    for (Iterator<Map.Entry<String, String>> it = map.entrySet().iterator(); it.hasNext(); ) {
      if (it.next().getKey().startsWith(PREFIX)) {
        it.remove();
        removed++;
      }
    }
    return removed;
  }
}

The lab ran this sequence (the output also shows the events described below):

ReadingList list = ReadingList.create();
list.add(1, "Don Quijote de la Mancha");
list.add(2, "La Regenta");
list.add(1, "Don Quijote (edicion ilustrada)");
log("titles=" + list.titles());
log("contains(2)=" + list.contains(2) + " contains(3)=" + list.contains(3));
Storage.getLocalStorageIfSupported().setItem("other.app.key", "x");
log("clear removed=" + list.clear() + " left=" + Storage.getLocalStorageIfSupported().getLength());
Output
titles=[La Regenta, Don Quijote (edicion ilustrada)]
contains(2)=true contains(3)=false
length=2
clear removed=2 left=1

The order of titles() is not the order of insertion: the browser enumerated bookshelf.reading.2 before bookshelf.reading.1. clear() on the list removed exactly its own two keys and left other.app.key alone, which Storage.clear() would not have. The StorageMap iterator removal worked, and put(key, null) or put(null, value) raised java.lang.NullPointerException in the browser, as the Javadoc promises. getItem of a missing key returned null, and a number written with String.valueOf came back as the string "42".

A review draft in sessionStorage

A draft that must survive a reload but not a closed tab belongs in sessionStorage. Objects need serialising; the example uses GWT’s JSON classes, which live in a module that User does not inherit (<inherits name="com.google.gwt.json.JSON" /> is required, otherwise the compiler reports "No source code is available for type com.google.gwt.json.client.JSONObject; did you forget to inherit a required module?"). For typed shared code, GWT’s AutoBeans or a hand-written codec are alternatives; the Developer Guide suggests RequestFactory#getSerializer() for proxies and JSON for other objects.

ReviewDraft.java
import com.google.gwt.json.client.JSONNumber;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.storage.client.Storage;

/** An unsent review, kept in sessionStorage so that it survives a reload but not a closed tab. */
public class ReviewDraft {
  private static final String KEY = "bookshelf.draft";

  private final Storage session = Storage.getSessionStorageIfSupported();

  public boolean save(long bookId, int stars, String text) {
    if (session == null) {
      return false;
    }
    JSONObject json = new JSONObject();
    json.put("bookId", new JSONNumber(bookId));
    json.put("stars", new JSONNumber(stars));
    json.put("text", new JSONString(text));
    session.setItem(KEY, json.toString());
    return true;
  }

  /** Returns the saved draft text, or {@code null} when there is none or it cannot be parsed. */
  public String load() {
    String raw = session == null ? null : session.getItem(KEY);
    if (raw == null) {
      return null;
    }
    try {
      JSONObject json = JSONParser.parseStrict(raw).isObject();
      return json == null ? null : json.get("text").isString().stringValue();
    } catch (RuntimeException e) {
      session.removeItem(KEY);
      return null;
    }
  }
}
Output
draft saved=true
draft text=Slow start, great ending <b>x</b>
corrupt draft -> null, key kept=false

The draft text contains markup on purpose. It comes back byte for byte, and it is data, not HTML: it must be shown with Label.setText(...) or TextArea.setText(...), never concatenated into HTML.setHTML(...). A value read from storage is as untrusted as a value typed by a user, because any script on the same origin (including an injected one) can write to it. After the "not json" value was stored, load() returned null, removed the bad key, and the key was gone.

Storage events

Storage.addStorageEventHandler(StorageEvent.Handler) registers a handler and returns a HandlerRegistration; the handler’s single method is onStorageChange(StorageEvent). The event has getKey(), getOldValue(), getNewValue(), getUrl() and getStorageArea(). Note that addStorageEventHandler is a static method of Storage: the Developer Guide’s snippet calls it on an instance (stockStore.addStorageEventHandler(...)), which compiles in Java but hides that the handler listens to both areas.

The lab registered a handler and ran the sequence above, and the handler logged one line per write made through GWT:

Storage.addStorageEventHandler(new StorageEvent.Handler() {
  @Override
  public void onStorageChange(StorageEvent event) {
    log("event key=" + event.getKey() + " old=" + event.getOldValue()
        + " new=" + event.getNewValue());
  }
});
Output
event key=bookshelf.reading.1 old=null new=Don Quijote de la Mancha
event key=bookshelf.reading.2 old=null new=La Regenta
event key=bookshelf.reading.1 old=Don Quijote de la Mancha new=Don Quijote (edicion ilustrada)
event key=bookshelf.reading.2 old=La Regenta new=null
event key=bookshelf.reading.1 old=Don Quijote (edicion ilustrada) new=null

This is not the browser’s storage event. Storage.gwt.xml replaces the internal implementation class with StorageImplNonNativeEvents for both remaining user.agent values (gecko1_8 and safari), so in GWT 2.13.1 every browser gets it. That class fires a GWT-made event from setItem, removeItem and clear itself, synchronously, in the window that made the write, and its addStorageEventHandler0 is an empty method, so GWT never listens for the native event. Two tabs of the lab and a raw JavaScript write showed the consequences:

Table 5. Which event reached which tab (Chrome 155)
Write GWT StorageEvent.Handler Native storage listener (Elemental2)

A click handler in tab A calls ReadingList.add(77, ...) through GWT

tab A: one event, immediately; tab B: nothing

tab A: nothing; tab B: one event

A script in tab A calls localStorage.setItem("raw.key", "1") directly

nothing, in either tab

tab A: nothing; tab B: one event

The lab’s tab B logged el2 storage event: key=bookshelf.reading.77 old=null new=Added by button for the first row, and el2 storage event: key=raw.key old=null new=1 for the second. GWT’s handler is therefore a same-tab change notification for writes your own GWT code makes. To learn that another tab changed the reading list (to refresh a badge, or to log the user out everywhere) use a native storage listener, shown in "When to use Elemental2 instead". The Storage Javadoc says the same thing: "Storage events into other windows are not supported." Two more details from the source and the run: removeItem of a key that does not exist still fires an event (key=k old=null new=null was logged), and Storage.clear() fires one event with a null key, null old value and null new value, as the Developer Guide notes.

Limits, errors and what not to store

The Developer Guide gives 5 MB for localStorage and "limited only by system memory" for sessionStorage. In Chrome 155 both areas failed at about the same place: the loop setItem("big" + i, <1,048,576-character string>) succeeded four times and the fifth call threw, for local and for session storage:

try {
  for (int i = 0; i < 50; i++) {
    local.setItem("big" + i, chunk.toString());
    stored++;
  }
} catch (JavaScriptException e) {
  log("quota: failed after " + stored + " chunks: " + e.getClass().getName()
      + " name=" + e.getName() + " description=" + e.getDescription());
}
Output
quota: failed after 4 chunks: com.google.gwt.core.client.JavaScriptException name=QuotaExceededError description=: Failed to execute 'setItem' on 'Storage': Setting the value of 'big4' exceeded the quota.

A failed setItem is therefore a JavaScriptException (a RuntimeException) with getName() equal to QuotaExceededError. Wrap writes of anything large (a cached catalogue, a page of search results) in a try/catch, and treat the cache as optional. The limit is a browser decision (the MDN page on storage quotas lists the numbers per browser); do not design around 5 MB.

Other rules that follow from the specification and the Developer Guide:

  • Storage is synchronous. A large getItem or setItem blocks the browser’s main thread; keep values small, or use IndexedDB through Elemental2 for bulk data.

  • Storage is not encrypted and is not private from scripts. The Developer Guide says it must not hold national ids, card numbers or login credentials. Do not keep session tokens in it either: any cross-site-scripting bug then exposes them. Keep authentication in an HttpOnly cookie, which JavaScript cannot read.

  • Storage is per origin, not per user. On a shared computer the next person sees the reading list unless the application deletes it at logout.

  • Storage is not sent to the server, unlike cookies; if the server needs a value (a language, say), send it explicitly.

  • Storage.setItem has assert key.length() > 0 in the source (the source comment cites a Firefox bug). With assertions off, which is the production default, an empty key is accepted: the lab stored and read back a value under "".

Canvas

com.google.gwt.canvas.client.Canvas is a widget around the <canvas> element, and it cannot be created with new: its constructor is private. Canvas.createIfSupported() returns a new canvas, or null; Canvas.wrap(CanvasElement) wraps an element that is already in the document (the Javadoc requires it to be attached, and the method asserts that) and also returns null when canvas is unsupported. Drawing goes through canvas.getContext2d(), which returns a com.google.gwt.canvas.dom.client.Context2d, an overlay type with the standard 2D methods (fillRect, fillText, drawImage, getImageData, scale, paths, gradients and patterns). canvas.getContext(String) returns the generic Context for other context ids. Colours are passed as a CSS string (setFillStyle("#336699")) or as a CssColor, a FillStrokeStyle made with CssColor.make("#336699") or CssColor.make(r, g, b).

Two sizes exist and they are independent. The coordinate space (setCoordinateSpaceWidth and setCoordinateSpaceHeight) is the number of pixels in the bitmap. The CSS size (setPixelSize, setWidth) is how large the element is laid out. If the coordinate space is smaller than the number of device pixels the canvas covers, the browser scales the bitmap and the drawing looks blurry on a high-density screen. The cure is to multiply the coordinate space by window.devicePixelRatio and scale the context by the same factor, so that drawing code keeps working in CSS pixels. devicePixelRatio is not wrapped by GWT’s Window; the example reads it through Elemental2 (DomGlobal.window.devicePixelRatio).

The Bookshelf copies chart

The catalogue page draws the number of copies per decade as a bar chart.

CopiesChart.java
import com.example.bookshelf.html5.shared.Book;
import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.canvas.dom.client.CssColor;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/** Draws the number of copies per decade as a bar chart. */
public class CopiesChart {
  private static final int WIDTH = 360;
  private static final int HEIGHT = 160;

  /** Returns the canvas, or {@code null} when the browser has no canvas. */
  public static Canvas draw(List<Book> books, double pixelRatio) {
    Canvas canvas = Canvas.createIfSupported();
    if (canvas == null) {
      return null;
    }
    // The coordinate space is the real number of pixels; the CSS size is what the page lays out.
    canvas.setCoordinateSpaceWidth((int) (WIDTH * pixelRatio));
    canvas.setCoordinateSpaceHeight((int) (HEIGHT * pixelRatio));
    canvas.setPixelSize(WIDTH, HEIGHT);

    Map<Integer, Integer> perDecade = new TreeMap<>();
    int max = 1;
    for (Book b : books) {
      int decade = b.getYear() / 10 * 10;
      int total = perDecade.getOrDefault(decade, 0) + b.getCopies();
      perDecade.put(decade, total);
      max = Math.max(max, total);
    }

    Context2d ctx = canvas.getContext2d();
    ctx.scale(pixelRatio, pixelRatio);
    ctx.setFillStyle(CssColor.make("#ffffff"));
    ctx.fillRect(0, 0, WIDTH, HEIGHT);
    ctx.setFillStyle("#336699");
    ctx.setFont("11px sans-serif");
    int slot = WIDTH / perDecade.size();
    int x = 0;
    for (Map.Entry<Integer, Integer> e : perDecade.entrySet()) {
      double h = (HEIGHT - 30) * e.getValue() / (double) max;
      ctx.setFillStyle("#336699");
      ctx.fillRect(x + 8, HEIGHT - 20 - h, slot - 16, h);
      ctx.setFillStyle("#222222");
      ctx.fillText(e.getKey() + "s", x + 8, HEIGHT - 6);
      x += slot;
    }
    return canvas;
  }
}

The caller adds the widget to the page, or shows a fallback (the same numbers as a table) when the method returns null:

Canvas canvas = CopiesChart.draw(books, DomGlobal.window.devicePixelRatio);
if (canvas == null) {
  // no canvas: show the numbers as a table instead
} else {
  RootPanel.get("chart").add(canvas);
}

The lab drew six books and read one pixel back from inside the first bar, with the page at a device-pixel ratio of 1 and again at 2 (the second run used the browser’s device-metrics emulation):

Output at devicePixelRatio 1 and 2
canvas coordinate space=360x160 css=360x160
canvas dpr=1 pixel(css 20,100) rgba=51,102,153,255
canvas toDataUrl prefix=data:image/png;base64, length>1000=true

canvas coordinate space=720x320 css=360x160
canvas dpr=2 pixel(css 20,100) rgba=51,102,153,255

51,102,153 is #336699, so the bar was painted where expected in both resolutions, and the element stayed 360 by 160 CSS pixels while its bitmap doubled. canvas.toDataUrl() and toDataUrl("image/jpeg") return the bitmap as a data URL, for a "download chart" link or an upload. Under the HTML Standard, a canvas that has drawn an image from another origin without CORS permission is "tainted", and reading it back (getImageData, toDataUrl) throws a SecurityError; that was not reproduced here because the lab draws no images.

A canvas is a bitmap, so a screen reader sees nothing in it. Put the same information in text next to it (a table, or an aria-label summary on a wrapper), as Accessibility & ARIA describes; a chart that is only pixels fails users who cannot see it.

Audio and Video

Audio and Video (package com.google.gwt.media.client) are widgets around the <audio> and <video> elements; both extend MediaBase, which extends FocusWidget. Like canvas, they are created with createIfSupported(), which returns null when the element is not supported. Video has a public constructor Video(String src) marked @Deprecated in favour of createIfSupported(), and Audio has none.

MediaBase mirrors the media element API:

  • sources: addSource(url), addSource(url, type) (returns the SourceElement), removeSource, setSrc, getCurrentSrc

  • playback: play(), pause(), load(), setCurrentTime, setVolume, setMuted, setLoop, setPlaybackRate, setAutoplay, setControls, setPreload

  • state: getReadyState, getNetworkState, getDuration, getBuffered, getError, isPaused, hasEnded, isSeeking

  • events (the HasAllMediaHandlers interface): addEndedHandler, addCanPlayThroughHandler, addProgressHandler, addLoadedMetadataHandler

  • getMediaElement() returns the underlying MediaElement (getAudioElement() and getVideoElement() give the typed elements) for anything the wrapper lacks

  • Video adds setPoster, getPoster, getVideoWidth, getVideoHeight

Constants help to avoid typos: AudioElement.TYPE_MP3 (audio/mpeg), TYPE_OGG, TYPE_WAV; VideoElement.TYPE_MP4, TYPE_OGG, TYPE_WEBM; MediaElement.CAN_PLAY_PROBABLY, CAN_PLAY_MAYBE and CANNOT_PLAY (the empty string), PRELOAD_NONE, PRELOAD_METADATA, PRELOAD_AUTO. Two things are not wrapped: there is no method for <track> elements (captions and subtitles), and no promise-returning play(). Add <track> elements through the DOM, as Document.get().createElement("track") and getMediaElement().appendChild(...).

The Bookshelf sample and trailer

The book detail page plays an audio sample of the reading and shows a trailer.

CoverPlayer.java
import com.google.gwt.dom.client.AudioElement;
import com.google.gwt.dom.client.MediaElement;
import com.google.gwt.dom.client.VideoElement;
import com.google.gwt.event.dom.client.EndedEvent;
import com.google.gwt.event.dom.client.EndedHandler;
import com.google.gwt.media.client.Audio;
import com.google.gwt.media.client.Video;
import com.google.gwt.user.client.ui.Label;

/** Audio and video for the book detail page: an audio sample and a book trailer. */
public class CoverPlayer {
  /** Returns the audio widget, or {@code null} when the browser has no audio element. */
  public static Audio sample(String wavUrl, final Label status) {
    Audio audio = Audio.createIfSupported();
    if (audio == null) {
      return null;
    }
    audio.addSource(wavUrl, AudioElement.TYPE_WAV);
    audio.setControls(true);
    audio.setPreload(MediaElement.PRELOAD_METADATA);
    audio.addEndedHandler(new EndedHandler() {
      @Override
      public void onEnded(EndedEvent event) {
        status.setText("ended");
      }
    });
    return audio;
  }

  public static Video trailer(String webmUrl, String posterUrl) {
    Video video = Video.createIfSupported();
    if (video == null) {
      return null;
    }
    video.addSource(webmUrl, VideoElement.TYPE_WEBM);
    video.setPoster(posterUrl);
    video.setControls(true);
    video.setMuted(true);
    return video;
  }

  /** "", "maybe" or "probably" for each container/codec pair. */
  public static String canPlay(MediaElement element, String type) {
    String answer = element.canPlayType(type);
    return answer.isEmpty() ? "no" : answer;
  }
}

A browser decides what it can decode, and answers canPlayType with "", "maybe" or "probably". The lab asked Chrome 155 (Google Chrome on macOS, which includes the proprietary codecs) about a few types:

canPlayType in Chrome 155
audio canPlayType wav=maybe mp3=probably ogg=maybe ogg+opus=probably
video canPlayType webm=maybe vp9=probably mp4=maybe h264=probably

Other browsers and open-source Chromium builds may answer differently (not tested), so list several <source> elements in order of preference (webm and mp4, ogg and mp3) and let the browser choose; GWT’s addSource(url, type) does exactly that. Do not branch on the browser name.

The sample used a half-second WAV file the lab generated, and the trailer a 160 by 90 WebM clip recorded in Chrome with MediaRecorder. Both played:

State read from the media elements
audio initial {"paused":true,"rs":4,"dur":0.5,"err":null,"controls":true,"preload":"metadata","src":"http://127.0.0.1:18913/sample.wav","sources":1,"type":"audio/wav"}
video state {"paused":false,"rs":4,"w":160,"h":90,"t":true,"err":null,"poster":"http://127.0.0.1:18913/poster.png","muted":true,"controls":true,"type":"video/webm"}
after real click: status label = "ended"  ended= true

The probe abbreviated the property names: rs is readyState (4 is HAVE_ENOUGH_DATA), dur the duration in seconds, err the media error code, w and h the intrinsic video size, and t is true when currentTime was greater than zero.

Autoplay and the void play()

Browsers refuse to start sound without a user gesture. MediaBase.play() returns void, so GWT code never sees the browser’s answer. The lab called play() on the audio element from a script that was not a user gesture, and then through a real mouse click on a GWT button:

Call Result in Chrome 155

audio.play() from a script, no gesture

the browser’s promise rejected with NotAllowedError: play() failed because the user didn't interact with the document first.; the element stayed paused. A GWT caller does not see this.

video.play() on a muted video, no gesture

the promise resolved and the video played: muted autoplay is allowed

audio.play() in the click handler of a GWT button, dispatched by a real mouse event

the sample played, and after half a second EndedHandler ran: the status label read ended

So start audio only inside a click or key handler, mute videos that must autoplay, and for feedback on a refused play read isPaused() shortly afterwards (or call getMediaElement().play() through Elemental2 and handle its promise).

Media is not accessible by default. Give a video controls (setControls(true)), provide captions through a <track> element, and never rely on sound alone to communicate.

Typed arrays

Typed arrays are fixed-size binary buffers with typed views: an ArrayBuffer holds bytes, and views such as Uint8Array, Int16Array and Float64Array read and write them as numbers. GWT exposes them in com.google.gwt.typedarrays.shared as interfaces plus a static factory, TypedArrays: createArrayBuffer(length), createUint8Array(...), createDataView(buffer), and so on for the nine typed-array views and DataView. In the browser each interface is implemented by the native JavaScript object, so, as the Javadoc warns, you cannot use instanceof on the returned values. On the JVM the factory returns pure-Java implementations (com.google.gwt.typedarrays.server.JavaImpl), which makes the package unusual: code written against it is shared code.

The element and view classes:

Class Elements Notes

ArrayBuffer

bytes (byteLength())

the storage; views share it

Int8Array, Uint8Array, Uint8ClampedArray

8-bit

Uint8ClampedArray clamps stores to 0..255 instead of wrapping

Int16Array, Uint16Array, Int32Array, Uint32Array

16 and 32-bit integers

platform byte order; the buffer length must be a multiple of the element size

Float32Array, Float64Array

IEEE floats

get and set use float and double

DataView

any mix, with an explicit endianness flag

getUint16(byteOffset, littleEndian), setFloat64(...); no alignment rules

DataViewStream

sequential reads over a DataView

keeps an offset; big-endian by default

A binary book record shared by browser and JVM

The Bookshelf label printer wants a compact record of a book: a version byte, the year and copy count as 16-bit integers, the id as a 64-bit float, the title as UTF-8 with a length byte, and a one-byte checksum. DataView handles the mixed layout, and the integer views handle bytes.

BookCodec.java (shared)
import com.google.gwt.typedarrays.shared.ArrayBuffer;
import com.google.gwt.typedarrays.shared.DataView;
import com.google.gwt.typedarrays.shared.TypedArrays;
import com.google.gwt.typedarrays.shared.Uint8Array;
import java.nio.charset.StandardCharsets;

/**
 * Packs a book into a compact binary record: version (u8), year (u16), copies (u16), id (f64),
 * title length (u8), UTF-8 title bytes, checksum (u8). Big-endian.
 */
public final class BookCodec {
  public static final int VERSION = 1;
  private static final int HEADER = 1 + 2 + 2 + 8 + 1;

  private BookCodec() {}

  public static ArrayBuffer encode(Book book) {
    byte[] title = book.getTitle().getBytes(StandardCharsets.UTF_8);
    if (title.length > 255) {
      throw new IllegalArgumentException("title longer than 255 bytes: " + title.length);
    }
    ArrayBuffer buffer = TypedArrays.createArrayBuffer(HEADER + title.length + 1);
    DataView view = TypedArrays.createDataView(buffer);
    view.setUint8(0, VERSION);
    view.setUint16(1, book.getYear());
    view.setUint16(3, book.getCopies());
    view.setFloat64(5, book.getId());
    view.setUint8(13, title.length);
    Uint8Array bytes = TypedArrays.createUint8Array(buffer);
    for (int i = 0; i < title.length; i++) {
      bytes.set(HEADER + i, title[i] & 0xFF);
    }
    bytes.set(buffer.byteLength() - 1, checksum(bytes, buffer.byteLength() - 1));
    return buffer;
  }

  public static Book decode(ArrayBuffer buffer) {
    Uint8Array bytes = TypedArrays.createUint8Array(buffer);
    int last = buffer.byteLength() - 1;
    if (bytes.get(last) != checksum(bytes, last)) {
      throw new IllegalArgumentException("checksum mismatch");
    }
    DataView view = TypedArrays.createDataView(buffer);
    if (view.getUint8(0) != VERSION) {
      throw new IllegalArgumentException("unsupported version " + view.getUint8(0));
    }
    int length = view.getUint8(13);
    byte[] title = new byte[length];
    for (int i = 0; i < length; i++) {
      title[i] = (byte) bytes.get(HEADER + i);
    }
    return new Book((long) view.getFloat64(5), new String(title, StandardCharsets.UTF_8),
        view.getUint16(1), view.getUint16(3));
  }

  public static String toHex(ArrayBuffer buffer) {
    Uint8Array bytes = TypedArrays.createUint8Array(buffer);
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length(); i++) {
      String h = Integer.toHexString(bytes.get(i));
      sb.append(h.length() == 1 ? "0" : "").append(h);
    }
    return sb.toString();
  }

  private static int checksum(Uint8Array bytes, int length) {
    int sum = 0;
    for (int i = 0; i < length; i++) {
      sum = (sum + bytes.get(i)) & 0xFF;
    }
    return sum;
  }
}

The same class ran twice. In the browser, the lab encoded a book with a non-ASCII title, decoded it, and then flipped one bit:

Book in = new Book(42, "Cien años de soledad", 1967, 7);
ArrayBuffer buffer = BookCodec.encode(in);
log("record bytes=" + buffer.byteLength() + " hex=" + BookCodec.toHex(buffer));
Book out = BookCodec.decode(buffer);
Output in Chrome 155
record bytes=36 hex=0107af00074045000000000000154369656e2061c3b16f7320646520736f6c6564616493
decoded=42|Cien años de soledad|1967|7
tampered -> checksum mismatch

Read the hex: 01 version, 07af year 1967, 0007 copies, 4045000000000000 is the double 42.0, 15 is 21 bytes of title (UTF-8, so c3b1 is the one character ñ), and 93 the checksum. On the plain JVM, a JUnit test (JUnit 4, no GWT runtime, gwt-user on the classpath) encoded Don Quijote de la Mancha and decoded it back with the same class and produced the same layout:

hex=0106450003404500000000000018446f6e205175696a6f7465206465206c61204d616e6368614c bytes=39
OK (2 tests)

Because DataView takes the endianness as a flag and the factory is shared, a record written by the browser is readable by a Java servlet and the reverse. The sizes and behaviour that the lab measured:

  • Uint8ClampedArray stored 300 as 255; a Uint8Array stored it as 44 (300 modulo 256).

  • TypedArrays.createFloat32Array(TypedArrays.createArrayBuffer(5)) raised com.google.gwt.core.client.JavaScriptException (RangeError) : byte length of Float32Array should be a multiple of 4. A native RangeError reaches Java as a JavaScriptException.

  • String.getBytes(StandardCharsets.UTF_8) and new String(bytes, StandardCharsets.UTF_8) work in client code (the JRE emulation supports them), so no hand-written UTF-8 is needed.

Where typed arrays enter GWT elsewhere: XMLHttpRequest.setResponseType(ResponseType.ArrayBuffer) followed by getResponseArrayBuffer() receives binary data from the server; that low-level class is what RequestBuilder is built on, and it is not covered by the Developer Guide. For Blob, File, fetch or TextEncoder use Elemental2 (see below): its Uint8Array is a different Java type from GWT’s Uint8Array, so a method written for one does not accept the other.

Geolocation

com.google.gwt.geolocation.client.Geolocation wraps navigator.geolocation. You obtain it with Geolocation.getIfSupported() (or test Geolocation.isSupported() first); it returns null where the browser has no geolocation. The first request makes the browser ask the user for permission; the browser can also refuse silently (a policy, a denied site setting, an insecure page). Browsers expose geolocation only to secure contexts (HTTPS, and localhost or 127.0.0.1 for development), according to the MDN page; the lab ran on 127.0.0.1 and an insecure origin was not tested.

The API takes GWT’s com.google.gwt.core.client.Callback<Position, PositionError>:

  • getCurrentPosition(callback) and getCurrentPosition(callback, options) ask once

  • watchPosition(callback) and watchPosition(callback, options) call back whenever the position changes and return a watch id (-1 if unsupported); clearWatch(id) stops it

  • Geolocation.PositionOptions has a fluent API: setHighAccuracyEnabled(boolean) (default false), setMaximumAge(int) in milliseconds (default 0, meaning no cached position), setTimeout(int) in milliseconds (default -1, meaning no timeout)

  • Position gives getCoordinates() and getTimestamp(); Position.Coordinates gives getLatitude(), getLongitude(), getAccuracy() (metres) and the nullable Double values getAltitude(), getAltitudeAccuracy(), getHeading() and getSpeed()

  • PositionError is a Throwable with getCode() and constants UNKNOWN_ERROR (0), PERMISSION_DENIED (1), POSITION_UNAVAILABLE (2) and TIMEOUT (3)

Nearest branch

The Bookshelf "Find my nearest branch" button asks for the position once, picks the closest branch by great-circle distance (Libraries.nearest(lat, lon), a haversine formula in shared code), and explains every failure instead of leaving the button dead.

NearestBranch.java
import com.example.bookshelf.html5.shared.Libraries;
import com.google.gwt.core.client.Callback;
import com.google.gwt.geolocation.client.Geolocation;
import com.google.gwt.geolocation.client.Position;
import com.google.gwt.geolocation.client.PositionError;

/** Finds the library branch closest to the reader. */
public class NearestBranch {
  /** Receives the branch name or an explanation. */
  public interface Result {
    void show(String text);
  }

  public static void find(final Result result) {
    Geolocation geo = Geolocation.getIfSupported();
    if (geo == null) {
      result.show("This browser has no geolocation; pick a branch from the list.");
      return;
    }
    Geolocation.PositionOptions options = new Geolocation.PositionOptions()
        .setTimeout(5000)
        .setMaximumAge(60000);
    geo.getCurrentPosition(new Callback<Position, PositionError>() {
      @Override
      public void onSuccess(Position position) {
        Position.Coordinates c = position.getCoordinates();
        Libraries.Branch branch = Libraries.nearest(c.getLatitude(), c.getLongitude());
        result.show(branch.name + " (accuracy " + c.getAccuracy() + " m)");
      }

      @Override
      public void onFailure(PositionError error) {
        switch (error.getCode()) {
          case PositionError.PERMISSION_DENIED:
            result.show("Location denied; pick a branch from the list.");
            break;
          case PositionError.POSITION_UNAVAILABLE:
            result.show("Position unavailable: " + error.getMessage());
            break;
          case PositionError.TIMEOUT:
            result.show("Timed out; pick a branch from the list.");
            break;
          default:
            result.show("Unknown error: " + error.getMessage());
        }
      }
    }, options);
  }
}

The lab drove this code in headless Chrome with the DevTools Protocol, which can grant or deny the permission and supply a fake position (Emulation.setGeolocationOverride). Each row is a separate run:

Table 6. NearestBranch results (Chrome 155 headless, emulated location)
Setup Output

permission granted, position overridden to 41.3874 N, 2.1686 E, accuracy 25 m

nearest: Bilbao Riverside (accuracy 25 m)

permission left at the headless default (no prompt can be shown)

nearest: Location denied; pick a branch from the list. (code 1, PERMISSION_DENIED)

permission granted, no position source at all

nearest: Timed out; pick a branch from the list. after the 5000 ms in setTimeout(5000) (code 3, TIMEOUT)

permission granted, override cleared (the protocol’s way to emulate "unavailable")

nearest: Position unavailable: ` with an empty message (code 2, `POSITION_UNAVAILABLE)

The message of a PositionError is browser text and may be empty, so switch on getCode() and write your own explanation. getCurrentPosition is a native method guarded by if (isSupported()): on a browser without geolocation it calls neither callback, so a caller that skips getIfSupported() waits forever. The PositionOptions defaults mean "no cache and no timeout": without setTimeout, a request on a machine with no location source may never finish.

Watching and stopping

watchPosition keeps the callback alive until clearWatch is called, and it keeps using the sensor and the battery. Store the id and clear it when the view goes away.

BranchWatcher.java
import com.example.bookshelf.html5.shared.Libraries;
import com.google.gwt.core.client.Callback;
import com.google.gwt.geolocation.client.Geolocation;
import com.google.gwt.geolocation.client.Position;
import com.google.gwt.geolocation.client.PositionError;

/** Keeps the "nearest branch" label current while the reader moves. */
public class BranchWatcher {
  private final Geolocation geo = Geolocation.getIfSupported();
  private int watchId = -1;

  public boolean start(final NearestBranch.Result result) {
    if (geo == null || watchId != -1) {
      return false;
    }
    watchId = geo.watchPosition(new Callback<Position, PositionError>() {
      @Override
      public void onSuccess(Position p) {
        result.show(Libraries.nearest(p.getCoordinates().getLatitude(),
            p.getCoordinates().getLongitude()).name);
      }

      @Override
      public void onFailure(PositionError e) {
        result.show("error " + e.getCode());
      }
    }, new Geolocation.PositionOptions().setHighAccuracyEnabled(false));
    return watchId != -1;
  }

  /** Call from the view's onUnload or when the user leaves the page, or the watch keeps running. */
  public void stop() {
    if (geo != null && watchId != -1) {
      geo.clearWatch(watchId);
      watchId = -1;
    }
  }
}

With the override changed from Madrid to Bilbao and then to Sevilla:

watch started=true
watch: Madrid Central
watch: error 2
watch: Bilbao Riverside
watch stopped

After watch stopped the third position (Sevilla) produced no callback, so clearWatch worked. The error 2 line appeared while the emulated position was being swapped; whether a real device reports a transient POSITION_UNAVAILABLE there was not tested, so make the callback tolerant of an occasional error between two good fixes.

A location is personal data. Ask for it only after a user action (the button), say what it is for, do not store or send it without consent, and never base an authorisation decision on a client-reported position: the server cannot trust coordinates the browser sent. Round-trip the result (the selected branch) if the server needs it, not the raw latitude and longitude.

When to use Elemental2 instead

GWT’s wrappers are overlay types written for browsers of 2010, they are frozen, and they cover a narrow slice of what a browser offers. Elemental2 (com.google.elemental2, 1.3.2 stable as of 2026-10-08) is generated from the browser externs and tracks the platform. The DOM & Elemental2 page explains how to add it and why mixing the two libraries is safe. Use the GWT classes in this page when they do the whole job; reach for Elemental2 in these cases.

Table 7. Needs and where to find them
You need GWT 2.13.1 class Elemental2 (artifact, class)

a storage event from other tabs

none: StorageEvent.Handler only sees writes made through GWT in the same tab

elemental2-webstorage: StorageEvent, listened to with window.addEventListener("storage", ...)

how much storage quota is left, or to ask for persistent storage

none

elemental2-dom: DomGlobal.navigator.storage (StorageManager: estimate(), persist(), persisted())

large or structured offline data

none (only strings, about 5 MB)

elemental2-indexeddb: IndexedDB

WebGL, OffscreenCanvas, a canvas in a Worker

generic Context only for WebGL; no OffscreenCanvas

elemental2-webgl; elemental2.dom.OffscreenCanvas, Worker

fetch, Blob, File, FileReader, TextEncoder, BroadcastChannel, Notification, MediaRecorder

none

elemental2-dom

play() that reports a refusal

MediaBase.play() returns void

HTMLAudioElement / HTMLVideoElement: play() returns a Promise

Float32Array and friends for non-GWT APIs (WebGL, Web Audio, Workers)

com.google.gwt.typedarrays.shared.* (different types)

elemental2-core: elemental2.core.Float32Array, Uint8Array, ArrayBuffer

The lab used three of these. All needed <inherits name="elemental2.dom.Dom" />, elemental2.core.Core, elemental2.promise.Promise and elemental2.webstorage.WebStorage in the module.

Elemental2Extras.java
import elemental2.core.Uint8Array;
import elemental2.dom.DomGlobal;
import elemental2.dom.TextEncoder;
import elemental2.webstorage.StorageEvent;
import jsinterop.base.Js;

/** What GWT's own HTML5 classes do not cover, through Elemental2. */
public class Elemental2Extras {
  /** Receives a line of text. */
  public interface Sink {
    void accept(String line);
  }

  /** navigator.storage.estimate(): how much of the origin's quota is used. */
  public static void quota(final Sink sink) {
    if (DomGlobal.navigator.storage == null) {
      sink.accept("StorageManager not available (insecure context?)");
      return;
    }
    DomGlobal.navigator.storage.estimate().then(estimate -> {
      sink.accept("quota > 0: " + (estimate.getQuota() > 0) + ", usage >= 0: " + (estimate.getUsage() >= 0));
      return null;
    });
  }

  /** The browser's own storage event: fired in the OTHER tabs of the origin that write. */
  public static void listenForOtherTabs(final Sink sink) {
    DomGlobal.window.addEventListener("storage", e -> {
      StorageEvent se = Js.cast(e);
      sink.accept("storage event: key=" + se.key + " old=" + se.oldValue + " new=" + se.newValue);
    });
  }

  /** TextEncoder returns a real Uint8Array that can be handed to fetch, Blob or WebSocket. */
  public static int utf8Length(String text) {
    Uint8Array bytes = new TextEncoder().encode(text);
    return bytes.length;
  }
}
Output
el2 quota > 0: true, usage >= 0: true
el2 storage event: key=bookshelf.reading.77 old=null new=Added by button     (in tab B)
native Uint8Array: 10 utf8 bytes via TextEncoder

A rule of thumb that held in the lab: pick the GWT class when you want isSupported()-style degradation (it returns null and you branch), and Elemental2 when you want the exact platform API. Elemental2 types assume the feature exists: the MDN page for navigator.storage lists it as available only in secure contexts, so the code above tests the field for null before using it (that case was not exercised). On GWT, remember that com.google.gwt.typedarrays.shared and elemental2.core define separate Uint8Array types; keep each at its own boundary (GWT’s in shared codecs, Elemental2’s at the platform API) and convert in one small method.

The Bookshelf worked example

The pieces above make one coherent feature set, each degrading independently.

flowchart LR subgraph page["Bookshelf catalogue page"] rl["Reading list
localStorage"] dr["Review draft
sessionStorage"] ch["Copies chart
Canvas"] pl["Sample and trailer
Audio, Video"] nb["Nearest branch
Geolocation"] end rl -- "null: hide the saved list" --> deg["Fallback UI"] dr -- "null: keep the draft in memory" --> deg ch -- "null: table of numbers" --> deg pl -- "null: link to the file" --> deg nb -- "denied, timed out: branch list" --> deg lab["Label printer
typed arrays, shared codec"] -. "same code on JVM" .-> srv["Server"]
Table 8. Which API serves which Bookshelf feature
Feature API Degradation tested

Reading list across visits

Storage (local), StorageMap, key prefix

storage access made to throw: list hidden, no exception

Unsent review survives a reload

Storage (session), JSONObject

a corrupt value returns null and the key is removed

Copies per decade chart

Canvas, Context2d

lite build: createIfSupported() is null, no chart

Audio sample, trailer

Audio, Video

lite build: both null

Nearest branch

Geolocation

denied, timed out and unavailable all produce a message

Binary label record

TypedArrays, DataView

checksum mismatch raises IllegalArgumentException

The entry point only wires the features; each helper class returns null or reports in plain text, and nothing throws at start-up when a feature is missing:

@Override
public void onModuleLoad() {
  ReadingList list = ReadingList.create();      // null when storage is blocked
  Canvas chart = CopiesChart.draw(books, DomGlobal.window.devicePixelRatio);
  Audio sample = CoverPlayer.sample("sample.wav", status);
  NearestBranch.find(text -> branchLabel.setText(text));   // setText, not setHTML
}

Pitfalls

The messages are the ones printed by the Chrome 155 run unless the row says otherwise.

Symptom (messages quoted as printed) Cause Fix

NullPointerException right after start-up in a browser that blocks site data

Storage.getLocalStorageIfSupported() returned null and the result was used unchecked

Always test for null, as ReadingList.create() does, and keep the application usable without storage.

java.lang.NullPointerException from StorageMap.put("k", null) or put(null, "v")

storage accepts neither null keys nor null values

Remove the key with remove, or store an empty string.

JavaScriptException, getName() is QuotaExceededError: Failed to execute 'setItem' on 'Storage': Setting the value of 'big4' exceeded the quota.

local and session storage have a small per-origin limit (about 5 million characters in Chrome 155), and the Developer Guide’s "unlimited" session storage is outdated

Catch around large writes; treat storage as a cache; keep values small.

A StorageEvent.Handler in one tab never fires when another tab changes the data

in 2.13.1 GWT replaces the native event with its own, fired only for writes made through GWT in the same window (Storage Javadoc: "Storage events into other windows are not supported.")

Listen to the native storage event through Elemental2.

The handler fires for a delete that changed nothing (key=k old=null new=null)

StorageImplNonNativeEvents.removeItem always fires

Compare old and new values in the handler.

Storage.clear() wipes other applications' keys on the same origin

localStorage belongs to the origin

Prefix your keys and remove only those, as ReadingList.clear() does.

No source code is available for type com.google.gwt.json.client.JSONObject; did you forget to inherit a required module?

the JSON module is not part of User

Add <inherits name="com.google.gwt.json.JSON" />.

A canvas looks blurry on a Retina or phone screen

the bitmap has fewer pixels than the device pixels it covers

Multiply the coordinate space by devicePixelRatio, set the CSS size separately, and scale the context.

Canvas.createIfSupported() returns null although the browser has canvas

the module sets canvasElementSupport to no

Remove the set-property or build a separate module for the lite version.

nearest: never prints anything

getCurrentPosition does nothing on a browser without geolocation, and with no setTimeout a request may never complete

Use getIfSupported(), always set a timeout, and handle all four error codes.

`Position unavailable: ` with an empty message

the browser gave no text

Switch on getCode() and write your own message.

NotAllowedError: play() failed because the user didn't interact with the document first. (browser promise, invisible to GWT)

autoplay policy; MediaBase.play() returns void

Start audio inside a user gesture, mute a video that must autoplay, or use the Elemental2 element and handle the promise.

com.google.gwt.core.client.JavaScriptException (RangeError) : byte length of Float32Array should be a multiple of 4

a view whose element size does not divide the buffer length

Size the buffer as a multiple, or use DataView, which has no alignment rule.

instanceof Uint8Array on a GWT typed array fails or misleads

in the browser the interfaces are implemented by native objects (Javadoc)

Do not test types; keep the views you created in variables.

What was run and what was not

The scratch application was a single gwt-app module with the modular-webapp conventions (net.ltgt.gwt.maven:gwt-maven-plugin 1.3.0, GWT BOM 2.13.1, sourceLevel 17, failOnError true). It compiled with mvn -B package, was served as static files from localhost, and was driven through headless Google Chrome 155 with a fresh profile per run, via the DevTools Protocol.

What ran, with a real engine:

  • all the classes and outputs above in Chrome 155 on macOS: storage (both areas, events, quota, StorageMap), canvas (pixel read-back at two pixel ratios), real WAV and WebM playback and a real mouse click, typed arrays, and the Elemental2 snippets

  • the compile-time canvasElementSupport, audioElementSupport and videoElementSupport properties, with the compiled sizes

  • the shared codec on the JVM with JUnit 4 and JDK 21

  • geolocation callbacks for granted, denied, timed-out and unavailable outcomes, and watchPosition with clearWatch, using the browser’s location emulation

What did not run, and is therefore stated from the sources or the specifications only:

  • a real GPS or network location, a real permission prompt, a transient POSITION_UNAVAILABLE on a device, and geolocation on an insecure origin

  • Firefox and Safari (the only two user.agent permutations compiled are gecko1_8 and safari, both exercised through Chrome’s engine for safari; the Firefox engine was not run)

  • codec support of other browsers and Chromium builds

  • the assert key.length() > 0 in Storage.setItem and the assert in Canvas.wrap (assertions were off)

  • tainted-canvas errors, the Storage.clear() event, and getCurrentPosition doing nothing on a browser without geolocation (all read from the source or the specification)

  • storage disabled by a real browser setting: the blocked case was simulated with throwing property getters; only the sandbox error text was measured on a real sandboxed document

Where older material is outdated

  • The book. GWT in Action (2007) predates HTML5: its text has no canvas, storage, media or geolocation material, and mentions cookies only in passing. Client-side persistence in that era meant cookies (com.google.gwt.user.client.Cookies, still in the library), whose limits (about 4 KB each, sent with every request) are the reason Web Storage exists. The book’s browser-compatibility discussion is about Internet Explorer; GWT 2.13.1 compiles only gecko1_8 and safari permutations.

  • The Developer Guide’s storage page. It says session storage is "limited only by system memory" and local storage is 5 MB; in Chrome 155 both failed at the same size. It says storage events fire in the current window and are rarely implemented elsewhere; in 2.13.1 GWT’s own events are same-window by construction and the native cross-tab event is not listened to. Its snippets call the static addStorageEventHandler on an instance, and its examples use getURL, while the Java method is getUrl().

  • The Developer Guide’s HTML5 index. It lists storage, canvas, audio and video; it does not mention geolocation or typed arrays, which are in the jar.

  • "Experimental API". The warning has been on these classes for many years; they have not changed in the 2.x line shown here.

What changed recently

  • 2.3. The Developer Guide dates its HTML5 storage support and HTML5 feature support to GWT 2.3.

  • User agents. In 2.13.1 the user.agent property has only the values gecko1_8 and safari, so a feature that GWT selects per browser engine (the storage-events implementation among them) has two cases, not the half-dozen of the book’s era.

  • 2.13.x. No change to the storage, canvas, media, typed-array or geolocation classes is listed in the release notes consulted for this page.

  • J2CL. The J2CL-ready org.gwtproject.* ports (gwt-core, gwt-dom, gwt-event, gwt-aria) exist on Maven Central only as release candidates, and there is no gwt-user module in that layout, so the wrappers on this page have no J2CL replacement. New code that must move to J2CL should use Elemental2 for storage, canvas and media from the start.