scroll area moves in jitters/jumps on windows with trackpad scrolls
Author: Jaysmito101Created Jun 2, 2026Updated Sep 10, 2026
Labelsbug
Describe the bug
I have a simple egui scroll view with a bunch of rects in it, when I just make a single fast swipe on the trackpad the scroll moves in jumps/jitters (as shown in the video, it was a single scroll swipe).
To Reproduce
use eframe::egui;
const COLS: usize = 8;
const ROWS: usize = 200;
const RECT_SIZE: f32 = 60.0;
const GAP: f32 = 4.0;
struct App;
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.show(ui, |ui| {
let cell = RECT_SIZE + GAP;
let total_w = COLS as f32 * cell - GAP;
let total_h = ROWS as f32 * cell - GAP;
let (rect, _) =
ui.allocate_exact_size(egui::vec2(total_w, total_h), egui::Sense::hover());
let painter = ui.painter();
for row in 0..ROWS {
for col in 0..COLS {
let idx = row * COLS + col;
let hue = idx as f32 / (ROWS * COLS) as f32;
let color =
egui::Color32::from(egui::ecolor::Hsva::new(hue, 0.75, 0.85, 1.0));
let min = rect.min + egui::vec2(col as f32 * cell, row as f32 * cell);
let tile =
egui::Rect::from_min_size(min, egui::vec2(RECT_SIZE, RECT_SIZE));
painter.rect_filled(tile, 4.0, color);
painter.text(
tile.center(),
egui::Align2::CENTER_CENTER,
idx.to_string(),
egui::FontId::proportional(14.0),
egui::Color32::WHITE,
);
}
}
});
});
}
}
fn main() -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([640.0, 480.0])
.with_title("egui scroll repro"),
..Default::default()
};
eframe::run_native("repro", options, Box::new(|_cc| Ok(Box::new(App))))
}
Run this example and scroll fast with a trackpad. For me this is consistently reproducible.
Expected behavior
Screenshots
https://github.com/user-attachments/assets/4dca28d8-cb0e-4143-8364-a1c3ae663ec9
Desktop (please complete the following information):
- OS: Windows 11
Additional context
I tried to fix it for myself by driving the scroll offset the raw input data, but I am not sure if this is a good way of doing it.
use eframe::egui;
const COLS: usize = 8;
const ROWS: usize = 200;
const RECT_SIZE: f32 = 60.0;
const GAP: f32 = 4.0;
const EASE_RATE: f32 = 12.0;
const SNAP: f32 = 0.3;
#[derive(Default)]
struct App {
target: f32,
pos: f32,
max_off: f32,
velocity: f32,
had_scroll: bool,
}
impl eframe::App for App {
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw_input: &mut egui::RawInput) {
raw_input.events.retain(|event| {
if let egui::Event::MouseWheel { unit, delta, .. } = event {
let dy = match unit {
egui::MouseWheelUnit::Point => delta.y,
egui::MouseWheelUnit::Line => delta.y * (RECT_SIZE + GAP),
egui::MouseWheelUnit::Page => delta.y * self.max_off,
};
self.target = (self.target - dy).clamp(0.0, self.max_off);
self.velocity = -dy;
self.had_scroll = true;
false
} else {
true
}
});
}
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
let cell = RECT_SIZE + GAP;
let total_h = ROWS as f32 * cell - GAP;
self.max_off = (total_h - ui.available_height()).max(0.0);
self.target = self.target.clamp(0.0, self.max_off);
self.pos = self.pos.clamp(0.0, self.max_off);
let had_scroll = self.had_scroll;
self.had_scroll = false;
if !had_scroll {
self.target = (self.target + self.velocity).clamp(0.0, self.max_off);
self.velocity *= 0.9;
if self.velocity.abs() < 0.5 {
self.velocity = 0.0;
}
}
let dt = ctx.input(|i| i.unstable_dt).min(0.05);
let diff = self.target - self.pos;
self.pos += diff * (1.0 - (-EASE_RATE * dt).exp());
if diff.abs() < SNAP {
self.pos = self.target;
}
egui::ScrollArea::vertical()
.vertical_scroll_offset(self.pos)
.auto_shrink(false)
.animated(false)
.show(ui, |ui| {
let total_w = COLS as f32 * cell - GAP;
let (rect, _) =
ui.allocate_exact_size(egui::vec2(total_w, total_h), egui::Sense::hover());
let painter = ui.painter();
for row in 0..ROWS {
for col in 0..COLS {
let idx = row * COLS + col;
let hue = idx as f32 / (ROWS * COLS) as f32;
let color =
egui::Color32::from(egui::ecolor::Hsva::new(hue, 0.75, 0.85, 1.0));
let min = rect.min + egui::vec2(col as f32 * cell, row as f32 * cell);
let tile =
egui::Rect::from_min_size(min, egui::vec2(RECT_SIZE, RECT_SIZE));
painter.rect_filled(tile, 4.0, color);
painter.text(
tile.center(),
egui::Align2::CENTER_CENTER,
idx.to_string(),
egui::FontId::proportional(14.0),
egui::Color32::WHITE,
);
}
}
});
});
ctx.request_repaint();
}
}
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([640.0, 480.0])
.with_title("egui scroll repro"),
..Default::default()
};
eframe::run_native(
"repro",
options,
Box::new(|_cc| Ok(Box::new(App::default()))),
)
}https://github.com/user-attachments/assets/402ff7b2-3261-4f8d-be29-2712a61cd999
Source: emilk/egui