bevyengine/bevy · error

Cannot query for canvas element.

Error message

Cannot query for canvas element.

What it means

On wasm32, when Window.canvas is set to a CSS selector string, bevy_winit's winit_windows passes it to document.querySelector(selector) and .expect("Cannot query for canvas element."). An unparsable selector throws a DOM SyntaxError which web_sys surfaces as Err — i.e. this panic means the selector string itself is malformed, before any element lookup happens.

Source

Thrown at crates/bevy_winit/src/winit_windows.rs:279

        #[expect(clippy::allow_attributes, reason = "`unused_mut` is not always linted")]
        #[allow(
            unused_mut,
            reason = "This variable needs to be mutable if `cfg(target_arch = \"wasm32\")`"
        )]
        let mut winit_window_attributes = winit_window_attributes.with_title(window.title.as_str());

        #[cfg(target_arch = "wasm32")]
        {
            use wasm_bindgen::JsCast;
            use winit::platform::web::WindowAttributesExtWebSys;

            if let Some(selector) = &window.canvas {
                let window = web_sys::window().unwrap();
                let document = window.document().unwrap();
                let canvas = document
                    .query_selector(selector)
                    .expect("Cannot query for canvas element.");
                if let Some(canvas) = canvas {
                    let canvas = canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok();
                    winit_window_attributes = winit_window_attributes.with_canvas(canvas);
                } else {
                    panic!("Cannot find element: {selector}.");
                }
            }

            winit_window_attributes =
                winit_window_attributes.with_prevent_default(window.prevent_default_event_handling);
            winit_window_attributes = winit_window_attributes.with_append(true);
        }

        let winit_window = event_loop.create_window(winit_window_attributes).unwrap();
        let name = window.title.clone();
        prepare_accessibility_for_window(
            event_loop,
            &winit_window,

View on GitHub (pinned to 396ca72708)

Solutions

  1. Write a valid CSS selector: prefix ids with '#' (e.g. "#my-canvas") and classes with '.'.
  2. If targeting a class, ensure it is uniquely applied or made specific enough ("canvas.game").
  3. Validate dynamically built selectors before assigning them to Window.canvas.
  4. Remember an empty Some("") string is also invalid — use None to let Bevy create its own canvas.

Example fix

// before
Window {
    canvas: Some("my-canvas".into()), // invalid CSS selector -> panic
    ..default()
}

// after
Window {
    canvas: Some("#my-canvas".into()), // id selector
    ..default()
}
Defensive patterns

Strategy: validation

Validate before calling

#[cfg(target_arch = "wasm32")]
fn selector_is_valid(selector: &str) -> bool {
    web_sys::window()
        .and_then(|w| w.document())
        .map(|d| d.query_selector(selector).is_ok()) // Err = SyntaxError from a bad selector
        .unwrap_or(false)
}

Type guard

fn as_canvas_selector(s: &str) -> Option<&str> {
    let valid = !s.is_empty() && s.starts_with(['#', '.']);
    valid.then_some(s)
}

Try / catch

// The expect() panics before any Result escapes; validate the selector string
// (and its match, see error 856) before assigning Window.canvas.

Prevention

When it happens

Trigger: Setting Window { canvas: Some("my-canvas".into()) } without the '#' id prefix or '.' class prefix; selectors with stray characters, unbalanced brackets, or empty strings that still pass Some("").

Common situations: Copy-pasting an element id into the canvas field as if it were a raw id rather than a CSS selector; dynamically building selector strings that end up empty or malformed; following examples for older Bevy versions where the field took an id.

Related errors


AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20). Data as JSON: /api/errors/59a81705401b3567. Report an issue: GitHub.