diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6a989f6bb..c68ad2c67b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Helpers to change viewport alignment of a `Scrollable`. [#1953](https://github.com/iced-rs/iced/pull/1953)
 - `scroll_to` widget operation. [#1796](https://github.com/iced-rs/iced/pull/1796)
 - `scroll_to` helper. [#1804](https://github.com/iced-rs/iced/pull/1804)
+- `visible_bounds` widget operation for `Container`. [#1971](https://github.com/iced-rs/iced/pull/1971)
 - Command to fetch window size. [#1927](https://github.com/iced-rs/iced/pull/1927)
 - Conversion support from `Fn` trait to custom theme. [#1861](https://github.com/iced-rs/iced/pull/1861)
 - Configurable border radii on relevant widgets. [#1869](https://github.com/iced-rs/iced/pull/1869)
diff --git a/core/src/element.rs b/core/src/element.rs
index b9b76247da..d2c6358b6c 100644
--- a/core/src/element.rs
+++ b/core/src/element.rs
@@ -5,7 +5,9 @@ use crate::overlay;
 use crate::renderer;
 use crate::widget;
 use crate::widget::tree::{self, Tree};
-use crate::{Clipboard, Color, Layout, Length, Rectangle, Shell, Widget};
+use crate::{
+    Clipboard, Color, Layout, Length, Rectangle, Shell, Vector, Widget,
+};
 
 use std::any::Any;
 use std::borrow::Borrow;
@@ -325,11 +327,12 @@ where
             fn container(
                 &mut self,
                 id: Option<&widget::Id>,
+                bounds: Rectangle,
                 operate_on_children: &mut dyn FnMut(
                     &mut dyn widget::Operation<T>,
                 ),
             ) {
-                self.operation.container(id, &mut |operation| {
+                self.operation.container(id, bounds, &mut |operation| {
                     operate_on_children(&mut MapOperation { operation });
                 });
             }
@@ -346,8 +349,10 @@ where
                 &mut self,
                 state: &mut dyn widget::operation::Scrollable,
                 id: Option<&widget::Id>,
+                bounds: Rectangle,
+                translation: Vector,
             ) {
-                self.operation.scrollable(state, id);
+                self.operation.scrollable(state, id, bounds, translation);
             }
 
             fn text_input(
diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs
index c2134343db..29b404b83f 100644
--- a/core/src/overlay/element.rs
+++ b/core/src/overlay/element.rs
@@ -172,11 +172,12 @@ where
             fn container(
                 &mut self,
                 id: Option<&widget::Id>,
+                bounds: Rectangle,
                 operate_on_children: &mut dyn FnMut(
                     &mut dyn widget::Operation<T>,
                 ),
             ) {
-                self.operation.container(id, &mut |operation| {
+                self.operation.container(id, bounds, &mut |operation| {
                     operate_on_children(&mut MapOperation { operation });
                 });
             }
@@ -193,8 +194,10 @@ where
                 &mut self,
                 state: &mut dyn widget::operation::Scrollable,
                 id: Option<&widget::Id>,
+                bounds: Rectangle,
+                translation: Vector,
             ) {
-                self.operation.scrollable(state, id);
+                self.operation.scrollable(state, id, bounds, translation);
             }
 
             fn text_input(
diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs
index deffaad030..691686cdd5 100644
--- a/core/src/overlay/group.rs
+++ b/core/src/overlay/group.rs
@@ -138,7 +138,7 @@ where
         renderer: &Renderer,
         operation: &mut dyn widget::Operation<Message>,
     ) {
-        operation.container(None, &mut |operation| {
+        operation.container(None, layout.bounds(), &mut |operation| {
             self.children.iter_mut().zip(layout.children()).for_each(
                 |(child, layout)| {
                     child.operate(layout, renderer, operation);
diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs
index 7ff324cb89..db56aa18cf 100644
--- a/core/src/rectangle.rs
+++ b/core/src/rectangle.rs
@@ -197,3 +197,18 @@ where
         }
     }
 }
+
+impl<T> std::ops::Sub<Vector<T>> for Rectangle<T>
+where
+    T: std::ops::Sub<Output = T>,
+{
+    type Output = Rectangle<T>;
+
+    fn sub(self, translation: Vector<T>) -> Self {
+        Rectangle {
+            x: self.x - translation.x,
+            y: self.y - translation.y,
+            ..self
+        }
+    }
+}
diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs
index ad188c364d..b91cf9ac94 100644
--- a/core/src/widget/operation.rs
+++ b/core/src/widget/operation.rs
@@ -8,6 +8,7 @@ pub use scrollable::Scrollable;
 pub use text_input::TextInput;
 
 use crate::widget::Id;
+use crate::{Rectangle, Vector};
 
 use std::any::Any;
 use std::fmt;
@@ -23,6 +24,7 @@ pub trait Operation<T> {
     fn container(
         &mut self,
         id: Option<&Id>,
+        bounds: Rectangle,
         operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
     );
 
@@ -30,7 +32,14 @@ pub trait Operation<T> {
     fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
 
     /// Operates on a widget that can be scrolled.
-    fn scrollable(&mut self, _state: &mut dyn Scrollable, _id: Option<&Id>) {}
+    fn scrollable(
+        &mut self,
+        _state: &mut dyn Scrollable,
+        _id: Option<&Id>,
+        _bounds: Rectangle,
+        _translation: Vector,
+    ) {
+    }
 
     /// Operates on a widget that has text input.
     fn text_input(&mut self, _state: &mut dyn TextInput, _id: Option<&Id>) {}
@@ -92,6 +101,7 @@ where
         fn container(
             &mut self,
             id: Option<&Id>,
+            bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
         ) {
             struct MapRef<'a, A> {
@@ -102,11 +112,12 @@ where
                 fn container(
                     &mut self,
                     id: Option<&Id>,
+                    bounds: Rectangle,
                     operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
                 ) {
                     let Self { operation, .. } = self;
 
-                    operation.container(id, &mut |operation| {
+                    operation.container(id, bounds, &mut |operation| {
                         operate_on_children(&mut MapRef { operation });
                     });
                 }
@@ -115,8 +126,10 @@ where
                     &mut self,
                     state: &mut dyn Scrollable,
                     id: Option<&Id>,
+                    bounds: Rectangle,
+                    translation: Vector,
                 ) {
-                    self.operation.scrollable(state, id);
+                    self.operation.scrollable(state, id, bounds, translation);
                 }
 
                 fn focusable(
@@ -145,15 +158,21 @@ where
             MapRef {
                 operation: operation.as_mut(),
             }
-            .container(id, operate_on_children);
+            .container(id, bounds, operate_on_children);
         }
 
         fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
             self.operation.focusable(state, id);
         }
 
-        fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
-            self.operation.scrollable(state, id);
+        fn scrollable(
+            &mut self,
+            state: &mut dyn Scrollable,
+            id: Option<&Id>,
+            bounds: Rectangle,
+            translation: Vector,
+        ) {
+            self.operation.scrollable(state, id, bounds, translation);
         }
 
         fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
@@ -197,6 +216,7 @@ pub fn scope<T: 'static>(
         fn container(
             &mut self,
             id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<Message>),
         ) {
             if id == Some(&self.target) {
diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs
index 312e48943d..ab1b677ea4 100644
--- a/core/src/widget/operation/focusable.rs
+++ b/core/src/widget/operation/focusable.rs
@@ -1,6 +1,7 @@
 //! Operate on widgets that can be focused.
 use crate::widget::operation::{Operation, Outcome};
 use crate::widget::Id;
+use crate::Rectangle;
 
 /// The internal state of a widget that can be focused.
 pub trait Focusable {
@@ -45,6 +46,7 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
         ) {
             operate_on_children(self)
@@ -80,6 +82,7 @@ where
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
         ) {
             operate_on_children(self)
@@ -126,6 +129,7 @@ pub fn focus_previous<T>() -> impl Operation<T> {
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
         ) {
             operate_on_children(self)
@@ -159,6 +163,7 @@ pub fn focus_next<T>() -> impl Operation<T> {
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
         ) {
             operate_on_children(self)
@@ -185,6 +190,7 @@ pub fn find_focused() -> impl Operation<Id> {
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<Id>),
         ) {
             operate_on_children(self)
diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs
index f947344ded..4f8b2a9819 100644
--- a/core/src/widget/operation/scrollable.rs
+++ b/core/src/widget/operation/scrollable.rs
@@ -1,5 +1,6 @@
 //! Operate on widgets that can be scrolled.
 use crate::widget::{Id, Operation};
+use crate::{Rectangle, Vector};
 
 /// The internal state of a widget that can be scrolled.
 pub trait Scrollable {
@@ -22,12 +23,19 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
         ) {
             operate_on_children(self)
         }
 
-        fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
+        fn scrollable(
+            &mut self,
+            state: &mut dyn Scrollable,
+            id: Option<&Id>,
+            _bounds: Rectangle,
+            _translation: Vector,
+        ) {
             if Some(&self.target) == id {
                 state.snap_to(self.offset);
             }
@@ -49,12 +57,19 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
         ) {
             operate_on_children(self)
         }
 
-        fn scrollable(&mut self, state: &mut dyn Scrollable, id: Option<&Id>) {
+        fn scrollable(
+            &mut self,
+            state: &mut dyn Scrollable,
+            id: Option<&Id>,
+            _bounds: Rectangle,
+            _translation: Vector,
+        ) {
             if Some(&self.target) == id {
                 state.scroll_to(self.offset);
             }
diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs
index 4c773e9984..a9ea2e8137 100644
--- a/core/src/widget/operation/text_input.rs
+++ b/core/src/widget/operation/text_input.rs
@@ -1,6 +1,7 @@
 //! Operate on widgets that have text input.
 use crate::widget::operation::Operation;
 use crate::widget::Id;
+use crate::Rectangle;
 
 /// The internal state of a widget that has text input.
 pub trait TextInput {
@@ -34,6 +35,7 @@ pub fn move_cursor_to_front<T>(target: Id) -> impl Operation<T> {
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
         ) {
             operate_on_children(self)
@@ -63,6 +65,7 @@ pub fn move_cursor_to_end<T>(target: Id) -> impl Operation<T> {
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
         ) {
             operate_on_children(self)
@@ -93,6 +96,7 @@ pub fn move_cursor_to<T>(target: Id, position: usize) -> impl Operation<T> {
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
         ) {
             operate_on_children(self)
@@ -121,6 +125,7 @@ pub fn select_all<T>(target: Id) -> impl Operation<T> {
         fn container(
             &mut self,
             _id: Option<&Id>,
+            _bounds: Rectangle,
             operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
         ) {
             operate_on_children(self)
diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs
index 5d29e89544..42f6c34840 100644
--- a/examples/toast/src/main.rs
+++ b/examples/toast/src/main.rs
@@ -381,7 +381,7 @@ mod toast {
             renderer: &Renderer,
             operation: &mut dyn Operation<Message>,
         ) {
-            operation.container(None, &mut |operation| {
+            operation.container(None, layout.bounds(), &mut |operation| {
                 self.content.as_widget().operate(
                     &mut state.children[0],
                     layout,
@@ -622,7 +622,7 @@ mod toast {
             renderer: &Renderer,
             operation: &mut dyn widget::Operation<Message>,
         ) {
-            operation.container(None, &mut |operation| {
+            operation.container(None, layout.bounds(), &mut |operation| {
                 self.toasts
                     .iter()
                     .zip(self.state.iter_mut())
diff --git a/examples/visible_bounds/Cargo.toml b/examples/visible_bounds/Cargo.toml
new file mode 100644
index 0000000000..cfa56dd2e4
--- /dev/null
+++ b/examples/visible_bounds/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "visible_bounds"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["debug"] }
+once_cell = "1"
diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs
new file mode 100644
index 0000000000..8b68451438
--- /dev/null
+++ b/examples/visible_bounds/src/main.rs
@@ -0,0 +1,187 @@
+use iced::executor;
+use iced::mouse;
+use iced::subscription::{self, Subscription};
+use iced::theme::{self, Theme};
+use iced::widget::{
+    column, container, horizontal_space, row, scrollable, text, vertical_space,
+};
+use iced::window;
+use iced::{
+    Alignment, Application, Color, Command, Element, Event, Font, Length,
+    Point, Rectangle, Settings,
+};
+
+pub fn main() -> iced::Result {
+    Example::run(Settings::default())
+}
+
+struct Example {
+    mouse_position: Option<Point>,
+    outer_bounds: Option<Rectangle>,
+    inner_bounds: Option<Rectangle>,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum Message {
+    MouseMoved(Point),
+    WindowResized,
+    Scrolled(scrollable::Viewport),
+    OuterBoundsFetched(Option<Rectangle>),
+    InnerBoundsFetched(Option<Rectangle>),
+}
+
+impl Application for Example {
+    type Message = Message;
+    type Theme = Theme;
+    type Flags = ();
+    type Executor = executor::Default;
+
+    fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
+        (
+            Self {
+                mouse_position: None,
+                outer_bounds: None,
+                inner_bounds: None,
+            },
+            Command::none(),
+        )
+    }
+
+    fn title(&self) -> String {
+        String::from("Visible bounds - Iced")
+    }
+
+    fn update(&mut self, message: Message) -> Command<Message> {
+        match message {
+            Message::MouseMoved(position) => {
+                self.mouse_position = Some(position);
+
+                Command::none()
+            }
+            Message::Scrolled(_) | Message::WindowResized => {
+                Command::batch(vec![
+                    container::visible_bounds(OUTER_CONTAINER.clone())
+                        .map(Message::OuterBoundsFetched),
+                    container::visible_bounds(INNER_CONTAINER.clone())
+                        .map(Message::InnerBoundsFetched),
+                ])
+            }
+            Message::OuterBoundsFetched(outer_bounds) => {
+                self.outer_bounds = outer_bounds;
+
+                Command::none()
+            }
+            Message::InnerBoundsFetched(inner_bounds) => {
+                self.inner_bounds = inner_bounds;
+
+                Command::none()
+            }
+        }
+    }
+
+    fn view(&self) -> Element<Message> {
+        let data_row = |label, value, color| {
+            row![
+                text(label),
+                horizontal_space(Length::Fill),
+                text(value).font(Font::MONOSPACE).size(14).style(color),
+            ]
+            .height(40)
+            .align_items(Alignment::Center)
+        };
+
+        let view_bounds = |label, bounds: Option<Rectangle>| {
+            data_row(
+                label,
+                match bounds {
+                    Some(bounds) => format!("{:?}", bounds),
+                    None => "not visible".to_string(),
+                },
+                if bounds
+                    .zip(self.mouse_position)
+                    .map(|(bounds, mouse_position)| {
+                        bounds.contains(mouse_position)
+                    })
+                    .unwrap_or_default()
+                {
+                    Color {
+                        g: 1.0,
+                        ..Color::BLACK
+                    }
+                    .into()
+                } else {
+                    theme::Text::Default
+                },
+            )
+        };
+
+        column![
+            data_row(
+                "Mouse position",
+                match self.mouse_position {
+                    Some(Point { x, y }) => format!("({x}, {y})"),
+                    None => "unknown".to_string(),
+                },
+                theme::Text::Default,
+            ),
+            view_bounds("Outer container", self.outer_bounds),
+            view_bounds("Inner container", self.inner_bounds),
+            scrollable(
+                column![
+                    text("Scroll me!"),
+                    vertical_space(400),
+                    container(text("I am the outer container!"))
+                        .id(OUTER_CONTAINER.clone())
+                        .padding(40)
+                        .style(theme::Container::Box),
+                    vertical_space(400),
+                    scrollable(
+                        column![
+                            text("Scroll me!"),
+                            vertical_space(400),
+                            container(text("I am the inner container!"))
+                                .id(INNER_CONTAINER.clone())
+                                .padding(40)
+                                .style(theme::Container::Box),
+                            vertical_space(400)
+                        ]
+                        .padding(20)
+                    )
+                    .on_scroll(Message::Scrolled)
+                    .width(Length::Fill)
+                    .height(300),
+                ]
+                .padding(20)
+            )
+            .on_scroll(Message::Scrolled)
+            .width(Length::Fill)
+            .height(300),
+        ]
+        .spacing(10)
+        .padding(20)
+        .into()
+    }
+
+    fn subscription(&self) -> Subscription<Message> {
+        subscription::events_with(|event, _| match event {
+            Event::Mouse(mouse::Event::CursorMoved { position }) => {
+                Some(Message::MouseMoved(position))
+            }
+            Event::Window(window::Event::Resized { .. }) => {
+                Some(Message::WindowResized)
+            }
+            _ => None,
+        })
+    }
+
+    fn theme(&self) -> Theme {
+        Theme::Dark
+    }
+}
+
+use once_cell::sync::Lazy;
+
+static OUTER_CONTAINER: Lazy<container::Id> =
+    Lazy::new(|| container::Id::new("outer"));
+static INNER_CONTAINER: Lazy<container::Id> =
+    Lazy::new(|| container::Id::new("inner"));
diff --git a/widget/src/button.rs b/widget/src/button.rs
index 1312095f85..5727c63180 100644
--- a/widget/src/button.rs
+++ b/widget/src/button.rs
@@ -181,7 +181,7 @@ where
         renderer: &Renderer,
         operation: &mut dyn Operation<Message>,
     ) {
-        operation.container(None, &mut |operation| {
+        operation.container(None, layout.bounds(), &mut |operation| {
             self.content.as_widget().operate(
                 &mut tree.children[0],
                 layout.children().next().unwrap(),
diff --git a/widget/src/column.rs b/widget/src/column.rs
index 9271d5efac..c16477f394 100644
--- a/widget/src/column.rs
+++ b/widget/src/column.rs
@@ -148,7 +148,7 @@ where
         renderer: &Renderer,
         operation: &mut dyn Operation<Message>,
     ) {
-        operation.container(None, &mut |operation| {
+        operation.container(None, layout.bounds(), &mut |operation| {
             self.children
                 .iter()
                 .zip(&mut tree.children)
diff --git a/widget/src/container.rs b/widget/src/container.rs
index 64cf5cd534..1f1df86171 100644
--- a/widget/src/container.rs
+++ b/widget/src/container.rs
@@ -8,8 +8,9 @@ use crate::core::renderer;
 use crate::core::widget::{self, Operation, Tree};
 use crate::core::{
     Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels,
-    Point, Rectangle, Shell, Widget,
+    Point, Rectangle, Shell, Size, Vector, Widget,
 };
+use crate::runtime::Command;
 
 pub use iced_style::container::{Appearance, StyleSheet};
 
@@ -180,6 +181,7 @@ where
     ) {
         operation.container(
             self.id.as_ref().map(|id| &id.0),
+            layout.bounds(),
             &mut |operation| {
                 self.content.as_widget().operate(
                     &mut tree.children[0],
@@ -368,3 +370,92 @@ impl From<Id> for widget::Id {
         id.0
     }
 }
+
+/// Produces a [`Command`] that queries the visible screen bounds of the
+/// [`Container`] with the given [`Id`].
+pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
+    struct VisibleBounds {
+        target: widget::Id,
+        depth: usize,
+        scrollables: Vec<(Vector, Rectangle, usize)>,
+        bounds: Option<Rectangle>,
+    }
+
+    impl Operation<Option<Rectangle>> for VisibleBounds {
+        fn scrollable(
+            &mut self,
+            _state: &mut dyn widget::operation::Scrollable,
+            _id: Option<&widget::Id>,
+            bounds: Rectangle,
+            translation: Vector,
+        ) {
+            match self.scrollables.last() {
+                Some((last_translation, last_viewport, _depth)) => {
+                    let viewport = last_viewport
+                        .intersection(&(bounds - *last_translation))
+                        .unwrap_or(Rectangle::new(Point::ORIGIN, Size::ZERO));
+
+                    self.scrollables.push((
+                        translation + *last_translation,
+                        viewport,
+                        self.depth,
+                    ));
+                }
+                None => {
+                    self.scrollables.push((translation, bounds, self.depth));
+                }
+            }
+        }
+
+        fn container(
+            &mut self,
+            id: Option<&widget::Id>,
+            bounds: Rectangle,
+            operate_on_children: &mut dyn FnMut(
+                &mut dyn Operation<Option<Rectangle>>,
+            ),
+        ) {
+            if self.bounds.is_some() {
+                return;
+            }
+
+            if id == Some(&self.target) {
+                match self.scrollables.last() {
+                    Some((translation, viewport, _)) => {
+                        self.bounds =
+                            viewport.intersection(&(bounds - *translation));
+                    }
+                    None => {
+                        self.bounds = Some(bounds);
+                    }
+                }
+
+                return;
+            }
+
+            self.depth += 1;
+
+            operate_on_children(self);
+
+            self.depth -= 1;
+
+            match self.scrollables.last() {
+                Some((_, _, depth)) if self.depth == *depth => {
+                    let _ = self.scrollables.pop();
+                }
+                _ => {}
+            }
+        }
+
+        fn finish(&self) -> widget::operation::Outcome<Option<Rectangle>> {
+            widget::operation::Outcome::Some(self.bounds)
+        }
+    }
+
+    Command::widget(VisibleBounds {
+        target: id.into(),
+        depth: 0,
+        scrollables: Vec::new(),
+        bounds: None,
+    })
+}
diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs
index bc0e23df56..19df279284 100644
--- a/widget/src/lazy/component.rs
+++ b/widget/src/lazy/component.rs
@@ -7,7 +7,8 @@ use crate::core::renderer;
 use crate::core::widget;
 use crate::core::widget::tree::{self, Tree};
 use crate::core::{
-    self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Widget,
+    self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector,
+    Widget,
 };
 use crate::runtime::overlay::Nested;
 
@@ -340,11 +341,12 @@ where
             fn container(
                 &mut self,
                 id: Option<&widget::Id>,
+                bounds: Rectangle,
                 operate_on_children: &mut dyn FnMut(
                     &mut dyn widget::Operation<T>,
                 ),
             ) {
-                self.operation.container(id, &mut |operation| {
+                self.operation.container(id, bounds, &mut |operation| {
                     operate_on_children(&mut MapOperation { operation });
                 });
             }
@@ -369,8 +371,10 @@ where
                 &mut self,
                 state: &mut dyn widget::operation::Scrollable,
                 id: Option<&widget::Id>,
+                bounds: Rectangle,
+                translation: Vector,
             ) {
-                self.operation.scrollable(state, id);
+                self.operation.scrollable(state, id, bounds, translation);
             }
 
             fn custom(
diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs
index 4f6dfbe83f..0f4ab9eb16 100644
--- a/widget/src/pane_grid.rs
+++ b/widget/src/pane_grid.rs
@@ -297,7 +297,7 @@ where
         renderer: &Renderer,
         operation: &mut dyn widget::Operation<Message>,
     ) {
-        operation.container(None, &mut |operation| {
+        operation.container(None, layout.bounds(), &mut |operation| {
             self.contents
                 .iter()
                 .zip(&mut tree.children)
diff --git a/widget/src/row.rs b/widget/src/row.rs
index 7baaaae31c..99b2a0bf0a 100644
--- a/widget/src/row.rs
+++ b/widget/src/row.rs
@@ -137,7 +137,7 @@ where
         renderer: &Renderer,
         operation: &mut dyn Operation<Message>,
     ) {
-        operation.container(None, &mut |operation| {
+        operation.container(None, layout.bounds(), &mut |operation| {
             self.children
                 .iter()
                 .zip(&mut tree.children)
diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs
index f621fb26c4..103e3944e2 100644
--- a/widget/src/scrollable.rs
+++ b/widget/src/scrollable.rs
@@ -254,10 +254,22 @@ where
     ) {
         let state = tree.state.downcast_mut::<State>();
 
-        operation.scrollable(state, self.id.as_ref().map(|id| &id.0));
+        let bounds = layout.bounds();
+        let content_layout = layout.children().next().unwrap();
+        let content_bounds = content_layout.bounds();
+        let translation =
+            state.translation(self.direction, bounds, content_bounds);
+
+        operation.scrollable(
+            state,
+            self.id.as_ref().map(|id| &id.0),
+            bounds,
+            translation,
+        );
 
         operation.container(
             self.id.as_ref().map(|id| &id.0),
+            bounds,
             &mut |operation| {
                 self.content.as_widget().operate(
                     &mut tree.children[0],