Merge mozilla-central to autoland. a=merge on a CLOSED TREE
authorRazvan Maries <rmaries@mozilla.com>
Fri, 11 Jan 2019 11:36:24 +0200
changeset 510550 d084d6b3b5334c8bb3e96aaa1861276f0aa26095
parent 510549 30fd54bb80897d6139d84e6c64937a6bc182f70d (current diff)
parent 510502 340d5146c4052a47c5aa4f70817dc3ee9fd4e7da (diff)
child 510551 939479cc5903c7849098a9826fae052e2a5f5553
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to autoland. a=merge on a CLOSED TREE
layout/base/PresShell.cpp
layout/base/nsLayoutUtils.cpp
testing/web-platform/meta/css/css-scroll-anchoring/abspos-containing-block-outside-scroller.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/abspos-contributes-to-static-parent-bounds.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/ancestor-change-heuristic.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/anchor-updates-after-explicit-scroll.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/anchoring-with-bounds-clamping-div.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/anchoring-with-bounds-clamping.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/anonymous-block-box.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/basic.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/clipped-scrollers-skipped.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/descend-into-container-with-float.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/descend-into-container-with-overflow.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/exclude-fixed-position.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/inline-block.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/opt-out.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/position-change-heuristic.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/subtree-exclusion.html.ini
testing/web-platform/meta/css/css-scroll-anchoring/wrapped-text.html.ini
--- a/devtools/client/debugger/new/test/mochitest/browser.ini
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -649,16 +649,18 @@ support-files =
   examples/frames.js
   examples/pause-points.js
   examples/script-mutate.js
   examples/script-switching-02.js
   examples/script-switching-01.js
   examples/times2.js
   examples/doc-windowless-workers.html
   examples/simple-worker.js
+  examples/doc-event-handler.html
+  examples/doc-eval-throw.html
   examples/doc_rr_basic.html
   examples/doc_rr_continuous.html
   examples/doc_rr_logs.html
   examples/doc_rr_recovery.html
   examples/doc_rr_error.html
 
 [browser_dbg-asm.js]
 [browser_dbg-async-stepping.js]
@@ -766,16 +768,18 @@ skip-if = os == "win"
 [browser_dbg-tabs-pretty-print.js]
 [browser_dbg-tabs-without-urls.js]
 [browser_dbg-toggling-tools.js]
 [browser_dbg-react-app.js]
 skip-if = os == "win"
 [browser_dbg-wasm-sourcemaps.js]
 skip-if = true
 [browser_dbg-windowless-workers.js]
+[browser_dbg-event-handler.js]
+[browser_dbg-eval-throw.js]
 [browser_dbg_rr_breakpoints-01.js]
 skip-if = os != "mac" || debug || !nightly_build
 [browser_dbg_rr_breakpoints-02.js]
 skip-if = os != "mac" || debug || !nightly_build
 [browser_dbg_rr_breakpoints-03.js]
 skip-if = os != "mac" || debug || !nightly_build
 [browser_dbg_rr_breakpoints-04.js]
 skip-if = os != "mac" || debug || !nightly_build
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-eval-throw.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that exceptions thrown while evaluating code will pause at the point the
+// exception was generated when pausing on uncaught exceptions.
+add_task(async function() {
+  const dbg = await initDebugger("doc-eval-throw.html");
+  await togglePauseOnExceptions(dbg, true, true);
+
+  invokeInTab("evaluateException");
+
+  await waitForPaused(dbg);
+
+  const state = dbg.store.getState();
+  const source = dbg.selectors.getSelectedSource(state);
+  ok(!source.url, "Selected source should not have a URL");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/browser_dbg-event-handler.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that pausing within an event handler on an element does *not* show the
+// HTML page containing that element. It should show a sources tab containing
+// just the handler's text instead.
+add_task(async function() {
+  const dbg = await initDebugger("doc-event-handler.html");
+
+  invokeInTab("synthesizeClick");
+
+  await waitForPaused(dbg);
+
+  const state = dbg.store.getState();
+  const source = dbg.selectors.getSelectedSource(state);
+  ok(!source.url, "Selected source should not have a URL");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-eval-throw.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<meta charset=UTF-8>
+<script>
+function evaluateException() {
+  try {
+    eval("throw new Error()");
+  } catch (e) {}
+}
+</script>
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/new/test/mochitest/examples/doc-event-handler.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta charset=UTF-8>
+<script>
+function synthesizeClick() {
+const elem = document.getElementById("clicky");
+const event = new MouseEvent("click", {});
+elem.dispatchEvent(event);
+}
+</script>
+<span onclick="debugger" id="clicky">click me</span>
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -801,16 +801,17 @@ a.learn-more-link.webconsole-learn-more-
 .new-consoletable > [role=gridcell].even {
   background-color: var(--table-zebra-background);
 }
 
 /* Layout */
 .webconsole-output {
   direction: ltr;
   overflow: auto;
+  overflow-anchor: none;
   -moz-user-select: text;
   position: relative;
 }
 
 html,
 body {
   height: 100vh;
   margin: 0;
--- a/devtools/server/actors/animation-type-longhand.js
+++ b/devtools/server/actors/animation-type-longhand.js
@@ -102,16 +102,17 @@ exports.ANIMATION_TYPE_FOR_LONGHANDS = [
     "mask-origin",
     "mask-repeat",
     "mask-type",
     "mix-blend-mode",
     "object-fit",
     "-moz-orient",
     "-moz-osx-font-smoothing",
     "outline-style",
+    "overflow-anchor",
     "overflow-clip-box-block",
     "overflow-clip-box-inline",
     "overflow-wrap",
     "overflow-x",
     "overflow-y",
     "overscroll-behavior-x",
     "overscroll-behavior-y",
     "break-after",
--- a/devtools/server/actors/thread.js
+++ b/devtools/server/actors/thread.js
@@ -1820,19 +1820,23 @@ const ThreadActor = ActorClassWithSpec(t
     // handled cleanly by native code.
     if (value == Cr.NS_ERROR_NO_INTERFACE) {
       return undefined;
     }
 
     const { generatedSourceActor } = this.sources.getFrameLocation(youngestFrame);
     const url = generatedSourceActor ? generatedSourceActor.url : null;
 
-    // We ignore sources without a url because we do not
-    // want to pause at console evaluations or watch expressions.
-    if (!url || this.skipBreakpoints || this.sources.isBlackBoxed(url)) {
+    // Don't pause on exceptions thrown while inside an evaluation being done on
+    // behalf of the client.
+    if (this.insideClientEvaluation) {
+      return undefined;
+    }
+
+    if (this.skipBreakpoints || this.sources.isBlackBoxed(url)) {
       return undefined;
     }
 
     try {
       const packet = this._paused(youngestFrame);
       if (!packet) {
         return undefined;
       }
--- a/devtools/server/actors/utils/TabSources.js
+++ b/devtools/server/actors/utils/TabSources.js
@@ -88,17 +88,17 @@ TabSources.prototype = {
       return null;
     }
 
     // It's a hack, but inline HTML scripts each have real sources,
     // but we want to represent all of them as one source as the
     // HTML page. The actor representing this fake HTML source is
     // stored in this array, which always has a URL, so check it
     // first.
-    if (source.url in this._htmlDocumentSourceActors) {
+    if (isInlineSource && source.url in this._htmlDocumentSourceActors) {
       return this._htmlDocumentSourceActors[source.url];
     }
 
     let originalUrl = null;
     if (isInlineSource) {
       // If it's an inline source, the fake HTML source hasn't been
       // created yet (would have returned above), so flip this source
       // into a sourcemapped state by giving it an `originalUrl` which
@@ -229,20 +229,20 @@ TabSources.prototype = {
 
     // XXX bug 915433: We can't rely on Debugger.Source.prototype.text
     // if the source is an HTML-embedded <script> tag. Since we don't
     // have an API implemented to detect whether this is the case, we
     // need to be conservative and only treat valid js files as real
     // sources. Otherwise, use the `originalUrl` property to treat it
     // as an HTML source that manages multiple inline sources.
 
-    // Assume the source is inline if the element that introduced it is not a
-    // script element, or does not have a src attribute.
+    // Assume the source is inline if the element that introduced it is a
+    // script element and does not have a src attribute.
     const element = source.element ? source.element.unsafeDereference() : null;
-    if (element && (element.tagName !== "SCRIPT" || !element.hasAttribute("src"))) {
+    if (element && element.tagName === "SCRIPT" && !element.hasAttribute("src")) {
       if (source.introductionScript) {
         // As for other evaluated sources, script elements which were
         // dynamically generated when another script ran should have
         // a javascript content-type.
         spec.contentType = "text/javascript";
       } else {
         spec.isInlineSource = true;
       }
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -1025,17 +1025,24 @@ WebConsoleActor.prototype =
       bindObjectActor: request.bindObjectActor,
       frameActor: request.frameActor,
       url: request.url,
       selectedNodeActor: request.selectedNodeActor,
       selectedObjectActor: request.selectedObjectActor,
     };
     const {mapped} = request;
 
+    // Set a flag on the thread actor which indicates an evaluation is being
+    // done for the client. This can affect how debugger handlers behave.
+    this.parentActor.threadActor.insideClientEvaluation = true;
+
     const evalInfo = evalWithDebugger(input, evalOptions, this);
+
+    this.parentActor.threadActor.insideClientEvaluation = false;
+
     const evalResult = evalInfo.result;
     const helperResult = evalInfo.helperResult;
 
     let result, errorDocURL, errorMessage, errorNotes = null, errorGrip = null,
       frame = null, awaitResult;
     if (evalResult) {
       if ("return" in evalResult) {
         result = evalResult.return;
--- a/devtools/server/tests/unit/test_pause_exceptions-01.js
+++ b/devtools/server/tests/unit/test_pause_exceptions-01.js
@@ -31,31 +31,29 @@ function run_test() {
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
   gThreadClient.addOneTimeListener("paused", function(event, packet) {
     gThreadClient.addOneTimeListener("paused", function(event, packet) {
-      Assert.equal(packet.why.type, "debuggerStatement");
-      Assert.equal(packet.frame.where.line, 9);
+      Assert.equal(packet.why.type, "exception");
+      Assert.equal(packet.why.exception, 42);
       gThreadClient.resume(() => finishClient(gClient));
     });
 
     gThreadClient.pauseOnExceptions(true);
     gThreadClient.resume();
   });
 
   /* eslint-disable */
   gDebuggee.eval("(" + function () {
     function stopMe() {
       debugger;
       throw 42;
     }
     try {
       stopMe();
-    } catch (e) {
-      debugger
-    }
+    } catch (e) {}
   } + ")()");
   /* eslint-enable */
 }
--- a/devtools/server/tests/unit/test_pause_exceptions-02.js
+++ b/devtools/server/tests/unit/test_pause_exceptions-02.js
@@ -30,27 +30,25 @@ function run_test() {
                            });
   });
   do_test_pending();
 }
 
 function test_pause_frame() {
   gThreadClient.pauseOnExceptions(true, false, function() {
     gThreadClient.addOneTimeListener("paused", function(event, packet) {
-      Assert.equal(packet.why.type, "debuggerStatement");
-      Assert.equal(packet.frame.where.line, 8);
+      Assert.equal(packet.why.type, "exception");
+      Assert.equal(packet.why.exception, 42);
       gThreadClient.resume(() => finishClient(gClient));
     });
 
     /* eslint-disable */
     gDebuggee.eval("(" + function () {   // 1
       function stopMe() {                // 2
         throw 42;                        // 3
       }                                  // 4
       try {                              // 5
         stopMe();                        // 6
-      } catch (e) {                      // 7
-        debugger;                        // 8
-      }                                  // 9
+      } catch (e) {}                     // 7
     } + ")()");
     /* eslint-enable */
   });
 }
--- a/devtools/shared/css/generated/properties-db.js
+++ b/devtools/shared/css/generated/properties-db.js
@@ -2912,16 +2912,17 @@ exports.CSS_PROPERTIES = {
       "position",
       "float",
       "clear",
       "vertical-align",
       "overflow-clip-box-inline",
       "overflow-clip-box-block",
       "overflow-x",
       "overflow-y",
+      "overflow-anchor",
       "transition-duration",
       "transition-timing-function",
       "transition-property",
       "transition-delay",
       "animation-name",
       "animation-duration",
       "animation-timing-function",
       "animation-iteration-count",
@@ -7638,16 +7639,30 @@ exports.CSS_PROPERTIES = {
       "hidden",
       "inherit",
       "initial",
       "scroll",
       "unset",
       "visible"
     ]
   },
+  "overflow-anchor": {
+    "isInherited": false,
+    "subproperties": [
+      "overflow-anchor"
+    ],
+    "supports": [],
+    "values": [
+      "auto",
+      "inherit",
+      "initial",
+      "none",
+      "unset"
+    ]
+  },
   "overflow-wrap": {
     "isInherited": true,
     "subproperties": [
       "overflow-wrap"
     ],
     "supports": [],
     "values": [
       "anywhere",
@@ -9372,16 +9387,20 @@ exports.PREFERENCES = [
     "initial-letter",
     "layout.css.initial-letter.enabled"
   ],
   [
     "-moz-osx-font-smoothing",
     "layout.css.osx-font-smoothing.enabled"
   ],
   [
+    "overflow-anchor",
+    "layout.css.scroll-anchoring.enabled"
+  ],
+  [
     "scrollbar-width",
     "layout.css.scrollbar-width.enabled"
   ],
   [
     "text-justify",
     "layout.css.text-justify.enabled"
   ],
   [
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -173,16 +173,17 @@
 #include "mozilla/gfx/2D.h"
 #include "nsSubDocumentFrame.h"
 #include "nsQueryObject.h"
 #include "nsLayoutStylesheetCache.h"
 #include "mozilla/layers/InputAPZContext.h"
 #include "mozilla/layers/FocusTarget.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
 #include "mozilla/layers/WebRenderUserData.h"
+#include "mozilla/layout/ScrollAnchorContainer.h"
 #include "mozilla/ServoBindings.h"
 #include "mozilla/ServoStyleSet.h"
 #include "mozilla/StyleSheet.h"
 #include "mozilla/StyleSheetInlines.h"
 #include "mozilla/dom/ImageTracker.h"
 #include "nsIDocShellTreeOwner.h"
 #include "nsBindingManager.h"
 #include "nsClassHashtable.h"
@@ -1306,16 +1307,17 @@ void PresShell::Destroy() {
   mCurrentEventFrame = nullptr;
 
   int32_t i, count = mCurrentEventFrameStack.Length();
   for (i = 0; i < count; i++) {
     mCurrentEventFrameStack[i] = nullptr;
   }
 
   mFramesToDirty.Clear();
+  mDirtyScrollAnchorContainers.Clear();
 
   if (mViewManager) {
     // Clear the view manager's weak pointer back to |this| in case it
     // was leaked.
     mViewManager->SetPresShell(nullptr);
     mViewManager = nullptr;
   }
 
@@ -2133,16 +2135,21 @@ void PresShell::NotifyDestroyingFrame(ns
         // pop it we can still get its new frame from its content
         nsIContent* currentEventContent = aFrame->GetContent();
         mCurrentEventContentStack.ReplaceObjectAt(currentEventContent, i);
         mCurrentEventFrameStack[i] = nullptr;
       }
     }
 
     mFramesToDirty.RemoveEntry(aFrame);
+
+    nsIScrollableFrame* scrollableFrame = do_QueryFrame(aFrame);
+    if (scrollableFrame) {
+      mDirtyScrollAnchorContainers.RemoveEntry(scrollableFrame);
+    }
   }
 }
 
 already_AddRefed<nsCaret> PresShell::GetCaret() const {
   RefPtr<nsCaret> caret = mCaret;
   return caret.forget();
 }
 
@@ -2554,16 +2561,29 @@ void PresShell::VerifyHasDirtyRootAncest
   }
 
   MOZ_ASSERT_UNREACHABLE(
       "Frame has dirty bits set but isn't scheduled to be "
       "reflowed?");
 }
 #endif
 
+void PresShell::PostDirtyScrollAnchorContainer(nsIScrollableFrame* aFrame) {
+  mDirtyScrollAnchorContainers.PutEntry(aFrame);
+}
+
+void PresShell::FlushDirtyScrollAnchorContainers() {
+  for (auto iter = mDirtyScrollAnchorContainers.Iter(); !iter.Done();
+       iter.Next()) {
+    nsIScrollableFrame* scroll = iter.Get()->GetKey();
+    scroll->GetAnchor()->SelectAnchor();
+  }
+  mDirtyScrollAnchorContainers.Clear();
+}
+
 void PresShell::FrameNeedsReflow(nsIFrame* aFrame,
                                  IntrinsicDirty aIntrinsicDirty,
                                  nsFrameState aBitToAdd,
                                  ReflowRootHandling aRootHandling) {
   MOZ_ASSERT(aBitToAdd == NS_FRAME_IS_DIRTY ||
                  aBitToAdd == NS_FRAME_HAS_DIRTY_CHILDREN || !aBitToAdd,
              "Unexpected bits being added");
 
@@ -8473,16 +8493,18 @@ bool PresShell::DoReflow(nsIFrame* targe
 #ifdef MOZ_GECKO_PROFILER
   DECLARE_DOCSHELL_AND_HISTORY_ID(docShell);
   AutoProfilerTracing tracingLayoutFlush("Paint", "Reflow",
                                          std::move(mReflowCause), docShellId,
                                          docShellHistoryId);
   mReflowCause = nullptr;
 #endif
 
+  FlushDirtyScrollAnchorContainers();
+
   if (mReflowContinueTimer) {
     mReflowContinueTimer->Cancel();
     mReflowContinueTimer = nullptr;
   }
 
   const bool isRoot = target == mFrameConstructor->GetRootFrame();
 
   MOZ_ASSERT(isRoot || aOverflowTracker,
@@ -8592,16 +8614,20 @@ bool PresShell::DoReflow(nsIFrame* targe
   // "window dimensions" code depends on it.
   nsContainerFrame::SyncFrameViewAfterReflow(
       mPresContext, target, target->GetView(), boundsRelativeToTarget);
   nsContainerFrame::SyncWindowProperties(mPresContext, target,
                                          target->GetView(), rcx,
                                          nsContainerFrame::SET_ASYNC);
 
   target->DidReflow(mPresContext, nullptr);
+  if (target->IsInScrollAnchorChain()) {
+    ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(target);
+    container->ApplyAdjustments();
+  }
   if (isRoot && size.BSize(wm) == NS_UNCONSTRAINEDSIZE) {
     mPresContext->SetVisibleArea(boundsRelativeToTarget);
   }
 
 #ifdef DEBUG
   mCurrentReflowRoot = nullptr;
 #endif
 
@@ -10002,17 +10028,18 @@ void PresShell::AddSizeOfIncludingThis(n
   MallocSizeOf mallocSizeOf = aSizes.mState.mMallocSizeOf;
   mFrameArena.AddSizeOfExcludingThis(aSizes);
   aSizes.mLayoutPresShellSize += mallocSizeOf(this);
   if (mCaret) {
     aSizes.mLayoutPresShellSize += mCaret->SizeOfIncludingThis(mallocSizeOf);
   }
   aSizes.mLayoutPresShellSize +=
       mApproximatelyVisibleFrames.ShallowSizeOfExcludingThis(mallocSizeOf) +
-      mFramesToDirty.ShallowSizeOfExcludingThis(mallocSizeOf);
+      mFramesToDirty.ShallowSizeOfExcludingThis(mallocSizeOf) +
+      mDirtyScrollAnchorContainers.ShallowSizeOfExcludingThis(mallocSizeOf);
 
   StyleSet()->AddSizeOfIncludingThis(aSizes);
 
   aSizes.mLayoutTextRunsSize += SizeOfTextRuns(mallocSizeOf);
 
   aSizes.mLayoutPresContextSize +=
       mPresContext->SizeOfIncludingThis(mallocSizeOf);
 
--- a/layout/base/PresShell.h
+++ b/layout/base/PresShell.h
@@ -108,16 +108,19 @@ class PresShell final : public nsIPresSh
       nscoord aOldHeight = 0,
       ResizeReflowOptions aOptions = ResizeReflowOptions::eBSizeExact) override;
   nsresult ResizeReflowIgnoreOverride(
       nscoord aWidth, nscoord aHeight, nscoord aOldWidth, nscoord aOldHeight,
       ResizeReflowOptions aOptions = ResizeReflowOptions::eBSizeExact) override;
   nsIPageSequenceFrame* GetPageSequenceFrame() const override;
   nsCanvasFrame* GetCanvasFrame() const override;
 
+  void PostDirtyScrollAnchorContainer(nsIScrollableFrame* aFrame) override;
+  void FlushDirtyScrollAnchorContainers() override;
+
   void FrameNeedsReflow(
       nsIFrame* aFrame, IntrinsicDirty aIntrinsicDirty, nsFrameState aBitToAdd,
       ReflowRootHandling aRootHandling = eInferFromBitToAdd) override;
   void FrameNeedsToContinueReflow(nsIFrame* aFrame) override;
   void CancelAllPendingReflows() override;
   void DoFlushPendingNotifications(FlushType aType) override;
   void DoFlushPendingNotifications(ChangesToFlush aType) override;
 
@@ -739,16 +742,17 @@ class PresShell final : public nsIPresSh
   layers::ScrollableLayerGuid mMouseEventTargetGuid;
 
   // mStyleSet owns it but we maintain a ref, may be null
   RefPtr<StyleSheet> mPrefStyleSheet;
 
   // Set of frames that we should mark with NS_FRAME_HAS_DIRTY_CHILDREN after
   // we finish reflowing mCurrentReflowRoot.
   nsTHashtable<nsPtrHashKey<nsIFrame>> mFramesToDirty;
+  nsTHashtable<nsPtrHashKey<nsIScrollableFrame>> mDirtyScrollAnchorContainers;
 
   nsTArray<UniquePtr<DelayedEvent>> mDelayedEvents;
 
  private:
   nsRevocableEventPtr<nsSynthMouseMoveEvent> mSynthMouseMoveEvent;
   nsCOMPtr<nsIContent> mLastAnchorScrolledTo;
   RefPtr<nsCaret> mCaret;
   RefPtr<nsCaret> mOriginalCaret;
--- a/layout/base/RestyleManager.cpp
+++ b/layout/base/RestyleManager.cpp
@@ -9,16 +9,17 @@
 #include "mozilla/AutoRestyleTimelineMarker.h"
 #include "mozilla/AutoTimelineMarker.h"
 #include "mozilla/ComputedStyle.h"
 #include "mozilla/ComputedStyleInlines.h"
 #include "mozilla/DocumentStyleRootIterator.h"
 #include "mozilla/GeckoBindings.h"
 #include "mozilla/LayerAnimationInfo.h"
 #include "mozilla/layers/AnimationInfo.h"
+#include "mozilla/layout/ScrollAnchorContainer.h"
 #include "mozilla/ServoBindings.h"
 #include "mozilla/ServoStyleSetInlines.h"
 #include "mozilla/Unused.h"
 #include "mozilla/ViewportFrame.h"
 #include "mozilla/dom/ChildIterator.h"
 #include "mozilla/dom/DocumentInlines.h"
 #include "mozilla/dom/ElementInlines.h"
 #include "mozilla/dom/HTMLBodyElement.h"
@@ -768,16 +769,20 @@ static bool RecomputePosition(nsIFrame* 
           cont->AddProperty(nsIFrame::NormalPositionProperty(),
                             new nsPoint(normalPosition));
         }
         cont->SetPosition(normalPosition +
                           nsPoint(newOffsets.left, newOffsets.top));
       }
     }
 
+    if (aFrame->IsInScrollAnchorChain()) {
+      ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(aFrame);
+      container->ApplyAdjustments();
+    }
     return true;
   }
 
   // For the absolute positioning case, set up a fake HTML reflow state for
   // the frame, and then get the offsets and size from it. If the frame's size
   // doesn't need to change, we can simply update the frame position. Otherwise
   // we fall back to a reflow.
   RefPtr<gfxContext> rc =
@@ -873,16 +878,20 @@ static bool RecomputePosition(nsIFrame* 
 
     // Move the frame
     nsPoint pos(parentBorder.left + reflowInput.ComputedPhysicalOffsets().left +
                     reflowInput.ComputedPhysicalMargin().left,
                 parentBorder.top + reflowInput.ComputedPhysicalOffsets().top +
                     reflowInput.ComputedPhysicalMargin().top);
     aFrame->SetPosition(pos);
 
+    if (aFrame->IsInScrollAnchorChain()) {
+      ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(aFrame);
+      container->ApplyAdjustments();
+    }
     return true;
   }
 
   // Fall back to a reflow
   StyleChangeReflow(aFrame, nsChangeHint_NeedReflow |
                                 nsChangeHint_ReflowChangesSizeOrPosition);
   return false;
 }
@@ -1485,27 +1494,72 @@ void RestyleManager::ProcessRestyledFram
         }
         // Don't remove NS_FRAME_MAY_BE_TRANSFORMED since it may still be
         // transformed by other means. It's OK to have the bit even if it's
         // not needed.
       }
     }
 
     if (hint & nsChangeHint_ReconstructFrame) {
+      // Record whether this frame was absolutely positioned before and after
+      // frame construction, to detect changes for scroll anchor adjustment
+      // suppression.
+      bool wasAbsPosStyle = false;
+      ScrollAnchorContainer* previousAnchorContainer = nullptr;
+      AutoWeakFrame previousAnchorContainerFrame;
+      if (frame) {
+        wasAbsPosStyle = frame->StyleDisplay()->IsAbsolutelyPositionedStyle();
+        previousAnchorContainer = ScrollAnchorContainer::FindFor(frame);
+
+        // It's possible for the scroll anchor container to be destroyed by
+        // frame construction, so use a weak frame to detect this.
+        if (previousAnchorContainer) {
+          previousAnchorContainerFrame = previousAnchorContainer->Frame();
+        }
+      }
+
       // If we ever start passing true here, be careful of restyles
       // that involve a reframe and animations.  In particular, if the
       // restyle we're processing here is an animation restyle, but
       // the style resolution we will do for the frame construction
       // happens async when we're not in an animation restyle already,
       // problems could arise.
       // We could also have problems with triggering of CSS transitions
       // on elements whose frames are reconstructed, since we depend on
       // the reconstruction happening synchronously.
       frameConstructor->RecreateFramesForContent(
           content, nsCSSFrameConstructor::InsertionKind::Sync);
+      frame = content->GetPrimaryFrame();
+
+      // See the check above for absolutely positioned style.
+      bool isAbsPosStyle = false;
+      ScrollAnchorContainer* newAnchorContainer = nullptr;
+      if (frame) {
+        isAbsPosStyle = frame->StyleDisplay()->IsAbsolutelyPositionedStyle();
+        newAnchorContainer = ScrollAnchorContainer::FindFor(frame);
+      }
+
+      // If this frame construction was due to a change in absolute
+      // positioning, then suppress scroll anchor adjustments in the scroll
+      // anchor container the frame was in, and the one it moved into.
+      //
+      // This isn't entirely accurate to the specification, which requires us
+      // to do this for all frames that change being absolutely positioned. It's
+      // possible for multiple style changes to cause frame reconstruction and
+      // coalesce, which could cause a suppression trigger to be missed. It's
+      // unclear whether this will be an issue as suppression triggers are just
+      // heuristics.
+      if (wasAbsPosStyle != isAbsPosStyle) {
+        if (previousAnchorContainerFrame) {
+          previousAnchorContainer->SuppressAdjustments();
+        }
+        if (newAnchorContainer) {
+          newAnchorContainer->SuppressAdjustments();
+        }
+      }
     } else {
       NS_ASSERTION(frame, "This shouldn't happen");
 
       if (!frame->FrameMaintainsOverflow()) {
         // frame does not maintain overflow rects, so avoid calling
         // FinishAndStoreOverflow on it:
         hint &=
             ~(nsChangeHint_UpdateOverflow | nsChangeHint_ChildrenOnlyTransform |
@@ -2910,16 +2964,21 @@ void RestyleManager::DoProcessPendingRes
     // PresShell::FlushPendingNotifications doesn't early-return in the case
     // where the PresShell hasn't yet been initialized (and therefore we haven't
     // yet done the initial style traversal of the DOM tree). We should arguably
     // fix up the callers and assert against this case, but we just detect and
     // handle it for now.
     return;
   }
 
+  // Select scroll anchors for frames that have been scrolled. Do this
+  // before restyling so that anchor nodes are correctly marked for
+  // scroll anchor update suppressions.
+  presContext->PresShell()->FlushDirtyScrollAnchorContainers();
+
   // Create a AnimationsWithDestroyedFrame during restyling process to
   // stop animations and transitions on elements that have no frame at the end
   // of the restyling process.
   AnimationsWithDestroyedFrame animationsWithDestroyedFrame(this);
 
   ServoStyleSet* styleSet = StyleSet();
   Document* doc = presContext->Document();
 
--- a/layout/base/nsIPresShell.h
+++ b/layout/base/nsIPresShell.h
@@ -449,16 +449,19 @@ class nsIPresShell : public nsStubDocume
   virtual nsIPageSequenceFrame* GetPageSequenceFrame() const = 0;
 
   /**
    * Returns the canvas frame associated with the frame hierarchy.
    * Returns nullptr if is XUL document.
    */
   virtual nsCanvasFrame* GetCanvasFrame() const = 0;
 
+  virtual void PostDirtyScrollAnchorContainer(nsIScrollableFrame* aFrame) = 0;
+  virtual void FlushDirtyScrollAnchorContainers() = 0;
+
   /**
    * Tell the pres shell that a frame needs to be marked dirty and needs
    * Reflow.  It's OK if this is an ancestor of the frame needing reflow as
    * long as the ancestor chain between them doesn't cross a reflow root.
    *
    * The bit to add should be NS_FRAME_IS_DIRTY, NS_FRAME_HAS_DIRTY_CHILDREN
    * or nsFrameState(0); passing 0 means that dirty bits won't be set on the
    * frame or its ancestors/descendants, but that intrinsic widths will still
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -1485,21 +1485,21 @@ bool nsLayoutUtils::IsAncestorFrameCross
   for (const nsIFrame* f = aFrame; f != aCommonAncestor;
        f = GetCrossDocParentFrame(f)) {
     if (f == aAncestorFrame) return true;
   }
   return aCommonAncestor == aAncestorFrame;
 }
 
 // static
-bool nsLayoutUtils::IsProperAncestorFrame(nsIFrame* aAncestorFrame,
-                                          nsIFrame* aFrame,
-                                          nsIFrame* aCommonAncestor) {
+bool nsLayoutUtils::IsProperAncestorFrame(const nsIFrame* aAncestorFrame,
+                                          const nsIFrame* aFrame,
+                                          const nsIFrame* aCommonAncestor) {
   if (aFrame == aAncestorFrame) return false;
-  for (nsIFrame* f = aFrame; f != aCommonAncestor; f = f->GetParent()) {
+  for (const nsIFrame* f = aFrame; f != aCommonAncestor; f = f->GetParent()) {
     if (f == aAncestorFrame) return true;
   }
   return aCommonAncestor == aAncestorFrame;
 }
 
 // static
 int32_t nsLayoutUtils::DoCompareTreePosition(
     nsIContent* aContent1, nsIContent* aContent2, int32_t aIf1Ancestor,
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -523,18 +523,19 @@ class nsLayoutUtils {
 
   /**
    * IsProperAncestorFrame checks whether aAncestorFrame is an ancestor
    * of aFrame and not equal to aFrame.
    * @param aCommonAncestor nullptr, or a common ancestor of aFrame and
    * aAncestorFrame. If non-null, this can bound the search and speed up
    * the function
    */
-  static bool IsProperAncestorFrame(nsIFrame* aAncestorFrame, nsIFrame* aFrame,
-                                    nsIFrame* aCommonAncestor = nullptr);
+  static bool IsProperAncestorFrame(const nsIFrame* aAncestorFrame,
+                                    const nsIFrame* aFrame,
+                                    const nsIFrame* aCommonAncestor = nullptr);
 
   /**
    * Like IsProperAncestorFrame, but looks across document boundaries.
    *
    * Just like IsAncestorFrameCrossDoc, except that it returns false when
    * aFrame == aAncestorFrame.
    */
   static bool IsProperAncestorFrameCrossDoc(
new file mode 100644
--- /dev/null
+++ b/layout/generic/ScrollAnchorContainer.cpp
@@ -0,0 +1,514 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScrollAnchorContainer.h"
+
+#include "mozilla/StaticPrefs.h"
+#include "nsGfxScrollFrame.h"
+#include "nsLayoutUtils.h"
+
+#define ANCHOR_LOG(...)
+// #define ANCHOR_LOG(...) printf_stderr("ANCHOR: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layout {
+
+ScrollAnchorContainer::ScrollAnchorContainer(ScrollFrameHelper* aScrollFrame)
+    : mScrollFrame(aScrollFrame),
+      mAnchorNode(nullptr),
+      mLastAnchorPos(0, 0),
+      mAnchorNodeIsDirty(true),
+      mApplyingAnchorAdjustment(false),
+      mSuppressAnchorAdjustment(false) {}
+
+ScrollAnchorContainer::~ScrollAnchorContainer() {}
+
+ScrollAnchorContainer* ScrollAnchorContainer::FindFor(nsIFrame* aFrame) {
+  aFrame = aFrame->GetParent();
+  if (!aFrame) {
+    return nullptr;
+  }
+  nsIScrollableFrame* nearest = nsLayoutUtils::GetNearestScrollableFrame(
+      aFrame, nsLayoutUtils::SCROLLABLE_SAME_DOC |
+                  nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
+  if (nearest) {
+    return nearest->GetAnchor();
+  }
+  return nullptr;
+}
+
+nsIFrame* ScrollAnchorContainer::Frame() const { return mScrollFrame->mOuter; }
+
+nsIScrollableFrame* ScrollAnchorContainer::ScrollableFrame() const {
+  return Frame()->GetScrollTargetFrame();
+}
+
+/**
+ * Set the appropriate frame flags for a frame that has become or is no longer
+ * an anchor node.
+ */
+static void SetAnchorFlags(const nsIFrame* aScrolledFrame,
+                           nsIFrame* aAnchorNode, bool aInScrollAnchorChain) {
+  nsIFrame* frame = aAnchorNode;
+  while (frame && frame != aScrolledFrame) {
+    MOZ_ASSERT(
+        frame == aAnchorNode || !frame->IsScrollFrame(),
+        "We shouldn't select an anchor node inside a nested scroll frame.");
+
+    frame->SetInScrollAnchorChain(aInScrollAnchorChain);
+    frame = frame->GetParent();
+  }
+  MOZ_ASSERT(frame,
+             "The anchor node should be a descendant of the scroll frame");
+  // If needed, invalidate the frame so that we start/stop highlighting the
+  // anchor
+  if (StaticPrefs::layout_css_scroll_anchoring_highlight()) {
+    for (nsIFrame* frame = aAnchorNode->FirstContinuation(); !!frame;
+         frame = frame->GetNextContinuation()) {
+      frame->InvalidateFrame();
+    }
+  }
+}
+
+/**
+ * Compute the scrollable overflow rect [1] of aCandidate relative to
+ * aScrollFrame with all transforms applied. The specification is also
+ * ambiguous about what can be selected as a scroll anchor, which makes
+ * the scroll anchoring bounding rect partially undefined [2]. This code
+ * attempts to match the implementation in Blink.
+ *
+ * [1]
+ * https://drafts.csswg.org/css-scroll-anchoring-1/#scroll-anchoring-bounding-rect
+ * [2] https://github.com/w3c/csswg-drafts/issues/3478
+ */
+static nsRect FindScrollAnchoringBoundingRect(const nsIFrame* aScrollFrame,
+                                              nsIFrame* aCandidate) {
+  MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrame(aScrollFrame, aCandidate));
+  if (aCandidate->GetContent()->IsText()) {
+    nsRect bounding;
+    for (nsIFrame* continuation = aCandidate->FirstContinuation(); continuation;
+         continuation = continuation->GetNextContinuation()) {
+      nsRect localRect =
+          continuation->GetScrollableOverflowRectRelativeToSelf();
+      nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor(
+          continuation, localRect, aScrollFrame);
+      bounding = bounding.Union(transformed);
+    }
+    return bounding;
+  }
+
+  nsRect localRect = aCandidate->GetScrollableOverflowRectRelativeToSelf();
+  nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor(
+      aCandidate, localRect, aScrollFrame);
+  return transformed;
+}
+
+void ScrollAnchorContainer::SelectAnchor() {
+  MOZ_ASSERT(mScrollFrame->mScrolledFrame);
+  MOZ_ASSERT(mAnchorNodeIsDirty);
+
+  if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) {
+    return;
+  }
+
+  ANCHOR_LOG("Selecting anchor for %p with scroll-port [%d %d x %d %d].\n",
+             this, mScrollFrame->mScrollPort.x, mScrollFrame->mScrollPort.y,
+             mScrollFrame->mScrollPort.width, mScrollFrame->mScrollPort.height);
+
+  const nsStyleDisplay* disp = Frame()->StyleDisplay();
+
+  // Don't select a scroll anchor if the scroll frame has `overflow-anchor:
+  // none`.
+  bool overflowAnchor =
+      disp->mOverflowAnchor == mozilla::StyleOverflowAnchor::Auto;
+
+  // Or if the scroll frame has not been scrolled from the logical origin. This
+  // is not in the specification [1], but Blink does this.
+  //
+  // [1] https://github.com/w3c/csswg-drafts/issues/3319
+  bool isScrolled = mScrollFrame->GetLogicalScrollPosition() != nsPoint();
+
+  // Or if there is perspective that could affect the scrollable overflow rect
+  // for descendant frames. This is not in the specification as Blink doesn't
+  // share this behavior with perspective [1].
+  //
+  // [1] https://github.com/w3c/csswg-drafts/issues/3322
+  bool hasPerspective = Frame()->ChildrenHavePerspective();
+
+  // Select a new scroll anchor
+  nsIFrame* oldAnchor = mAnchorNode;
+  if (overflowAnchor && isScrolled && !hasPerspective) {
+    ANCHOR_LOG("Beginning candidate selection.\n");
+    mAnchorNode = FindAnchorIn(mScrollFrame->mScrolledFrame);
+  } else {
+    if (!overflowAnchor) {
+      ANCHOR_LOG("Skipping candidate selection for `overflow-anchor: none`\n");
+    }
+    if (!isScrolled) {
+      ANCHOR_LOG("Skipping candidate selection for not being scrolled\n");
+    }
+    if (hasPerspective) {
+      ANCHOR_LOG(
+          "Skipping candidate selection for scroll frame with perspective\n");
+    }
+    mAnchorNode = nullptr;
+  }
+
+  // Update the anchor flags if needed
+  if (oldAnchor != mAnchorNode) {
+    ANCHOR_LOG("Anchor node has changed from (%p) to (%p).\n", oldAnchor,
+               mAnchorNode);
+
+    // Unset all flags for the old scroll anchor
+    if (oldAnchor) {
+      SetAnchorFlags(mScrollFrame->mScrolledFrame, oldAnchor, false);
+    }
+
+    // Set all flags for the new scroll anchor
+    if (mAnchorNode) {
+      // Anchor selection will never select a descendant of a different scroll
+      // frame, so we can set flags without conflicting with other scroll
+      // anchor containers.
+      SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, true);
+    }
+  } else {
+    ANCHOR_LOG("Anchor node has remained (%p).\n", mAnchorNode);
+  }
+
+  // Calculate the position to use for scroll adjustments
+  if (mAnchorNode) {
+    mLastAnchorPos =
+        FindScrollAnchoringBoundingRect(Frame(), mAnchorNode).TopLeft();
+    ANCHOR_LOG("Using last anchor position = [%d, %d].\n", mLastAnchorPos.x,
+               mLastAnchorPos.y);
+  } else {
+    mLastAnchorPos = nsPoint();
+  }
+
+  mAnchorNodeIsDirty = false;
+}
+
+void ScrollAnchorContainer::UserScrolled() {
+  if (mApplyingAnchorAdjustment) {
+    return;
+  }
+  InvalidateAnchor();
+}
+
+void ScrollAnchorContainer::SuppressAdjustments() {
+  ANCHOR_LOG("Received a scroll anchor suppression for %p.\n", this);
+  mSuppressAnchorAdjustment = true;
+}
+
+void ScrollAnchorContainer::InvalidateAnchor() {
+  if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) {
+    return;
+  }
+
+  ANCHOR_LOG("Invalidating scroll anchor %p for %p.\n", mAnchorNode, this);
+
+  if (mAnchorNode) {
+    SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, false);
+  }
+  mAnchorNode = nullptr;
+  mAnchorNodeIsDirty = true;
+  mLastAnchorPos = nsPoint();
+  Frame()->PresShell()->PostDirtyScrollAnchorContainer(ScrollableFrame());
+}
+
+void ScrollAnchorContainer::Destroy() {
+  if (mAnchorNode) {
+    SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, false);
+  }
+  mAnchorNode = nullptr;
+  mAnchorNodeIsDirty = false;
+  mLastAnchorPos = nsPoint();
+}
+
+void ScrollAnchorContainer::ApplyAdjustments() {
+  if (!mAnchorNode || mAnchorNodeIsDirty) {
+    mSuppressAnchorAdjustment = false;
+    ANCHOR_LOG("Ignoring post-reflow (anchor=%p, dirty=%d, container=%p).\n",
+               mAnchorNode, mAnchorNodeIsDirty, this);
+    return;
+  }
+
+  nsPoint current =
+      FindScrollAnchoringBoundingRect(Frame(), mAnchorNode).TopLeft();
+  nsPoint adjustment = current - mLastAnchorPos;
+  nsIntPoint adjustmentDevicePixels =
+      adjustment.ToNearestPixels(Frame()->PresContext()->AppUnitsPerDevPixel());
+
+  ANCHOR_LOG("Anchor has moved from [%d, %d] to [%d, %d].\n", mLastAnchorPos.x,
+             mLastAnchorPos.y, current.x, current.y);
+
+  WritingMode writingMode = Frame()->GetWritingMode();
+
+  // The specification only allows for anchor adjustments in the block
+  // dimension, so remove the other component.
+  if (writingMode.IsVertical()) {
+    adjustmentDevicePixels.y = 0;
+  } else {
+    adjustmentDevicePixels.x = 0;
+  }
+
+  if (adjustmentDevicePixels == nsIntPoint()) {
+    ANCHOR_LOG("Ignoring zero delta anchor adjustment for %p.\n", this);
+    mSuppressAnchorAdjustment = false;
+    return;
+  }
+
+  if (mSuppressAnchorAdjustment) {
+    ANCHOR_LOG("Applying anchor adjustment suppression for %p.\n", this);
+    mSuppressAnchorAdjustment = false;
+    InvalidateAnchor();
+    return;
+  }
+
+  ANCHOR_LOG("Applying anchor adjustment of (%d %d) for %p and anchor %p.\n",
+             adjustment.x, adjustment.y, this, mAnchorNode);
+
+  MOZ_ASSERT(!mApplyingAnchorAdjustment);
+  // We should use AutoRestore here, but that doesn't work with bitfields
+  mApplyingAnchorAdjustment = true;
+  mScrollFrame->ScrollBy(
+      adjustmentDevicePixels, nsIScrollableFrame::DEVICE_PIXELS,
+      nsIScrollableFrame::INSTANT, nullptr, nsGkAtoms::relative);
+  mApplyingAnchorAdjustment = false;
+
+  // The anchor position may not be in the same relative position after
+  // adjustment. Update ourselves so we have consistent state.
+  mLastAnchorPos =
+      FindScrollAnchoringBoundingRect(Frame(), mAnchorNode).TopLeft();
+}
+
+ScrollAnchorContainer::ExamineResult
+ScrollAnchorContainer::ExamineAnchorCandidate(nsIFrame* aFrame) const {
+#ifdef DEBUG_FRAME_DUMP
+  nsCString tag = aFrame->ListTag();
+  ANCHOR_LOG("\tVisiting frame=%s (%p).\n", tag.get(), aFrame);
+#else
+  ANCHOR_LOG("\t\tVisiting frame=%p.\n", aFrame);
+#endif
+
+  // Check if the author has opted out of scroll anchoring for this frame
+  // and its descendants.
+  const nsStyleDisplay* disp = aFrame->StyleDisplay();
+  if (disp->mOverflowAnchor == mozilla::StyleOverflowAnchor::None) {
+    ANCHOR_LOG("\t\tExcluding `overflow-anchor: none`.\n");
+    return ExamineResult::Exclude;
+  }
+
+  // Sticky positioned elements can move with the scroll frame, making them
+  // unsuitable scroll anchors. This isn't in the specification yet [1], but
+  // matches Blink's implementation.
+  //
+  // [1] https://github.com/w3c/csswg-drafts/issues/3319
+  if (aFrame->IsStickyPositioned()) {
+    ANCHOR_LOG("\t\tExcluding `position: sticky`.\n");
+    return ExamineResult::Exclude;
+  }
+
+  // The frame for a <br> element has a non-zero area, but Blink treats them
+  // as if they have no area, so exclude them specially.
+  if (aFrame->IsBrFrame()) {
+    ANCHOR_LOG("\t\tExcluding <br>.\n");
+    return ExamineResult::Exclude;
+  }
+
+  // Exclude frames that aren't accessible to content.
+  bool isChrome =
+      aFrame->GetContent() && aFrame->GetContent()->ChromeOnlyAccess();
+  bool isPseudo = aFrame->Style()->IsPseudoElement();
+  if (isChrome && !isPseudo) {
+    ANCHOR_LOG("\t\tExcluding chrome only content.\n");
+    return ExamineResult::Exclude;
+  }
+
+  // See if this frame could have its own anchor node. We could check
+  // IsScrollFrame(), but that would miss nsListControlFrame which is not a
+  // scroll frame, but still inherits from nsHTMLScrollFrame.
+  nsIScrollableFrame* scrollable = do_QueryFrame(aFrame);
+
+  // We don't allow scroll anchors to be selected inside of scrollable frames as
+  // it's not clear how an anchor adjustment should apply to multiple scrollable
+  // frames. Blink allows this to happen, but they're not sure why [1].
+  //
+  // We also don't allow scroll anchors to be selected inside of SVG as it uses
+  // a different layout model than CSS, and the specification doesn't say it
+  // should apply.
+  //
+  // [1] https://github.com/w3c/csswg-drafts/issues/3477
+  bool canDescend = !scrollable && !aFrame->IsSVGOuterSVGFrame();
+
+  // Check what kind of frame this is
+  bool isBlockOutside = aFrame->IsBlockOutside();
+  bool isText = aFrame->GetContent()->IsText();
+  bool isAnonBox = aFrame->Style()->IsAnonBox() && !isText;
+  bool isInlineOutside = aFrame->IsInlineOutside() && !isText;
+  bool isContinuation = !!aFrame->GetPrevContinuation();
+
+  // If the frame is anonymous or inline-outside, search its descendants for a
+  // scroll anchor.
+  if ((isAnonBox || isInlineOutside) && canDescend) {
+    ANCHOR_LOG(
+        "\t\tSearching descendants of anon or inline box (a=%d, i=%d).\n",
+        isAnonBox, isInlineOutside);
+    return ExamineResult::PassThrough;
+  }
+
+  // If the frame is not block-outside or a text node then exclude it.
+  if (!isBlockOutside && !isText) {
+    ANCHOR_LOG("\t\tExcluding non block-outside or text node (b=%d, t=%d).\n",
+               isBlockOutside, isText);
+    return ExamineResult::Exclude;
+  }
+
+  // Find the scroll anchoring bounding rect.
+  nsRect rect = FindScrollAnchoringBoundingRect(Frame(), aFrame);
+  ANCHOR_LOG("\t\trect = [%d %d x %d %d].\n", rect.x, rect.y, rect.width,
+             rect.height);
+
+  // Check if this frame is visible in the scroll port. This will exclude rects
+  // with zero sized area. The specification is ambiguous about this [1], but
+  // this matches Blink's implementation.
+  //
+  // [1] https://github.com/w3c/csswg-drafts/issues/3483
+  nsRect visibleRect;
+  if (!visibleRect.IntersectRect(rect, mScrollFrame->mScrollPort)) {
+    return ExamineResult::Exclude;
+  }
+
+  // At this point, if canDescend is true, we should only have visible
+  // non-anonymous frames that are either:
+  //   1. block-outside
+  //   2. text nodes
+  //
+  // It's not clear what the scroll anchoring bounding rect of elements that are
+  // block-outside should be when they are fragmented. For text nodes that are
+  // fragmented, it's specified that we need to consider the union of its line
+  // boxes.
+  //
+  // So for text nodes we handle them by including the union of line boxes in
+  // the bounding rect of the primary frame, and not selecting any
+  // continuations.
+  //
+  // For block-outside elements we choose to consider the bounding rect of each
+  // frame individually, allowing ourselves to descend into any frame, but only
+  // selecting a frame if it's not a continuation.
+  if (canDescend && isContinuation) {
+    ANCHOR_LOG("\t\tSearching descendants of a continuation.\n");
+    return ExamineResult::PassThrough;
+  }
+
+  // If this frame is fully visible, then select it as the scroll anchor.
+  if (visibleRect.IsEqualEdges(rect)) {
+    ANCHOR_LOG("\t\tFully visible, taking.\n");
+    return ExamineResult::Accept;
+  }
+
+  // If we can't descend into this frame, then select it as the scroll anchor.
+  if (!canDescend) {
+    ANCHOR_LOG("\t\tIntersects a frame that we can't descend into, taking.\n");
+    return ExamineResult::Accept;
+  }
+
+  // It must be partially visible and we can descend into this frame. Examine
+  // its children for a better scroll anchor or fall back to this one.
+  ANCHOR_LOG("\t\tIntersects valid candidate, checking descendants.\n");
+  return ExamineResult::Traverse;
+}
+
+nsIFrame* ScrollAnchorContainer::FindAnchorIn(nsIFrame* aFrame) const {
+  // Visit the child lists of this frame
+  for (nsIFrame::ChildListIterator lists(aFrame); !lists.IsDone();
+       lists.Next()) {
+    // Skip child lists that contain out-of-flow frames, we'll visit them by
+    // following placeholders in the in-flow lists so that we visit these
+    // frames in DOM order.
+    // XXX do we actually need to exclude kOverflowOutOfFlowList too?
+    if (lists.CurrentID() == FrameChildListID::kAbsoluteList ||
+        lists.CurrentID() == FrameChildListID::kFixedList ||
+        lists.CurrentID() == FrameChildListID::kFloatList ||
+        lists.CurrentID() == FrameChildListID::kOverflowOutOfFlowList) {
+      continue;
+    }
+
+    // Search the child list, and return if we selected an anchor
+    if (nsIFrame* anchor = FindAnchorInList(lists.CurrentList())) {
+      return anchor;
+    }
+  }
+
+  // The spec requires us to do an extra pass to visit absolutely positioned
+  // frames a second time after all the children of their containing block have
+  // been visited.
+  //
+  // It's not clear why this is needed [1], but it matches Blink's
+  // implementation, and is needed for a WPT test.
+  //
+  // [1] https://github.com/w3c/csswg-drafts/issues/3465
+  const nsFrameList& absPosList =
+      aFrame->GetChildList(FrameChildListID::kAbsoluteList);
+  if (nsIFrame* anchor = FindAnchorInList(absPosList)) {
+    return anchor;
+  }
+
+  return nullptr;
+}
+
+nsIFrame* ScrollAnchorContainer::FindAnchorInList(
+    const nsFrameList& aFrameList) const {
+  for (nsIFrame* child : aFrameList) {
+    // If this is a placeholder, try to follow it to the out of flow frame.
+    nsIFrame* realFrame = nsPlaceholderFrame::GetRealFrameFor(child);
+    if (child != realFrame) {
+      // If the out of flow frame is not a descendant of our scroll frame,
+      // then it must have a different containing block and cannot be an
+      // anchor node.
+      if (!nsLayoutUtils::IsProperAncestorFrame(Frame(), realFrame)) {
+        ANCHOR_LOG(
+            "\t\tSkipping out of flow frame that is not a descendant of the "
+            "scroll frame.\n");
+        continue;
+      }
+      ANCHOR_LOG("\t\tFollowing placeholder to out of flow frame.\n");
+      child = realFrame;
+    }
+
+    // Perform the candidate examination algorithm
+    ExamineResult examine = ExamineAnchorCandidate(child);
+
+    // See the comment before the definition of `ExamineResult` in
+    // `ScrollAnchorContainer.h` for an explanation of this behavior.
+    switch (examine) {
+      case ExamineResult::Exclude: {
+        continue;
+      }
+      case ExamineResult::PassThrough: {
+        nsIFrame* candidate = FindAnchorIn(child);
+        if (!candidate) {
+          continue;
+        }
+        return candidate;
+      }
+      case ExamineResult::Traverse: {
+        nsIFrame* candidate = FindAnchorIn(child);
+        if (!candidate) {
+          return child;
+        }
+        return candidate;
+      }
+      case ExamineResult::Accept: {
+        return child;
+      }
+    }
+  }
+  return nullptr;
+}
+
+}  // namespace layout
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/layout/generic/ScrollAnchorContainer.h
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_layout_ScrollAnchorContainer_h_
+#define mozilla_layout_ScrollAnchorContainer_h_
+
+#include "nsPoint.h"
+
+class nsIFrame;
+namespace mozilla {
+class ScrollFrameHelper;
+}  // namespace mozilla
+
+namespace mozilla {
+namespace layout {
+
+/**
+ * A scroll anchor container finds a descendent element of a scrollable frame
+ * to be an anchor node. After every reflow, the scroll anchor will apply
+ * scroll adjustments to keep the anchor node in the same relative position.
+ *
+ * See: https://drafts.csswg.org/css-scroll-anchoring/
+ */
+class ScrollAnchorContainer final {
+ public:
+  explicit ScrollAnchorContainer(ScrollFrameHelper* aScrollFrame);
+  ~ScrollAnchorContainer();
+
+  /**
+   * Returns the nearest scroll anchor container that could select aFrame as an
+   * anchor node.
+   */
+  static ScrollAnchorContainer* FindFor(nsIFrame* aFrame);
+
+  /**
+   * Returns the frame that is the selected anchor node or null if no anchor
+   * is selected.
+   */
+  nsIFrame* AnchorNode() const { return mAnchorNode; }
+
+  /**
+   * Returns the frame that owns this scroll anchor container. This is always
+   * non-null.
+   */
+  nsIFrame* Frame() const;
+
+  /**
+   * Returns the frame that owns this scroll anchor container as a scrollable
+   * frame. This is always non-null.
+   */
+  nsIScrollableFrame* ScrollableFrame() const;
+
+  /**
+   * Find a suitable anchor node among the descendants of the scrollable frame.
+   * This should only be called after the scroll anchor has been invalidated.
+   */
+  void SelectAnchor();
+
+  /**
+   * Notify the scroll anchor container that its scroll frame has been
+   * scrolled by a user and should invalidate itself.
+   */
+  void UserScrolled();
+
+  /**
+   * Notify the scroll anchor container that a reflow has happened and it
+   * should query its anchor to see if a scroll adjustment needs to occur.
+   */
+  void ApplyAdjustments();
+
+  /**
+   * Notify the scroll anchor container that it should suppress any scroll
+   * adjustment that may happen after the next layout flush.
+   */
+  void SuppressAdjustments();
+
+  /**
+   * Notify this scroll anchor container that its anchor node should be
+   * invalidated and recomputed at the next available opportunity.
+   */
+  void InvalidateAnchor();
+
+  /**
+   * Notify this scroll anchor container that it will be destroyed along with
+   * its parent frame.
+   */
+  void Destroy();
+
+ private:
+  // Represents an assessment of a frame's suitability as a scroll anchor,
+  // from the scroll-anchoring spec's "candidate examination algorithm":
+  // https://drafts.csswg.org/css-scroll-anchoring-1/#candidate-examination
+  enum class ExamineResult {
+    // The frame is an excluded subtree or fully clipped and should be ignored.
+    // This corresponds with step 1 in the algorithm.
+    Exclude,
+    // This frame is an anonymous or inline box and its descendants should be
+    // searched to find an anchor node. If none are found, then continue
+    // searching. This is implied by the prologue of the algorithm, and
+    // should be made explicit in the spec [1].
+    //
+    // [1] https://github.com/w3c/csswg-drafts/issues/3489
+    PassThrough,
+    // The frame is partially visible and its descendants should be searched to
+    // find an anchor node. If none are found then this frame should be
+    // selected. This corresponds with step 3 in the algorithm.
+    Traverse,
+    // The frame is fully visible and should be selected as an anchor node. This
+    // corresponds with step 2 in the algorithm.
+    Accept,
+  };
+
+  ExamineResult ExamineAnchorCandidate(nsIFrame* aPrimaryFrame) const;
+
+  // Search a frame's children to find an anchor node. Returns the frame for a
+  // valid anchor node, if one was found in the frames descendants, or null
+  // otherwise.
+  nsIFrame* FindAnchorIn(nsIFrame* aFrame) const;
+
+  // Search a child list to find an anchor node. Returns the frame for a valid
+  // anchor node, if one was found in this child list, or null otherwise.
+  nsIFrame* FindAnchorInList(const nsFrameList& aFrameList) const;
+
+  // The owner of this scroll anchor container
+  ScrollFrameHelper* mScrollFrame;
+
+  // The anchor node that we will scroll to keep in the same relative position
+  // after reflows. This may be null if we were not able to select a valid
+  // scroll anchor
+  nsIFrame* mAnchorNode;
+
+  // The last position of the scroll anchor node relative to the scrollable
+  // frame. This is used for calculating the distance to scroll to keep the
+  // anchor node in the same relative position
+  nsPoint mLastAnchorPos;
+
+  // True if we should recalculate our anchor node at the next chance
+  bool mAnchorNodeIsDirty : 1;
+  // True if we are applying a scroll anchor adjustment
+  bool mApplyingAnchorAdjustment : 1;
+  // True if we should suppress anchor adjustments
+  bool mSuppressAnchorAdjustment : 1;
+};
+
+}  // namespace layout
+}  // namespace mozilla
+
+#endif  // mozilla_layout_ScrollAnchorContainer_h_
--- a/layout/generic/moz.build
+++ b/layout/generic/moz.build
@@ -140,16 +140,17 @@ EXPORTS.mozilla += [
     'ReflowInput.h',
     'ReflowOutput.h',
     'ViewportFrame.h',
     'WritingModes.h',
 ]
 
 EXPORTS.mozilla.layout += [
     'FrameChildList.h',
+    'ScrollAnchorContainer.h',
 ]
 
 UNIFIED_SOURCES += [
     'BlockReflowInput.cpp',
     'BRFrame.cpp',
     'ColumnSetWrapperFrame.cpp',
     'CSSAlignUtils.cpp',
     'CSSOrderAwareFrameIterator.cpp',
@@ -197,16 +198,17 @@ UNIFIED_SOURCES += [
     'nsSubDocumentFrame.cpp',
     'nsTextFrame.cpp',
     'nsTextFrameUtils.cpp',
     'nsTextRunTransformations.cpp',
     'nsVideoFrame.cpp',
     'ReflowInput.cpp',
     'ReflowOutput.cpp',
     'RubyUtils.cpp',
+    'ScrollAnchorContainer.cpp',
     'ScrollAnimationBezierPhysics.cpp',
     'ScrollAnimationMSDPhysics.cpp',
     'ScrollbarActivity.cpp',
     'ScrollSnap.cpp',
     'ScrollVelocityQueue.cpp',
     'StickyScrollContainer.cpp',
     'TextOverflow.cpp',
     'ViewportFrame.cpp',
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -17,16 +17,17 @@
 #include "mozilla/ComputedStyle.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/dom/ElementInlines.h"
 #include "mozilla/dom/Selection.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/gfxVars.h"
 #include "mozilla/gfx/PathHelpers.h"
 #include "mozilla/Sprintf.h"
+#include "mozilla/StaticPrefs.h"
 
 #include "nsCOMPtr.h"
 #include "nsFlexContainerFrame.h"
 #include "nsFrameList.h"
 #include "nsPlaceholderFrame.h"
 #include "nsPluginFrame.h"
 #include "nsIBaseWindow.h"
 #include "nsIContent.h"
@@ -106,16 +107,17 @@
 #include "mozilla/LookAndFeel.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/ServoStyleSet.h"
 #include "mozilla/ServoStyleSetInlines.h"
 #include "mozilla/css/ImageLoader.h"
 #include "mozilla/dom/TouchEvent.h"
 #include "mozilla/gfx/Tools.h"
 #include "mozilla/layers/WebRenderUserData.h"
+#include "mozilla/layout/ScrollAnchorContainer.h"
 #include "nsPrintfCString.h"
 #include "ActiveLayerTracker.h"
 
 #include "nsITheme.h"
 
 using namespace mozilla;
 using namespace mozilla::css;
 using namespace mozilla::dom;
@@ -722,16 +724,21 @@ void nsFrame::DestroyFrom(nsIFrame* aDes
     }
   }
 
   if (IsPrimaryFrame()) {
     // This needs to happen before we clear our Properties() table.
     ActiveLayerTracker::TransferActivityToContent(this, mContent);
   }
 
+  ScrollAnchorContainer* anchor = nullptr;
+  if (IsScrollAnchor(&anchor)) {
+    anchor->InvalidateAnchor();
+  }
+
   if (HasCSSAnimations() || HasCSSTransitions() ||
       EffectSet::GetEffectSet(this)) {
     // If no new frame for this element is created by the end of the
     // restyling process, stop animations and transitions for this frame
     RestyleManager::AnimationsWithDestroyedFrame* adf =
         presContext->RestyleManager()->GetAnimationsWithDestroyedFrame();
     // AnimationsWithDestroyedFrame only lives during the restyling process.
     if (adf) {
@@ -1042,47 +1049,81 @@ void nsIFrame::MarkNeedsDisplayItemRebui
   AddAndRemoveImageAssociations(this, oldLayers, newLayers);
 
   oldLayers =
       aOldComputedStyle ? &aOldComputedStyle->StyleSVGReset()->mMask : nullptr;
   newLayers = &StyleSVGReset()->mMask;
   AddAndRemoveImageAssociations(this, oldLayers, newLayers);
 
   if (aOldComputedStyle) {
+    // Detect style changes that should trigger a scroll anchor adjustment
+    // suppression.
+    // https://drafts.csswg.org/css-scroll-anchoring/#suppression-triggers
+    bool needAnchorSuppression = false;
+
     // If we detect a change on margin, padding or border, we store the old
     // values on the frame itself between now and reflow, so if someone
     // calls GetUsed(Margin|Border|Padding)() before the next reflow, we
     // can give an accurate answer.
     // We don't want to set the property if one already exists.
     nsMargin oldValue(0, 0, 0, 0);
     nsMargin newValue(0, 0, 0, 0);
     const nsStyleMargin* oldMargin = aOldComputedStyle->PeekStyleMargin();
     if (oldMargin && oldMargin->GetMargin(oldValue)) {
-      if ((!StyleMargin()->GetMargin(newValue) || oldValue != newValue) &&
-          !HasProperty(UsedMarginProperty())) {
-        AddProperty(UsedMarginProperty(), new nsMargin(oldValue));
+      if (!StyleMargin()->GetMargin(newValue) || oldValue != newValue) {
+        if (!HasProperty(UsedMarginProperty())) {
+          AddProperty(UsedMarginProperty(), new nsMargin(oldValue));
+        }
+        needAnchorSuppression = true;
       }
     }
 
     const nsStylePadding* oldPadding = aOldComputedStyle->PeekStylePadding();
     if (oldPadding && oldPadding->GetPadding(oldValue)) {
-      if ((!StylePadding()->GetPadding(newValue) || oldValue != newValue) &&
-          !HasProperty(UsedPaddingProperty())) {
-        AddProperty(UsedPaddingProperty(), new nsMargin(oldValue));
+      if (!StylePadding()->GetPadding(newValue) || oldValue != newValue) {
+        if (!HasProperty(UsedPaddingProperty())) {
+          AddProperty(UsedPaddingProperty(), new nsMargin(oldValue));
+        }
+        needAnchorSuppression = true;
       }
     }
 
     const nsStyleBorder* oldBorder = aOldComputedStyle->PeekStyleBorder();
     if (oldBorder) {
       oldValue = oldBorder->GetComputedBorder();
       newValue = StyleBorder()->GetComputedBorder();
       if (oldValue != newValue && !HasProperty(UsedBorderProperty())) {
         AddProperty(UsedBorderProperty(), new nsMargin(oldValue));
       }
     }
+
+    if (mInScrollAnchorChain) {
+      const nsStylePosition* oldPosition =
+          aOldComputedStyle->PeekStylePosition();
+      if (oldPosition &&
+          (oldPosition->mOffset != StylePosition()->mOffset ||
+           oldPosition->mWidth != StylePosition()->mWidth ||
+           oldPosition->mMinWidth != StylePosition()->mMinWidth ||
+           oldPosition->mMaxWidth != StylePosition()->mMaxWidth ||
+           oldPosition->mHeight != StylePosition()->mHeight ||
+           oldPosition->mMinHeight != StylePosition()->mMinHeight ||
+           oldPosition->mMaxHeight != StylePosition()->mMaxHeight)) {
+        needAnchorSuppression = true;
+      }
+
+      const nsStyleDisplay* oldDisp = aOldComputedStyle->PeekStyleDisplay();
+      if (oldDisp && (oldDisp->mPosition != StyleDisplay()->mPosition ||
+                      oldDisp->TransformChanged(*StyleDisplay()))) {
+        needAnchorSuppression = true;
+      }
+    }
+
+    if (mInScrollAnchorChain && needAnchorSuppression) {
+      ScrollAnchorContainer::FindFor(this)->SuppressAdjustments();
+    }
   }
 
   ImageLoader* imageLoader = PresContext()->Document()->StyleImageLoader();
   imgIRequest* oldBorderImage =
       aOldComputedStyle
           ? aOldComputedStyle->StyleBorder()->GetBorderImageRequest()
           : nullptr;
   imgIRequest* newBorderImage = StyleBorder()->GetBorderImageRequest();
@@ -3472,16 +3513,27 @@ void nsIFrame::BuildDisplayListForChild(
 
   const bool isPaintingToWindow = aBuilder->IsPaintingToWindow();
   const bool doingShortcut =
       isPaintingToWindow &&
       (child->GetStateBits() & NS_FRAME_SIMPLE_DISPLAYLIST) &&
       // Animations may change the stacking context state.
       !(child->MayHaveTransformAnimation() || child->MayHaveOpacityAnimation());
 
+  if (StaticPrefs::layout_css_scroll_anchoring_highlight()) {
+    if (child->FirstContinuation()->IsScrollAnchor()) {
+      nsRect bounds = child->GetContentRectRelativeToSelf() +
+                      aBuilder->ToReferenceFrame(child);
+      nsDisplaySolidColor* color = MakeDisplayItem<nsDisplaySolidColor>(
+          aBuilder, child, bounds, NS_RGBA(255, 0, 255, 64));
+      color->SetOverrideZIndex(INT32_MAX);
+      aLists.PositionedDescendants()->AppendToTop(color);
+    }
+  }
+
   if (doingShortcut) {
     BuildDisplayListForSimpleChild(aBuilder, child, aLists);
     return;
   }
 
   // dirty rect in child-relative coordinates
   NS_ASSERTION(aBuilder->GetCurrentFrame() == this, "Wrong coord space!");
   const nsPoint offset = child->GetOffsetTo(this);
@@ -9138,16 +9190,38 @@ void nsIFrame::ComputePreserve3DChildren
         if (child->Extend3DContext(childDisp, child->StyleEffects())) {
           child->ComputePreserve3DChildrenOverflow(aOverflowAreas);
         }
       }
     }
   }
 }
 
+bool nsIFrame::IsScrollAnchor(ScrollAnchorContainer** aOutContainer) {
+  if (!mInScrollAnchorChain) {
+    return false;
+  }
+
+  ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(this);
+  if (container->AnchorNode() != this) {
+    return false;
+  }
+
+  if (aOutContainer) {
+    *aOutContainer = container;
+  }
+  return true;
+}
+
+bool nsIFrame::IsInScrollAnchorChain() const { return mInScrollAnchorChain; }
+
+void nsIFrame::SetInScrollAnchorChain(bool aInChain) {
+  mInScrollAnchorChain = aInChain;
+}
+
 uint32_t nsIFrame::GetDepthInFrameTree() const {
   uint32_t result = 0;
   for (nsContainerFrame* ancestor = GetParent(); ancestor;
        ancestor = ancestor->GetParent()) {
     result++;
   }
   return result;
 }
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -1116,16 +1116,22 @@ void nsHTMLScrollFrame::Reflow(nsPresCon
 
   mHelper.UpdatePrevScrolledRect();
 
   aStatus.Reset();  // This type of frame can't be split.
   NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize);
   mHelper.PostOverflowEvent();
 }
 
+void nsHTMLScrollFrame::DidReflow(nsPresContext* aPresContext,
+                                  const ReflowInput* aReflowInput) {
+  nsContainerFrame::DidReflow(aPresContext, aReflowInput);
+  mHelper.mAnchor.ApplyAdjustments();
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 
 #ifdef DEBUG_FRAME_DUMP
 nsresult nsHTMLScrollFrame::GetFrameName(nsAString& aResult) const {
   return MakeFrameName(NS_LITERAL_STRING("HTMLScroll"), aResult);
 }
 #endif
 
@@ -1955,16 +1961,17 @@ ScrollFrameHelper::ScrollFrameHelper(nsC
       mDestination(0, 0),
       mRestorePos(-1, -1),
       mLastPos(-1, -1),
       mApzScrollPos(0, 0),
       mScrollPosForLayerPixelAlignment(-1, -1),
       mLastUpdateFramesPos(-1, -1),
       mDisplayPortAtLastFrameUpdate(),
       mScrollParentID(mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID),
+      mAnchor(this),
       mAllowScrollOriginDowngrade(false),
       mHadDisplayPortAtLastFrameUpdate(false),
       mNeverHasVerticalScrollbar(false),
       mNeverHasHorizontalScrollbar(false),
       mHasVerticalScrollbar(false),
       mHasHorizontalScrollbar(false),
       mFrameIsUpdatingScrollbar(false),
       mDidHistoryRestore(false),
@@ -2735,16 +2742,17 @@ void ScrollFrameHelper::ScrollToImpl(nsP
     // offset. Otherwise, if we perform calculations that depend on this
     // offset (e.g. by using nsIDOMWindowUtils.getVisualViewportOffset()
     // in chrome JS code) before it's updated by the next APZ repaint,
     // we could get incorrect results.
     presContext->PresShell()->SetVisualViewportOffset(pt, curPos);
   }
 
   ScrollVisual();
+  mAnchor.UserScrolled();
 
   bool schedulePaint = true;
   if (nsLayoutUtils::AsyncPanZoomEnabled(mOuter) &&
       !nsLayoutUtils::ShouldDisableApzForElement(content) &&
       gfxPrefs::APZPaintSkipping()) {
     // If APZ is enabled with paint-skipping, there are certain conditions in
     // which we can skip paints:
     // 1) If APZ triggered this scroll, and the tile-aligned displayport is
@@ -4705,16 +4713,18 @@ void ScrollFrameHelper::AppendAnonymousC
   }
 
   if (mResizerContent) {
     aElements.AppendElement(mResizerContent);
   }
 }
 
 void ScrollFrameHelper::Destroy(PostDestroyData& aPostDestroyData) {
+  mAnchor.Destroy();
+
   if (mScrollbarActivity) {
     mScrollbarActivity->Destroy();
     mScrollbarActivity = nullptr;
   }
 
   // Unbind the content created in CreateAnonymousContent later...
   aPostDestroyData.AddAnonymousContent(mHScrollbarContent.forget());
   aPostDestroyData.AddAnonymousContent(mVScrollbarContent.forget());
--- a/layout/generic/nsGfxScrollFrame.h
+++ b/layout/generic/nsGfxScrollFrame.h
@@ -20,16 +20,17 @@
 #include "nsIReflowCallback.h"
 #include "nsBoxLayoutState.h"
 #include "nsQueryFrame.h"
 #include "nsRefreshDriver.h"
 #include "nsExpirationTracker.h"
 #include "TextOverflow.h"
 #include "ScrollVelocityQueue.h"
 #include "mozilla/PresState.h"
+#include "mozilla/layout/ScrollAnchorContainer.h"
 
 class nsPresContext;
 class nsIPresShell;
 class nsIContent;
 class nsAtom;
 class nsIScrollPositionListener;
 
 namespace mozilla {
@@ -47,16 +48,17 @@ class ScrollFrameHelper : public nsIRefl
   typedef nsIFrame::Sides Sides;
   typedef mozilla::CSSIntPoint CSSIntPoint;
   typedef mozilla::layout::ScrollbarActivity ScrollbarActivity;
   typedef mozilla::layers::FrameMetrics FrameMetrics;
   typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
   typedef mozilla::layers::ScrollSnapInfo ScrollSnapInfo;
   typedef mozilla::layers::Layer Layer;
   typedef mozilla::layers::LayerManager LayerManager;
+  typedef mozilla::layout::ScrollAnchorContainer ScrollAnchorContainer;
 
   class AsyncScroll;
   class AsyncSmoothMSDScroll;
 
   ScrollFrameHelper(nsContainerFrame* aOuter, bool aIsRoot);
   ~ScrollFrameHelper();
 
   mozilla::ScrollStyles GetScrollStylesFromFrame() const;
@@ -561,16 +563,18 @@ class ScrollFrameHelper : public nsIRefl
 
   nsRect mPrevScrolledRect;
 
   ScrollableLayerGuid::ViewID mScrollParentID;
 
   // Timer to remove the displayport some time after scrolling has stopped
   nsCOMPtr<nsITimer> mDisplayPortExpiryTimer;
 
+  ScrollAnchorContainer mAnchor;
+
   bool mAllowScrollOriginDowngrade : 1;
   bool mHadDisplayPortAtLastFrameUpdate : 1;
   bool mNeverHasVerticalScrollbar : 1;
   bool mNeverHasHorizontalScrollbar : 1;
   bool mHasVerticalScrollbar : 1;
   bool mHasHorizontalScrollbar : 1;
   bool mFrameIsUpdatingScrollbar : 1;
   bool mDidHistoryRestore : 1;
@@ -718,16 +722,17 @@ class ScrollFrameHelper : public nsIRefl
 class nsHTMLScrollFrame : public nsContainerFrame,
                           public nsIScrollableFrame,
                           public nsIAnonymousContentCreator,
                           public nsIStatefulFrame {
  public:
   typedef mozilla::ScrollFrameHelper ScrollFrameHelper;
   typedef mozilla::CSSIntPoint CSSIntPoint;
   typedef mozilla::ScrollReflowInput ScrollReflowInput;
+  typedef mozilla::layout::ScrollAnchorContainer ScrollAnchorContainer;
   friend nsHTMLScrollFrame* NS_NewHTMLScrollFrame(nsIPresShell* aPresShell,
                                                   ComputedStyle* aStyle,
                                                   bool aIsRoot);
 
   NS_DECL_QUERYFRAME
   NS_DECL_FRAMEARENA_HELPERS(nsHTMLScrollFrame)
 
   virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
@@ -755,16 +760,18 @@ class nsHTMLScrollFrame : public nsConta
   virtual nscoord GetMinISize(gfxContext* aRenderingContext) override;
   virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override;
   virtual nsresult GetXULPadding(nsMargin& aPadding) override;
   virtual bool IsXULCollapsed() override;
 
   virtual void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
                       const ReflowInput& aReflowInput,
                       nsReflowStatus& aStatus) override;
+  virtual void DidReflow(nsPresContext* aPresContext,
+                         const ReflowInput* aReflowInput) override;
 
   virtual bool ComputeCustomOverflow(nsOverflowAreas& aOverflowAreas) override {
     return mHelper.ComputeCustomOverflow(aOverflowAreas);
   }
 
   bool GetVerticalAlignBaseline(mozilla::WritingMode aWM,
                                 nscoord* aBaseline) const override {
     *aBaseline = GetLogicalBaseline(aWM);
@@ -1107,16 +1114,24 @@ class nsHTMLScrollFrame : public nsConta
   virtual void AsyncScrollbarDragRejected() override {
     return mHelper.AsyncScrollbarDragRejected();
   }
 
   virtual bool IsRootScrollFrameOfDocument() const override {
     return mHelper.IsRootScrollFrameOfDocument();
   }
 
+  virtual const ScrollAnchorContainer* GetAnchor() const override {
+    return &mHelper.mAnchor;
+  }
+
+  virtual ScrollAnchorContainer* GetAnchor() override {
+    return &mHelper.mAnchor;
+  }
+
   // Return the scrolled frame.
   void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override {
     aResult.AppendElement(OwnedAnonBox(mHelper.GetScrolledFrame()));
   }
 
 #ifdef DEBUG_FRAME_DUMP
   virtual nsresult GetFrameName(nsAString& aResult) const override;
 #endif
@@ -1169,16 +1184,17 @@ class nsHTMLScrollFrame : public nsConta
  */
 class nsXULScrollFrame final : public nsBoxFrame,
                                public nsIScrollableFrame,
                                public nsIAnonymousContentCreator,
                                public nsIStatefulFrame {
  public:
   typedef mozilla::ScrollFrameHelper ScrollFrameHelper;
   typedef mozilla::CSSIntPoint CSSIntPoint;
+  typedef mozilla::layout::ScrollAnchorContainer ScrollAnchorContainer;
 
   NS_DECL_QUERYFRAME
   NS_DECL_FRAMEARENA_HELPERS(nsXULScrollFrame)
 
   friend nsXULScrollFrame* NS_NewXULScrollFrame(nsIPresShell* aPresShell,
                                                 ComputedStyle* aStyle,
                                                 bool aIsRoot,
                                                 bool aClipAllDescendants);
@@ -1572,16 +1588,24 @@ class nsXULScrollFrame final : public ns
   virtual void AsyncScrollbarDragRejected() override {
     return mHelper.AsyncScrollbarDragRejected();
   }
 
   virtual bool IsRootScrollFrameOfDocument() const override {
     return mHelper.IsRootScrollFrameOfDocument();
   }
 
+  virtual const ScrollAnchorContainer* GetAnchor() const override {
+    return &mHelper.mAnchor;
+  }
+
+  virtual ScrollAnchorContainer* GetAnchor() override {
+    return &mHelper.mAnchor;
+  }
+
   // Return the scrolled frame.
   void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override {
     aResult.AppendElement(OwnedAnonBox(mHelper.GetScrolledFrame()));
   }
 
 #ifdef DEBUG_FRAME_DUMP
   virtual nsresult GetFrameName(nsAString& aResult) const override;
 #endif
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -110,16 +110,20 @@ class ServoRestyleState;
 class DisplayItemData;
 class EffectSet;
 
 namespace layers {
 class Layer;
 class LayerManager;
 }  // namespace layers
 
+namespace layout {
+class ScrollAnchorContainer;
+}  // namespace layout
+
 namespace dom {
 class Selection;
 }  // namespace dom
 
 }  // namespace mozilla
 
 //----------------------------------------------------------------------
 
@@ -561,17 +565,18 @@ class nsIFrame : public nsQueryFrame {
         mForceDescendIntoIfVisible(false),
         mBuiltDisplayList(false),
         mFrameIsModified(false),
         mHasOverrideDirtyRegion(false),
         mMayHaveWillChangeBudget(false),
         mIsPrimaryFrame(false),
         mMayHaveTransformAnimation(false),
         mMayHaveOpacityAnimation(false),
-        mAllDescendantsAreInvisible(false) {
+        mAllDescendantsAreInvisible(false),
+        mInScrollAnchorChain(false) {
     mozilla::PodZero(&mOverflow);
   }
 
   nsPresContext* PresContext() const { return Style()->PresContextForFrame(); }
 
   nsIPresShell* PresShell() const { return PresContext()->PresShell(); }
 
   /**
@@ -1842,16 +1847,34 @@ class nsIFrame : public nsQueryFrame {
    * Includes the overflow area of all descendants that participate in the
    * current 3d context into aOverflowAreas.
    */
   void ComputePreserve3DChildrenOverflow(nsOverflowAreas& aOverflowAreas);
 
   void RecomputePerspectiveChildrenOverflow(const nsIFrame* aStartFrame);
 
   /**
+   * Returns whether this frame is the anchor of some ancestor scroll frame. As
+   * this frame is moved, the scroll frame will apply adjustments to keep this
+   * scroll frame in the same relative position.
+   *
+   * aOutContainer will optionally be set to the scroll anchor container for
+   * this frame if this frame is an anchor.
+   */
+  bool IsScrollAnchor(
+      mozilla::layout::ScrollAnchorContainer** aOutContainer = nullptr);
+
+  /**
+   * Returns whether this frame is the anchor of some ancestor scroll frame, or
+   * has a descendant which is the scroll anchor.
+   */
+  bool IsInScrollAnchorChain() const;
+  void SetInScrollAnchorChain(bool aInChain);
+
+  /**
    * Returns the number of ancestors between this and the root of our frame tree
    */
   uint32_t GetDepthInFrameTree() const;
 
   /**
    * Event handling of GUI events.
    *
    * @param aEvent event structure describing the type of event and rge widget
@@ -3843,16 +3866,17 @@ class nsIFrame : public nsQueryFrame {
   inline bool IsBlockInside() const;
   inline bool IsBlockOutside() const;
   inline bool IsInlineOutside() const;
   inline mozilla::StyleDisplay GetDisplay() const;
   inline bool IsFloating() const;
   inline bool IsAbsPosContainingBlock() const;
   inline bool IsFixedPosContainingBlock() const;
   inline bool IsRelativelyPositioned() const;
+  inline bool IsStickyPositioned() const;
   inline bool IsAbsolutelyPositioned(
       const nsStyleDisplay* aStyleDisplay = nullptr) const;
 
   // Does this frame have "column-span: all" style.
   //
   // Note this only checks computed style, but not testing whether the
   // containing block formatting context was established by a multicol. Callers
   // need to use IsColumnSpanInMulticolSubtree() to check whether multi-column
@@ -4287,19 +4311,22 @@ class nsIFrame : public nsQueryFrame {
    *
    * This flag is conservative in that it might sometimes be false even if, in
    * fact, all descendants are invisible.
    * For example; an element is visibility:visible and has a visibility:hidden
    * child. This flag is stil false in such case.
    */
   bool mAllDescendantsAreInvisible : 1;
 
+  /**
+   * True if we are or contain the scroll anchor for a scrollable frame.
+   */
+  bool mInScrollAnchorChain : 1;
+
  protected:
-  // There is a 1-bit gap left here.
-
   // Helpers
   /**
    * Can we stop inside this frame when we're skipping non-rendered whitespace?
    *
    * @param aForward [in] Are we moving forward (or backward) in content order.
    *
    * @param aOffset [in/out] At what offset into the frame to start looking.
    * at offset was reached (whether or not we found a place to stop).
--- a/layout/generic/nsIFrameInlines.h
+++ b/layout/generic/nsIFrameInlines.h
@@ -42,16 +42,20 @@ bool nsIFrame::IsAbsPosContainingBlock()
 bool nsIFrame::IsFixedPosContainingBlock() const {
   return StyleDisplay()->IsFixedPosContainingBlock(this);
 }
 
 bool nsIFrame::IsRelativelyPositioned() const {
   return StyleDisplay()->IsRelativelyPositioned(this);
 }
 
+bool nsIFrame::IsStickyPositioned() const {
+  return StyleDisplay()->IsStickyPositioned(this);
+}
+
 bool nsIFrame::IsAbsolutelyPositioned(
     const nsStyleDisplay* aStyleDisplay) const {
   const nsStyleDisplay* disp = StyleDisplayWithOptionalParam(aStyleDisplay);
   return disp->IsAbsolutelyPositioned(this);
 }
 
 bool nsIFrame::IsBlockInside() const {
   return StyleDisplay()->IsBlockInside(this);
--- a/layout/generic/nsIScrollableFrame.h
+++ b/layout/generic/nsIScrollableFrame.h
@@ -34,28 +34,32 @@ class nsDisplayListBuilder;
 
 namespace mozilla {
 struct ContainerLayerParameters;
 namespace layers {
 struct ScrollMetadata;
 class Layer;
 class LayerManager;
 }  // namespace layers
+namespace layout {
+class ScrollAnchorContainer;
+}  // namespace layout
 }  // namespace mozilla
 
 /**
  * Interface for frames that are scrollable. This interface exposes
  * APIs for examining scroll state, observing changes to scroll state,
  * and triggering scrolling.
  */
 class nsIScrollableFrame : public nsIScrollbarMediator {
  public:
   typedef mozilla::CSSIntPoint CSSIntPoint;
   typedef mozilla::ContainerLayerParameters ContainerLayerParameters;
   typedef mozilla::layers::ScrollSnapInfo ScrollSnapInfo;
+  typedef mozilla::layout::ScrollAnchorContainer ScrollAnchorContainer;
 
   NS_DECL_QUERYFRAME_TARGET(nsIScrollableFrame)
 
   /**
    * Get the frame for the content that we are scrolling within
    * this scrollable frame.
    */
   virtual nsIFrame* GetScrolledFrame() const = 0;
@@ -543,11 +547,17 @@ class nsIScrollableFrame : public nsIScr
   virtual void AsyncScrollbarDragRejected() = 0;
 
   /**
    * Returns whether this scroll frame is the root scroll frame of the document
    * that it is in. Note that some documents don't have root scroll frames at
    * all (ie XUL documents) even though they may contain other scroll frames.
    */
   virtual bool IsRootScrollFrameOfDocument() const = 0;
+
+  /**
+   * Returns the scroll anchor associated with this scrollable frame.
+   */
+  virtual const ScrollAnchorContainer* GetAnchor() const = 0;
+  virtual ScrollAnchorContainer* GetAnchor() = 0;
 };
 
 #endif
--- a/layout/generic/test/test_bug633762.html
+++ b/layout/generic/test/test_bug633762.html
@@ -23,18 +23,17 @@ var doc;
 function runTests() {
   var i = document.getElementById("i");
   doc = i.contentDocument;
   var win = i.contentWindow;
   // set display none on b
   doc.getElementById("b").style.display = "none";
   // flush layout
   doc.documentElement.offsetLeft;
-  // click in middle of iframe document to give it focus
-  synthesizeMouseAtCenter(i, {}, win);
+  // focus on the iframe
   win.focus();
   // record scrolltop
   scrollTopBefore = doc.body.scrollTop;
   // send up arrow key event
   sendKey("UP");
   
   window.requestAnimationFrame(finish);
 }
--- a/layout/style/ServoBindings.toml
+++ b/layout/style/ServoBindings.toml
@@ -127,16 +127,17 @@ rusty-enums = [
     "mozilla::StyleUserSelect",
     "mozilla::StyleImageLayerRepeat",
     "mozilla::StyleImageLayerAttachment",
     "mozilla::StyleBoxDecorationBreak",
     "mozilla::StyleBorderStyle",
     "mozilla::StyleRuleInclusion",
     "mozilla::StyleGridTrackBreadth",
     "mozilla::StyleOverscrollBehavior",
+    "mozilla::StyleOverflowAnchor",
     "mozilla::StyleScrollbarWidth",
     "mozilla::StyleWhiteSpace",
     "mozilla::StyleTextRendering",
     "mozilla::StyleColorAdjust",
     "nsStyleImageType",
     "nsStyleSVGPaintType",
     "nsStyleSVGFallbackType",
     "nsINode_BooleanFlag",
@@ -405,16 +406,17 @@ cbindgen-types = [
     { gecko = "StyleOutlineStyle", servo = "values::computed::OutlineStyle" },
     { gecko = "StyleScrollSnapType", servo = "values::computed::ScrollSnapType" },
     { gecko = "StyleResize", servo = "values::computed::Resize" },
     { gecko = "StyleOverflowClipBox", servo = "values::computed::OverflowClipBox" },
     { gecko = "StyleFloat", servo = "values::computed::Float" },
     { gecko = "StyleOverscrollBehavior", servo = "values::computed::OverscrollBehavior" },
     { gecko = "StyleTextAlign", servo = "values::computed::TextAlign" },
     { gecko = "StyleOverflow", servo = "values::computed::Overflow" },
+    { gecko = "StyleOverflowAnchor", servo = "values::computed::OverflowAnchor" },
 ]
 
 mapped-generic-types = [
     { generic = true, gecko = "mozilla::RustCell", servo = "::std::cell::Cell" },
     { generic = false, gecko = "ServoNodeData", servo = "AtomicRefCell<ElementData>" },
     { generic = false, gecko = "mozilla::ServoWritingMode", servo = "::logical_geometry::WritingMode" },
     { generic = false, gecko = "mozilla::ServoCustomPropertiesMap", servo = "Option<::servo_arc::Arc<::custom_properties::CustomPropertiesMap>>" },
     { generic = false, gecko = "mozilla::ServoRuleNode", servo = "Option<::rule_tree::StrongRuleNode>" },
--- a/layout/style/ServoCSSPropList.mako.py
+++ b/layout/style/ServoCSSPropList.mako.py
@@ -126,16 +126,17 @@ SERIALIZED_PREDEFINED_TYPES = [
     "background::BackgroundSize",
     "basic_shape::ClippingShape",
     "basic_shape::FloatAreaShape",
     "position::HorizontalPosition",
     "position::VerticalPosition",
     "url::ImageUrlOrNone",
     "Appearance",
     "OverscrollBehavior",
+    "OverflowAnchor",
     "OverflowClipBox",
     "ScrollSnapType",
     "Float",
     "Overflow",
 ]
 
 def serialized_by_servo(prop):
     # If the property requires layout information, no such luck.
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -2985,16 +2985,17 @@ nsStyleDisplay::nsStyleDisplay(const nsP
       mOrient(StyleOrient::Inline),
       mIsolation(NS_STYLE_ISOLATION_AUTO),
       mTopLayer(NS_STYLE_TOP_LAYER_NONE),
       mWillChangeBitField(0),
       mTouchAction(NS_STYLE_TOUCH_ACTION_AUTO),
       mScrollBehavior(NS_STYLE_SCROLL_BEHAVIOR_AUTO),
       mOverscrollBehaviorX(StyleOverscrollBehavior::Auto),
       mOverscrollBehaviorY(StyleOverscrollBehavior::Auto),
+      mOverflowAnchor(StyleOverflowAnchor::Auto),
       mScrollSnapTypeX(StyleScrollSnapType::None),
       mScrollSnapTypeY(StyleScrollSnapType::None),
       mScrollSnapPointsX(eStyleUnit_None),
       mScrollSnapPointsY(eStyleUnit_None),
       mBackfaceVisibility(NS_STYLE_BACKFACE_VISIBILITY_VISIBLE),
       mTransformStyle(NS_STYLE_TRANSFORM_STYLE_FLAT),
       mTransformBox(StyleGeometryBox::BorderBox),
       mTransformOrigin{
@@ -3141,16 +3142,22 @@ void nsStyleDisplay::FinishStyle(nsPresC
                                  const nsStyleDisplay* aOldStyle) {
   MOZ_ASSERT(NS_IsMainThread());
 
   mShapeOutside.FinishStyle(aPresContext,
                             aOldStyle ? &aOldStyle->mShapeOutside : nullptr);
   GenerateCombinedIndividualTransform();
 }
 
+static inline bool TransformListChanged(
+    const RefPtr<nsCSSValueSharedList>& aList,
+    const RefPtr<nsCSSValueSharedList>& aNewList) {
+  return !aList != !aNewList || (aList && *aList != *aNewList);
+}
+
 static inline nsChangeHint CompareTransformValues(
     const RefPtr<nsCSSValueSharedList>& aList,
     const RefPtr<nsCSSValueSharedList>& aNewList) {
   nsChangeHint result = nsChangeHint(0);
 
   // Note: If we add a new change hint for transform changes here, we have to
   // modify KeyframeEffect::CalculateCumulativeChangeHint too!
   if (!aList != !aNewList || (aList && *aList != *aNewList)) {
@@ -3419,16 +3426,21 @@ nsChangeHint nsStyleDisplay::CalcDiffere
                 mScrollSnapCoordinate != aNewData.mScrollSnapCoordinate ||
                 mWillChange != aNewData.mWillChange)) {
     hint |= nsChangeHint_NeutralChange;
   }
 
   return hint;
 }
 
+bool nsStyleDisplay::TransformChanged(const nsStyleDisplay& aNewData) const {
+  return TransformListChanged(mSpecifiedTransform,
+                              aNewData.mSpecifiedTransform);
+}
+
 void nsStyleDisplay::GenerateCombinedIndividualTransform() {
   // FIXME(emilio): This should probably be called from somewhere like what we
   // do for image layers, instead of FinishStyle.
   //
   // This does and undoes the work a ton of times in Stylo.
   mIndividualTransform = nullptr;
 
   // Follow the order defined in the spec to append transform functions.
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -1876,16 +1876,18 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
   nsStyleDisplay(const nsStyleDisplay& aOther);
   ~nsStyleDisplay();
 
   void FinishStyle(nsPresContext*, const nsStyleDisplay*);
   const static bool kHasFinishStyle = true;
 
   nsChangeHint CalcDifference(const nsStyleDisplay& aNewData) const;
 
+  bool TransformChanged(const nsStyleDisplay& aNewData) const;
+
   // We guarantee that if mBinding is non-null, so are mBinding->GetURI() and
   // mBinding->mOriginPrincipal.
   RefPtr<mozilla::css::URLValue> mBinding;
   mozilla::StyleDisplay mDisplay;
   mozilla::StyleDisplay mOriginalDisplay;  // saved mDisplay for
                                            //         position:absolute/fixed
                                            //         and float:left/right;
                                            //         otherwise equal to
@@ -1917,16 +1919,17 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
                                 // of the properties in the will-change list
                                 // require a stacking context.
   nsTArray<RefPtr<nsAtom>> mWillChange;
 
   uint8_t mTouchAction;     // NS_STYLE_TOUCH_ACTION_*
   uint8_t mScrollBehavior;  // NS_STYLE_SCROLL_BEHAVIOR_*
   mozilla::StyleOverscrollBehavior mOverscrollBehaviorX;
   mozilla::StyleOverscrollBehavior mOverscrollBehaviorY;
+  mozilla::StyleOverflowAnchor mOverflowAnchor;
   mozilla::StyleScrollSnapType mScrollSnapTypeX;
   mozilla::StyleScrollSnapType mScrollSnapTypeY;
   nsStyleCoord mScrollSnapPointsX;
   nsStyleCoord mScrollSnapPointsY;
   mozilla::Position mScrollSnapDestination;
   nsTArray<mozilla::Position> mScrollSnapCoordinate;
 
   // mSpecifiedTransform is the list of transform functions as
@@ -2103,16 +2106,19 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
     return NS_STYLE_POSITION_ABSOLUTE == mPosition ||
            NS_STYLE_POSITION_FIXED == mPosition;
   }
 
   bool IsRelativelyPositionedStyle() const {
     return NS_STYLE_POSITION_RELATIVE == mPosition ||
            NS_STYLE_POSITION_STICKY == mPosition;
   }
+  bool IsStickyPositionedStyle() const {
+    return NS_STYLE_POSITION_STICKY == mPosition;
+  }
   bool IsPositionForcingStackingContext() const {
     return NS_STYLE_POSITION_STICKY == mPosition ||
            NS_STYLE_POSITION_FIXED == mPosition;
   }
 
   static bool IsRubyDisplayType(mozilla::StyleDisplay aDisplay) {
     return mozilla::StyleDisplay::Ruby == aDisplay ||
            IsInternalRubyDisplayType(aDisplay);
@@ -2224,16 +2230,17 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
   inline bool IsBlockInside(const nsIFrame* aContextFrame) const;
   inline bool IsBlockOutside(const nsIFrame* aContextFrame) const;
   inline bool IsInlineOutside(const nsIFrame* aContextFrame) const;
   inline bool IsOriginalDisplayInlineOutside(
       const nsIFrame* aContextFrame) const;
   inline mozilla::StyleDisplay GetDisplay(const nsIFrame* aContextFrame) const;
   inline bool IsFloating(const nsIFrame* aContextFrame) const;
   inline bool IsRelativelyPositioned(const nsIFrame* aContextFrame) const;
+  inline bool IsStickyPositioned(const nsIFrame* aContextFrame) const;
   inline bool IsAbsolutelyPositioned(const nsIFrame* aContextFrame) const;
 
   // These methods are defined in nsStyleStructInlines.h.
 
   /**
    * Returns true when the element has the transform property
    * or a related property, and supports CSS transforms.
    * aContextFrame is the frame for which this is the nsStyleDisplay.
--- a/layout/style/nsStyleStructInlines.h
+++ b/layout/style/nsStyleStructInlines.h
@@ -206,16 +206,23 @@ bool nsStyleDisplay::IsAbsPosContainingB
 bool nsStyleDisplay::IsRelativelyPositioned(
     const nsIFrame* aContextFrame) const {
   NS_ASSERTION(aContextFrame->StyleDisplay() == this,
                "unexpected aContextFrame");
   return IsRelativelyPositionedStyle() &&
          !nsSVGUtils::IsInSVGTextSubtree(aContextFrame);
 }
 
+bool nsStyleDisplay::IsStickyPositioned(const nsIFrame* aContextFrame) const {
+  NS_ASSERTION(aContextFrame->StyleDisplay() == this,
+               "unexpected aContextFrame");
+  return IsStickyPositionedStyle() &&
+         !nsSVGUtils::IsInSVGTextSubtree(aContextFrame);
+}
+
 bool nsStyleDisplay::IsAbsolutelyPositioned(
     const nsIFrame* aContextFrame) const {
   NS_ASSERTION(aContextFrame->StyleDisplay() == this,
                "unexpected aContextFrame");
   return IsAbsolutelyPositionedStyle() &&
          !nsSVGUtils::IsInSVGTextSubtree(aContextFrame);
 }
 
--- a/layout/style/test/property_database.js
+++ b/layout/style/test/property_database.js
@@ -222,16 +222,23 @@ var validGradientAndElementValues = [
   "repeating-radial-gradient(farthest-side circle at 45px, red, blue)",
   "repeating-radial-gradient(ellipse closest-side at 50%, red, blue)",
   "repeating-radial-gradient(circle farthest-corner at 4em, red, blue)",
 
   "repeating-radial-gradient(30% 40% at top left, red, blue)",
   "repeating-radial-gradient(50px 60px at 15% 20%, red, blue)",
   "repeating-radial-gradient(7em 8em at 45px, red, blue)",
 
+  // FIXME(emilio): We should not be allowing 3-value positions anywhere else
+  // than on `background-position`, see
+  // https://github.com/w3c/csswg-drafts/issues/2140.
+  //
+  // When that happens this should be moved to the `invalid` list.
+  "repeating-radial-gradient(circle closest-side at left bottom 7in, hsl(2,2%,5%), rgb(1,6,0))",
+
   "-moz-image-rect(url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKElEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==), 2, 10, 10, 2)",
   "-moz-image-rect(url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKElEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==), 10%, 50%, 30%, 0%)",
   "-moz-image-rect(url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKElEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg==), 10, 50%, 30%, 0)",
 
   "radial-gradient(at calc(25%) top, red, blue)",
   "radial-gradient(at left calc(25%), red, blue)",
   "radial-gradient(at calc(25px) top, red, blue)",
   "radial-gradient(at left calc(25px), red, blue)",
@@ -7263,16 +7270,27 @@ if (IsCSSPropertyPrefEnabled("layout.css
     applies_to_first_line: true,
     applies_to_placeholder: true,
     initial_values: [ "auto" ],
     other_values: [ "grayscale" ],
     invalid_values: [ "none", "subpixel-antialiased", "antialiased" ]
   };
 }
 
+if (IsCSSPropertyPrefEnabled("layout.css.scroll-anchoring.enabled")) {
+  gCSSProperties["overflow-anchor"] = {
+    domProp: "overflowAnchor",
+    inherited: false,
+    type: CSS_TYPE_LONGHAND,
+    initial_values: [ "auto" ],
+    other_values: [ "none" ],
+    invalid_values: []
+  };
+}
+
 if (IsCSSPropertyPrefEnabled("layout.css.overflow-clip-box.enabled")) {
   gCSSProperties["overflow-clip-box-block"] = {
     domProp: "overflowClipBoxBlock",
     inherited: false,
     type: CSS_TYPE_LONGHAND,
     applies_to_placeholder: true,
     initial_values: [ "padding-box" ],
     other_values: [ "content-box" ],
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -878,16 +878,35 @@ VARCACHE_PREF(
 
 // Are dynamic reflow roots enabled?
 VARCACHE_PREF(
    "layout.dynamic-reflow-roots.enabled",
    layout_dynamic_reflow_roots_enabled,
   bool, true
 )
 
+// Pref to control enabling scroll anchoring.
+#ifdef NIGHTLY_BUILD
+#define PREF_VALUE true
+#else
+#define PREF_VALUE false
+#endif
+VARCACHE_PREF(
+  "layout.css.scroll-anchoring.enabled",
+   layout_css_scroll_anchoring_enabled,
+  bool, PREF_VALUE
+)
+#undef PREF_VALUE
+
+VARCACHE_PREF(
+  "layout.css.scroll-anchoring.highlight",
+   layout_css_scroll_anchoring_highlight,
+  bool, false
+)
+
 //---------------------------------------------------------------------------
 // JavaScript prefs
 //---------------------------------------------------------------------------
 
 // nsJSEnvironmentObserver observes the memory-pressure notifications and
 // forces a garbage collection and cycle collection when it happens, if the
 // appropriate pref is set.
 #ifdef ANDROID
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -953,18 +953,17 @@ pref("gfx.webrender.debug.epochs", false
 pref("gfx.webrender.debug.compact-profiler", false);
 pref("gfx.webrender.debug.echo-driver-messages", false);
 pref("gfx.webrender.debug.new-frame-indicator", false);
 pref("gfx.webrender.debug.new-scene-indicator", false);
 pref("gfx.webrender.debug.show-overdraw", false);
 pref("gfx.webrender.debug.slow-frame-indicator", false);
 pref("gfx.webrender.dl.dump-parent", false);
 pref("gfx.webrender.dl.dump-content", false);
-
-pref("gfx.webrender.picture-caching", true);
+pref("gfx.webrender.picture-caching", false);
 
 #ifdef EARLY_BETA_OR_EARLIER
 pref("performance.adjust_to_machine", true);
 #else
 pref("performance.adjust_to_machine", false);
 #endif
 
 pref("performance.low_end_machine", false);
--- a/servo/components/style/cbindgen.toml
+++ b/servo/components/style/cbindgen.toml
@@ -58,13 +58,14 @@ include = [
   "OverflowWrap",
   "TimingFunction",
   "PathCommand",
   "UnicodeRange",
   "UserSelect",
   "Float",
   "OverscrollBehavior",
   "ScrollSnapType",
+  "OverflowAnchor",
   "OverflowClipBox",
   "Resize",
   "Overflow",
 ]
 item_types = ["enums", "structs", "typedefs"]
--- a/servo/components/style/gecko/conversions.rs
+++ b/servo/components/style/gecko/conversions.rs
@@ -17,29 +17,34 @@ use crate::gecko_bindings::structs::{sel
 use crate::gecko_bindings::structs::{nsStyleImage, nsresult, SheetType};
 use crate::gecko_bindings::sugar::ns_style_coord::{CoordData, CoordDataMut, CoordDataValue};
 use crate::stylesheets::{Origin, RulesMutateError};
 use crate::values::computed::image::LineDirection;
 use crate::values::computed::transform::Matrix3D;
 use crate::values::computed::url::ComputedImageUrl;
 use crate::values::computed::{Angle, Gradient, Image};
 use crate::values::computed::{Integer, LengthPercentage};
+use crate::values::computed::{Length, Percentage, TextAlign};
 use crate::values::computed::{LengthPercentageOrAuto, NonNegativeLengthPercentageOrAuto};
-use crate::values::computed::{Percentage, TextAlign};
 use crate::values::generics::box_::VerticalAlign;
 use crate::values::generics::grid::{TrackListValue, TrackSize};
 use crate::values::generics::image::{CompatMode, GradientItem, Image as GenericImage};
 use crate::values::generics::rect::Rect;
 use crate::values::generics::NonNegative;
 use app_units::Au;
 use std::f32::consts::PI;
 use style_traits::values::specified::AllowedNumericType;
 
 impl From<LengthPercentage> for nsStyleCoord_CalcValue {
     fn from(other: LengthPercentage) -> nsStyleCoord_CalcValue {
+        debug_assert!(
+            other.was_calc ||
+                other.percentage.is_none() ||
+                other.unclamped_length() == Length::zero()
+        );
         let has_percentage = other.percentage.is_some();
         nsStyleCoord_CalcValue {
             mLength: other.unclamped_length().to_i32_au(),
             mPercent: other.percentage.map_or(0., |p| p.0),
             mHasPercent: has_percentage,
         }
     }
 }
--- a/servo/components/style/properties/data.py
+++ b/servo/components/style/properties/data.py
@@ -321,16 +321,17 @@ class Longhand(object):
                 "MozForceBrokenImageIcon",
                 "MozScriptLevel",
                 "MozScriptMinSize",
                 "MozScriptSizeMultiplier",
                 "NonNegativeNumber",
                 "Opacity",
                 "OutlineStyle",
                 "Overflow",
+                "OverflowAnchor",
                 "OverflowClipBox",
                 "OverflowWrap",
                 "OverscrollBehavior",
                 "Percentage",
                 "Resize",
                 "SVGOpacity",
                 "SVGPaintOrder",
                 "ScrollSnapType",
--- a/servo/components/style/properties/gecko.mako.rs
+++ b/servo/components/style/properties/gecko.mako.rs
@@ -1414,16 +1414,17 @@ impl Clone for ${style_struct.gecko_stru
         "MozLength": impl_style_coord,
         "MozScriptMinSize": impl_absolute_length,
         "MozScriptSizeMultiplier": impl_simple,
         "NonNegativeLengthPercentage": impl_style_coord,
         "NonNegativeNumber": impl_simple,
         "Number": impl_simple,
         "Opacity": impl_simple,
         "OverflowWrap": impl_simple,
+        "OverflowAnchor": impl_simple,
         "Perspective": impl_style_coord,
         "Position": impl_position,
         "RGBAColor": impl_rgba_color,
         "SVGLength": impl_svg_length,
         "SVGOpacity": impl_svg_opacity,
         "SVGPaint": impl_svg_paint,
         "SVGWidth": impl_svg_length,
         "Transform": impl_transform,
--- a/servo/components/style/properties/longhands/box.mako.rs
+++ b/servo/components/style/properties/longhands/box.mako.rs
@@ -120,16 +120,28 @@
     "computed::Overflow::Visible",
     animation_value_type="discrete",
     flags="APPLIES_TO_PLACEHOLDER",
     spec="https://drafts.csswg.org/css-overflow/#propdef-overflow-y",
     needs_context=False,
     servo_restyle_damage = "reflow",
 )}
 
+${helpers.predefined_type(
+    "overflow-anchor",
+    "OverflowAnchor",
+    "computed::OverflowAnchor::Auto",
+    initial_specified_value="specified::OverflowAnchor::Auto",
+    products="gecko",
+    needs_context=False,
+    gecko_pref="layout.css.scroll-anchoring.enabled",
+    spec="https://drafts.csswg.org/css-scroll-anchoring/#exclusion-api",
+    animation_value_type="discrete",
+)}
+
 <% transition_extra_prefixes = "moz:layout.css.prefixes.transitions webkit" %>
 
 ${helpers.predefined_type(
     "transition-duration",
     "Time",
     "computed::Time::zero()",
     initial_specified_value="specified::Time::zero()",
     parse_method="parse_non_negative",
--- a/servo/components/style/values/computed/box.rs
+++ b/servo/components/style/values/computed/box.rs
@@ -8,17 +8,17 @@ use crate::values::computed::length::{Le
 use crate::values::computed::{Context, Number, ToComputedValue};
 use crate::values::generics::box_::AnimationIterationCount as GenericAnimationIterationCount;
 use crate::values::generics::box_::Perspective as GenericPerspective;
 use crate::values::generics::box_::VerticalAlign as GenericVerticalAlign;
 use crate::values::specified::box_ as specified;
 
 pub use crate::values::specified::box_::{AnimationName, Appearance, BreakBetween, BreakWithin};
 pub use crate::values::specified::box_::{Clear as SpecifiedClear, Float as SpecifiedFloat};
-pub use crate::values::specified::box_::{Contain, Display, Overflow, OverflowClipBox};
+pub use crate::values::specified::box_::{Contain, Display, Overflow, OverflowAnchor, OverflowClipBox};
 pub use crate::values::specified::box_::{OverscrollBehavior, ScrollSnapType};
 pub use crate::values::specified::box_::{TouchAction, TransitionProperty, WillChange};
 
 /// A computed value for the `vertical-align` property.
 pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>;
 
 /// A computed value for the `animation-iteration-count` property.
 pub type AnimationIterationCount = GenericAnimationIterationCount<Number>;
--- a/servo/components/style/values/computed/mod.rs
+++ b/servo/components/style/values/computed/mod.rs
@@ -38,17 +38,17 @@ pub use self::align::{AlignSelf, Justify
 pub use self::angle::Angle;
 pub use self::background::{BackgroundRepeat, BackgroundSize};
 pub use self::basic_shape::FillRule;
 pub use self::border::{BorderCornerRadius, BorderRadius, BorderSpacing};
 pub use self::border::{BorderImageRepeat, BorderImageSideWidth};
 pub use self::border::{BorderImageSlice, BorderImageWidth};
 pub use self::box_::{AnimationIterationCount, AnimationName, Contain};
 pub use self::box_::{Appearance, BreakBetween, BreakWithin, Clear, Float};
-pub use self::box_::{Display, Overflow, TransitionProperty};
+pub use self::box_::{Display, Overflow, OverflowAnchor, TransitionProperty};
 pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize};
 pub use self::box_::{ScrollSnapType, TouchAction, VerticalAlign, WillChange};
 pub use self::color::{Color, ColorPropertyValue, RGBAColor};
 pub use self::column::ColumnCount;
 pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset};
 pub use self::easing::TimingFunction;
 pub use self::effects::{BoxShadow, Filter, SimpleShadow};
 pub use self::flex::FlexBasis;
--- a/servo/components/style/values/specified/box.rs
+++ b/servo/components/style/values/specified/box.rs
@@ -423,16 +423,36 @@ pub enum OverscrollBehavior {
     MallocSizeOf,
     Parse,
     PartialEq,
     SpecifiedValueInfo,
     ToComputedValue,
     ToCss,
 )]
 #[repr(u8)]
+pub enum OverflowAnchor {
+    Auto,
+    None,
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+    Clone,
+    Copy,
+    Debug,
+    Eq,
+    MallocSizeOf,
+    Parse,
+    PartialEq,
+    SpecifiedValueInfo,
+    ToComputedValue,
+    ToCss,
+)]
+#[repr(u8)]
 pub enum OverflowClipBox {
     PaddingBox,
     ContentBox,
 }
 
 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss)]
 /// Provides a rendering hint to the user agent,
 /// stating what kinds of changes the author expects
--- a/servo/components/style/values/specified/mod.rs
+++ b/servo/components/style/values/specified/mod.rs
@@ -31,17 +31,17 @@ pub use self::align::{AlignContent, Alig
 pub use self::align::{JustifyContent, JustifyItems, JustifySelf, SelfAlignment};
 pub use self::angle::Angle;
 pub use self::background::{BackgroundRepeat, BackgroundSize};
 pub use self::basic_shape::FillRule;
 pub use self::border::{BorderCornerRadius, BorderImageSlice, BorderImageWidth};
 pub use self::border::{BorderImageRepeat, BorderImageSideWidth};
 pub use self::border::{BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle};
 pub use self::box_::{AnimationIterationCount, AnimationName, Contain, Display};
-pub use self::box_::{Appearance, BreakBetween, BreakWithin, Clear, Float, Overflow};
+pub use self::box_::{Appearance, BreakBetween, BreakWithin, Clear, Float, Overflow, OverflowAnchor};
 pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize};
 pub use self::box_::{ScrollSnapType, TouchAction, TransitionProperty, VerticalAlign, WillChange};
 pub use self::color::{Color, ColorPropertyValue, RGBAColor};
 pub use self::column::ColumnCount;
 pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset};
 pub use self::easing::TimingFunction;
 pub use self::effects::{BoxShadow, Filter, SimpleShadow};
 pub use self::flex::FlexBasis;
--- a/servo/components/style/values/specified/position.rs
+++ b/servo/components/style/values/specified/position.rs
@@ -258,21 +258,22 @@ impl<S: Side> ToComputedValue for Positi
             PositionComponent::Side(ref keyword, None) => {
                 let p = Percentage(if keyword.is_start() { 0. } else { 1. });
                 ComputedLengthPercentage::new_percent(p)
             },
             PositionComponent::Side(ref keyword, Some(ref length)) if !keyword.is_start() => {
                 let length = length.to_computed_value(context);
                 let p = Percentage(1. - length.percentage());
                 let l = -length.unclamped_length();
+                // We represent `<end-side> <length>` as `calc(100% - <length>)`.
                 ComputedLengthPercentage::with_clamping_mode(
                     l,
                     Some(p),
                     length.clamping_mode,
-                    length.was_calc,
+                    /* was_calc = */ true,
                 )
             },
             PositionComponent::Side(_, Some(ref length)) |
             PositionComponent::Length(ref length) => length.to_computed_value(context),
         }
     }
 
     fn from_computed_value(computed: &Self::ComputedValue) -> Self {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/css/css-scroll-anchoring/__dir__.ini
@@ -0,0 +1,1 @@
+prefs: [layout.css.scroll-anchoring.enabled:true]
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/abspos-containing-block-outside-scroller.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[abspos-containing-block-outside-scroller.html]
-  [Abs-pos descendant with containing block outside the scroller.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/abspos-contributes-to-static-parent-bounds.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[abspos-contributes-to-static-parent-bounds.html]
-  [Abs-pos with zero-height static parent.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/ancestor-change-heuristic.html.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[ancestor-change-heuristic.html]
-  [Ancestor changes in document scroller.]
-    expected: FAIL
-
-  [Ancestor changes in scrollable <div>.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/anchor-updates-after-explicit-scroll.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[anchor-updates-after-explicit-scroll.html]
-  [Anchor node recomputed after an explicit scroll occurs.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/anchoring-with-bounds-clamping-div.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[anchoring-with-bounds-clamping-div.html]
-  [Anchoring combined with scroll bounds clamping in a <div>.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/anchoring-with-bounds-clamping.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[anchoring-with-bounds-clamping.html]
-  [Anchoring combined with scroll bounds clamping in the document.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/anonymous-block-box.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[anonymous-block-box.html]
-  [Anchor selection descent into anonymous block boxes.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/basic.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[basic.html]
-  [Minimal scroll anchoring example.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/clipped-scrollers-skipped.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[clipped-scrollers-skipped.html]
-  [Anchor selection with nested scrollers.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/descend-into-container-with-float.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[descend-into-container-with-float.html]
-  [Zero-height container with float.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/descend-into-container-with-overflow.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[descend-into-container-with-overflow.html]
-  [Zero-height container with visible overflow.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/exclude-fixed-position.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[exclude-fixed-position.html]
-  [Fixed-position header.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/inline-block.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[inline-block.html]
-  [Anchor selection descent into inline blocks.]
-    expected: FAIL
-
--- a/testing/web-platform/meta/css/css-scroll-anchoring/negative-layout-overflow.html.ini
+++ b/testing/web-platform/meta/css/css-scroll-anchoring/negative-layout-overflow.html.ini
@@ -1,4 +1,5 @@
 [negative-layout-overflow.html]
   [Anchor selection accounts for negative positioning.]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1517287
 
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/opt-out.html.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[opt-out.html]
-  [Disabled on document, enabled on div.]
-    expected: FAIL
-
-  [Enabled on document, disabled on div.]
-    expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/position-change-heuristic.html.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[position-change-heuristic.html]
-  [Position changes in document scroller.]
-    expected: FAIL
-
-  [Position changes in scrollable <div>.]
-    expected: FAIL
-
--- a/testing/web-platform/meta/css/css-scroll-anchoring/start-edge-in-block-layout-direction.html.ini
+++ b/testing/web-platform/meta/css/css-scroll-anchoring/start-edge-in-block-layout-direction.html.ini
@@ -1,19 +1,11 @@
 [start-edge-in-block-layout-direction.html]
-  [Horizontal LTR.]
-    expected: FAIL
-
-  [Horizontal RTL.]
-    expected: FAIL
-
-  [Vertical-LR LTR.]
-    expected: FAIL
-
-  [Vertical-LR RTL.]
-    expected: FAIL
-
   [Vertical-RL LTR.]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1447743
+    issue: https://github.com/w3c/csswg-drafts/issues/2704
 
   [Vertical-RL RTL.]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1447743
+    issue: https://github.com/w3c/csswg-drafts/issues/2704
 
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/subtree-exclusion.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[subtree-exclusion.html]
-  [Subtree exclusion with overflow-anchor.]
-    expected: FAIL
-
--- a/testing/web-platform/meta/css/css-scroll-anchoring/text-anchor-in-vertical-rl.html.ini
+++ b/testing/web-platform/meta/css/css-scroll-anchoring/text-anchor-in-vertical-rl.html.ini
@@ -1,4 +1,6 @@
 [text-anchor-in-vertical-rl.html]
   [Line at edge of scrollport shouldn't jump visually when content is inserted before]
     expected: FAIL
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1447743
+    issue: https://github.com/w3c/csswg-drafts/issues/2704
 
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-scroll-anchoring/wrapped-text.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[wrapped-text.html]
-  [Anchoring with text wrapping changes.]
-    expected: FAIL
-
--- a/testing/web-platform/tests/css/css-scroll-anchoring/ancestor-change-heuristic.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/ancestor-change-heuristic.html
@@ -1,14 +1,14 @@
 <!DOCTYPE html>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style>
 
-#space { height: 1000px; }
+#space { height: 4000px; }
 #ancestor { position: relative; }
 #before, #anchor { height: 100px; }
 #anchor { background-color: green; }
 
 .layout1 { padding-top: 20px; }
 .layout2 { margin-right: 20px; }
 .layout3 { max-width: 100px; }
 .layout4 { min-height: 400px; }
--- a/testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping.html
@@ -1,17 +1,17 @@
 <!DOCTYPE html>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style>
 
 #changer { height: 1500px; }
 #anchor {
   width: 150px;
-  height: 1000px;
+  height: 4000px;
   background-color: pink;
 }
 
 </style>
 <div id="changer"></div>
 <div id="anchor"></div>
 <script>
 
--- a/testing/web-platform/tests/css/css-scroll-anchoring/basic.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/basic.html
@@ -1,14 +1,14 @@
 <!DOCTYPE html>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style>
 
-body { height: 1000px; }
+body { height: 4000px; }
 div { height: 100px; }
 
 </style>
 <div id="block1">abc</div>
 <div id="block2">def</div>
 <script>
 
 // Tests that growing an element above the viewport produces a scroll
--- a/testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-float.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-float.html
@@ -1,14 +1,14 @@
 <!DOCTYPE html>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style>
 
-body { height: 1000px; }
+body { height: 4000px; }
 #outer { width: 300px; }
 #outer:after { content: " "; clear:both; display: table; }
 #float {
   float: left; background-color: #ccc;
   height: 500px; width: 100%;
 }
 #inner { height: 100px; background-color: green; }
 
--- a/testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-overflow.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-overflow.html
@@ -1,14 +1,14 @@
 <!DOCTYPE html>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style>
 
-body { height: 1000px; }
+body { height: 4000px; }
 #outer { width: 300px; }
 #zeroheight { height: 0px; }
 #changer { height: 100px; background-color: red; }
 #bottom { margin-top: 600px; }
 
 </style>
 <div id="outer">
   <div id="zeroheight">
--- a/testing/web-platform/tests/css/css-scroll-anchoring/exclude-fixed-position.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/exclude-fixed-position.html
@@ -1,14 +1,14 @@
 <!DOCTYPE html>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style>
 
-body { height: 1000px; margin: 0; }
+body { height: 4000px; margin: 0; }
 #fixed, #content { width: 200px; height: 100px; }
 #fixed { position: fixed; left: 100px; top: 50px; }
 #before { height: 50px; }
 #content { margin-top: 100px; }
 
 </style>
 <div id="fixed">fixed</div>
 <div id="before"></div>
--- a/testing/web-platform/tests/css/css-scroll-anchoring/inline-block.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/inline-block.html
@@ -1,14 +1,14 @@
 <!DOCTYPE html>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style>
 
-body { height: 1000px }
+body { height: 4000px }
 #outer { line-height: 100px }
 #ib1, #ib2 { display: inline-block }
 
 </style>
 <span id=outer>
   <span id=ib1>abc</span>
   <br><br>
   <span id=ib2>def</span>
--- a/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic.html
@@ -1,15 +1,15 @@
 <!DOCTYPE html>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style>
 
 #space {
-  height: 1000px;
+  height: 4000px;
 }
 #header {
   background-color: #F5B335;
   height: 50px;
   width: 100%;
 }
 #content {
   background-color: #D3D3D3;
--- a/testing/web-platform/tests/css/css-scroll-anchoring/start-edge-in-block-layout-direction.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/start-edge-in-block-layout-direction.html
@@ -27,35 +27,39 @@ html.vrl { writing-mode: vertical-rl; }
   width: 100px;
   height: 100px;
 }
 #block_pusher { background-color: #e88; }
 #inline_pusher { background-color: #88e; }
 .vpush { height: 80px !important; }
 .hpush { width: 70px !important; }
 
+#anchor-container {
+  display: inline-block;
+}
 #anchor {
   position: relative;
-  display: inline-block;
   background-color: #8e8;
   min-width: 100px;
   min-height: 100px;
 }
 
 #grower { width: 0; height: 0; }
 .grow {
   width: 180px !important;
   height: 160px !important;
 }
 
 </style>
 <div id="container">
   <div id="block_pusher"></div><br>
-  <div id="inline_pusher"></div><div id="anchor">
-    <div id="grower"></div>
+  <div id="inline_pusher"></div><div id="anchor-container">
+    <div id="anchor">
+      <div id="grower"></div>
+    </div>
   </div>
 </div>
 <script>
 
 // Tests that anchoring adjustments are only on the block layout axis and that
 // their magnitude is based on the movement of the block start edge of the
 // anchor node, for all 6 combinations of text direction and writing mode,
 // regardless of which corner of the viewport the anchor node overlaps.
--- a/testing/web-platform/tests/css/css-scroll-anchoring/subtree-exclusion.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/subtree-exclusion.html
@@ -1,14 +1,14 @@
 <!DOCTYPE html>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style>
 
-body { height: 1000px }
+body { height: 4000px }
 #A, #B { width: 100px; background-color: #afa; }
 #B { height: 100px; }
 #inner { width: 100px; height: 100px; background-color: pink; }
 #A { overflow-anchor: none; }
 
 </style>
 <div id="changer1"></div>
 <div id="A">
--- a/testing/web-platform/tests/css/css-scroll-anchoring/wrapped-text.html
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/wrapped-text.html
@@ -2,17 +2,17 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <style>
 
 body {
   position: absolute;
   font-size: 100px;
   width: 200px;
-  height: 1000px;
+  height: 4000px;
   line-height: 100px;
 }
 
 </style>
 abc <b id=b>def</b> ghi
 <script>
 
 // Tests anchoring to a text node that is moved by preceding text.