Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Refs & NodeRef Example

Live Demo

Overview

The refs example demonstrates imperative access to rendered DOM nodes via NodeRef. It shows:

  • NodeRef::new() to create empty ref shells before the view is rendered.
  • ref= attribute binding to attach a ref to a specific element.
  • scroll_to_bottom() to imperatively scroll a container to its last child.
  • scroll_to(ix) to scroll to a specific child index.
  • focus(window, cx) to programmatically focus a focusable element.
  • bounds() returning Bounds<Pixels> with origin and size from the previous paint.
  • <Show> for conditional display of the bounds readout.

Source Code

#![cfg_attr(target_family = "wasm", no_main)]

use gpui::{px, size, App, Bounds, Pixels, WindowBounds, WindowOptions};
use vgui::prelude::*;

#[cfg(not(target_family = "wasm"))]
use gpui_platform::application;

#[cfg(target_family = "wasm")]
use gpui_platform::single_threaded_web;

fn app() -> impl gpui::IntoElement {
    // Create NodeRefs before view! — they're empty shells until bound
    // during render by the `ref=` attribute.
    let scroll_ref = NodeRef::new();
    let focus_ref = NodeRef::new();
    let items: Vec<u32> = (0..20).collect();
    let (bounds_text, set_bounds_text) = create_signal(String::new());
    let bounds_ref = NodeRef::new();

    // Clone refs for the event-handler closures (the ref= attributes
    // below consume separate clones).
    let scroll_ref_btn1 = scroll_ref.clone();
    let scroll_ref_btn2 = scroll_ref.clone();
    let focus_ref_btn = focus_ref.clone();
    let bounds_ref_btn = bounds_ref.clone();

    view! {
        <div class="flex flex-col gap-2 p-4 bg-[#505050] w-[400px] h-[500px] text-white">
            <h2 class="text-lg font-bold">{"Refs Demo"}</h2>

            // Buttons that call imperative methods on the refs.
            <div class="flex gap-2">
                <button
                    class="p-2 bg-[#0000ff] hover:bg-[#000088] rounded text-white"
                    on:click={click(move |_cx| {
                        scroll_ref_btn1.scroll_to_bottom();
                    })}
                >
                    {"Scroll to bottom"}
                </button>
                <button
                    class="p-2 bg-[#006600] hover:bg-[#004400] rounded text-white"
                    on:click={click(move |_cx| {
                        scroll_ref_btn2.scroll_to(2);
                    })}
                >
                    {"Scroll to #2"}
                </button>
                <button
                    class="p-2 bg-[#660066] hover:bg-[#440044] rounded text-white"
                    on:click={move |_e, window, cx| {
                        focus_ref_btn.focus(window, cx);
                    }}
                >
                    {"Focus box"}
                </button>
                <button
                    class="p-2 bg-[#0066cc] hover:bg-[#004499] rounded text-white"
                    on:click={move |_e, _window, _cx| {
                        let b = bounds_ref_btn.bounds();
                        set_bounds_text.set(_cx, format!(
                            "x: {:.0} y: {:.0} w: {:.0} h: {:.0}",
                            f32::from(b.origin.x),
                            f32::from(b.origin.y),
                            f32::from(b.size.width),
                            f32::from(b.size.height),
                        ));
                    }}
                >
                    {"Get bounds"}
                </button>
            </div>

            // A scrollable list bound to scroll_ref via ref=.
            // ref= forces an auto-id and applies track_focus + track_scroll
            // so scroll_to/scroll_to_bottom/bounds all work.
            <div
                ref={scroll_ref.clone()}
                class="flex-1 overflow-y-scroll bg-[#3a3a3a] rounded p-2 gap-1 flex-col"
            >
                <For each={items}>
                    {move |i: u32, _idx: usize| view! {
                        <div class="p-2 bg-[#2a2a2a] rounded">
                            {format!("Item {}", i)}
                        </div>
                    }}
                </For>
            </div>

            // A focusable box bound to focus_ref.
            <div
                ref={focus_ref.clone()}
                class="p-3 bg-[#2a2a2a] rounded border-2 border-[#666] focus:border-[#0f0]"
                tabindex={0}
            >
                {"Click 'Focus box' to focus me."}
            </div>

            // A div bound to bounds_ref — click "Get bounds" to read its
            // painted Bounds<Pixels> (origin + size) from the previous frame.
            <div
                ref={bounds_ref.clone()}
                class="p-3 bg-[#2a2a2a] rounded border-2 border-[#444]"
            >
                {"Bounds target div"}
            </div>

            <Show when={!bounds_text.get().is_empty()}>
                <span class="text-sm text-[#0f0]">{bounds_text.get()}</span>
            </Show>
        </div>
    }
}

fn run() {
    #[cfg(not(target_family = "wasm"))]
    let gpui_app = application();

    #[cfg(target_family = "wasm")]
    let gpui_app = single_threaded_web();

    let launch = |cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(400.), px(500.0)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |window, cx| vgui::mount(window, cx, app),
        )
        .unwrap();
    };

    #[cfg(not(target_family = "wasm"))]
    gpui_app.run(launch);

    #[cfg(target_family = "wasm")]
    std::mem::forget(gpui_app.run_embedded(launch));
}

#[cfg(not(target_family = "wasm"))]
fn main() {
    run();
}

#[cfg(target_family = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
    gpui_platform::web_init();
    vgui::intercept_keyboard_events();
    run();
}

Key Concepts

NodeRef lifecycle

NodeRef::new() creates an empty ref shell before the view! macro runs. During render, the ref={scroll_ref.clone()} attribute binds the shell to the actual rendered element, forcing an auto-id and enabling track_focus + track_scroll internally. Because event-handler closures capture the ref by move, you clone the ref once per closure (and once per ref= binding) — each clone shares the same underlying node handle.

Imperative scroll methods

scroll_to_bottom() scrolls the bound container so its last child is visible. scroll_to(ix) scrolls to child index ix. Both operate on the element bound via ref= and require the container to have overflow scrolling enabled (here overflow-y-scroll). These are called from click(move |_cx| ...) handlers — no window or cx argument is needed for scroll operations.

Programmatic focus

focus_ref_btn.focus(window, cx) moves keyboard focus to the element bound by ref=. Unlike scroll, focus needs the window and cx from the event handler signature, so this button uses the raw move |_e, window, cx| form rather than the click helper. The target div has tabindex={0} so it is focusable, and a focus: variant changes its border color when focused.

bounds() returning Bounds<Pixels>

bounds_ref_btn.bounds() returns a Bounds<Pixels> struct describing the element’s painted rectangle from the previous frame. It exposes .origin (a Point<Pixels> with .x and .y) and .size (with .width and .height). The “Get bounds” button reads these fields, formats them into a string, and stores it in a signal. A <Show> block conditionally renders the readout only once it is non-empty. Convert Pixels to f32 via f32::from(b.origin.x) for formatting.

<Show> for conditional display

<Show when={!bounds_text.get().is_empty()}> renders the bounds readout span only after the user has clicked “Get bounds” at least once. Before that, the signal holds an empty string and the <Show> renders nothing.

Running

Native:

cargo run -p vgui-refs

Web (WASM):

cargo build --target wasm32-unknown-unknown -p vgui-refs --release
wasm-bindgen --target web --out-dir examples/refs/dist \
    --no-typescript target/wasm32-unknown-unknown/release/refs.wasm
python3 scripts/serve_plain.py 8080 examples/refs