This extension saves your scroll positions, allowing you to resume reading later with ease.
You can save as many scroll positions as you want, add notes to them, rename them, and view all your saved spots on a separate page.
Here is an essay explaining my motivation, thought processes, and the implementation details of this project.
Although I try to keep Mark Scroll Positions as simple as possible, the task it is trying to solve, returning to the same meaningful place on a webpage, is not always trivial. Each site can have its own quirks, so Mark Scroll Positions may not behave exactly as you want on certain pages. If that is what you are experiencing with a particular site, I urge you to look into the options below: jump strategy rules, URL matching, and scroll container rules.
Jump strategies control how the extension returns to a saved mark when the page height is not exactly the same as it was when the mark was saved.
Keep in mind that marks do not store scrolls differently based on which jump strategy you are currently using. The scroll position details that get stored are basically the same. What changes with different jump strategy rules is how those scroll positions are interpreted with respect to the latest state of the page.
We currently have two different jump strategies: Page ratio and Screen ratio.
Page ratio interprets the saved offset as a percentage of the page's scrollable
range. For example, if a mark was saved halfway through the scrollable page,
jumping to it later goes halfway through the current scrollable page. This is a
good default for articles, documentation, and other mostly stable pages.
Screen ratio interprets the saved offset as a number of viewport heights from
the top. For example, if a mark was saved three screens down, jumping to it later
goes about three current screens down. This can work better on pages where the
total page height changes often, like Hacker News comment threads, Reddit
discussions, GitHub issue timelines, chat-style web apps, and infinite feeds.
The default rule applies unless a more specific hostname or path prefix matches. Use site-specific rules only for sites where the default jump strategy feels wrong.
By default, the extension ignores query parameters. This means URLs like
https://example.com/?utm_source=chatgpt and
https://example.com/?utm_source=claude are handled as if they point to the
same page (which, in this case, they do!).
This works well for not letting irrelevant query parameters make it look as if
we are viewing separate pages. On the other hand, for certain sites like Hacker
News, some pages are uniquely identified by a query value. For example,
https://news.ycombinator.com/item?id=40589852 is identified by the id query.
In cases where the page you are currently viewing has this kind of meaningful
query parameter, the Mark Scroll Positions popup will show you a checkbox like
Use query params for news.ycombinator.com. Feel free to enable it if the query
parameters are important for the content that you are seeing.
Most pages scroll the document itself, which the extension detects
automatically. Some web apps keep the document fixed and scroll an internal page
element instead. For those sites, add a scroll container rule with a URL prefix
and a CSS selector for the element that actually scrolls.
If saved marks always look like they are at 100% scrolled, or jumping does
not move the visible content, you may need a custom scroll container selector.
See the guide below for finding one.
Some web apps do not scroll the document itself. Instead, they keep the page fixed and scroll a specific element inside the page. Chat apps and pages with large comment panels often work this way.
If saved marks always look like they are at 100% scrolled, or jumping does
not move the visible content, you may need to add a custom scroll container rule
in the extension settings.
To find the selector:
- Open the page where scrolling is not detected correctly.
- Open the browser developer tools.
- Open the Console tab.
- Paste and run this script:
(() => {
const selectorFor = (element) => {
if (element.id) return `#${CSS.escape(element.id)}`
const parts = []
let current = element
while (current && current.nodeType === Node.ELEMENT_NODE && parts.length < 5) {
const tagName = current.tagName.toLowerCase()
const className = [...current.classList]
.slice(0, 3)
.map((name) => `.${CSS.escape(name)}`)
.join('')
parts.unshift(`${tagName}${className}`)
current = current.parentElement
}
return parts.join(' > ')
}
const before = new Map(
[...document.querySelectorAll('*')]
.filter((element) => element.scrollHeight > element.clientHeight)
.map((element) => [element, element.scrollTop])
)
console.log('Scroll the page now, then run: findChangedScrollContainers()')
window.findChangedScrollContainers = () => {
const changed = [...before]
.map(([element, scrollTop]) => ({
selector: selectorFor(element),
before: scrollTop,
after: element.scrollTop,
element,
}))
.filter(({before, after}) => before !== after)
console.table(changed.map(({selector, before, after}) => ({selector, before, after})))
return changed
}
})()- Scroll the page content that you want the extension to track.
- Run this in the Console:
findChangedScrollContainers()The table shows elements whose scrollTop changed while you scrolled. Copy the
most specific-looking selector from the selector column, then add it in:
Settings -> Scroll Container Rules
Use a URL prefix such as chatgpt.com or example.com/comments, and paste the
selector into the CSS selector field.
If the selector stops matching later, the extension falls back to normal document scrolling. In that case, repeat the steps above and update the rule.
This extension ships directly from the source tree. Runtime dependencies are
vendored in vendor/, including Preact, preact/hooks, and HTM.
npm is only used to make developer tooling available, such as TypeScript for
JSDoc-based type checking and web-ext. It is not required to build a bundle
before shipping.
If you want the developer tooling, install the npm dependencies:
npm install
Then, type-check the code:
npm run typecheck
After that, you can load the repository directory itself as an unpacked extension in Chrome or Firefox. No build step is required.
For development with auto-reload in Firefox, run:
npm run start:firefox
This loads the repository directly and reloads the extension when source files change.
Releases are tag-driven.
- Update
package.jsonandmanifest.jsonto the same version. - Commit that version bump on
master. - Create and push a matching
vX.Y.Ztag.
GitHub Actions then installs the developer dependencies, runs
npm run typecheck, packages manifest.json, public, src, and vendor
into build.zip, and creates the GitHub release from that tag.