Skip to content

Horizontal scrolling#93

Merged
hirasso merged 22 commits intonextfrom
feat/horizontal-scrolling
Jun 12, 2025
Merged

Horizontal scrolling#93
hirasso merged 22 commits intonextfrom
feat/horizontal-scrolling

Conversation

@daun
Copy link
Member

@daun daun commented Jun 8, 2025

Description

  • Scroll containers horizontally to reveal anchors
  • Change API of scrollTo from just number to number | { top: number, left: number }
  • Normalize a number returned from OffsetCallback to { top: number, left: 0}
  • Communicate that the OffsetCallback arguments are never undefined
  • Pass the target position to OffsetCallback to provide fine-grained control

Example from playground

Screen Recording 2025-06-08 at 22 45 41

Checks

  • The PR is submitted to the next branch
  • The code was linted before pushing (npm run lint)
  • All tests are passing (npm run test)
  • New or updated tests are included
  • The documentation was updated as required

Signed-off-by: Philipp Daun <post@philippdaun.net>
@github-actions
Copy link

github-actions bot commented Jun 8, 2025

Playwright test results

passed  3 passed

Details

stats  3 tests across 1 suite
duration  
commit  363b3cd

Signed-off-by: Philipp Daun <post@philippdaun.net>
@daun daun requested review from a team and Copilot June 8, 2025 21:21
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Adds horizontal scrolling support and updates the scrollTo API to accept an object with both top and left offsets.

  • Change swup.scrollTo signature to number | { top, left }
  • Update internal calls and implementation to handle both axes
  • Bind plugin’s scrollTo method directly to the Swup instance
Comments suppressed due to low confidence (3)

src/index.ts:405

  • [nitpick] Update the doc comment to mention both horizontal and vertical offsets, for example: "Scroll to a specific top & left offset, with optional animation."
* Scroll to a specific offset, with optional animation.

src/index.ts:33

  • New horizontal scrolling logic and API signature change should be covered by unit tests. Consider adding tests for scrollTo({ top, left }) and mixed number/object inputs.
scrollTo?: (
			position: number | ScrollPosition,

src/index.ts:465

  • getOffset likely only returns a vertical offset. Subtracting it from left may produce incorrect horizontal positioning. Consider calculating a separate horizontal offset or confirm that getOffset handles both axes.
this.scrollTo({ top: top - offset, left: left - offset }, animate, scrollContainer);

@daun daun linked an issue Jun 8, 2025 that may be closed by this pull request
4 tasks
@daun daun mentioned this pull request Jun 8, 2025
Merged
13 tasks
@hirasso
Copy link
Member

hirasso commented Jun 9, 2025

Nice! Did you activate those Copilot reviews? I'm only on mobile right now, but the point about offset currently only being for top offset looks like it makes sense.

@daun
Copy link
Member Author

daun commented Jun 9, 2025

I asked him for fun :) Haven't looked at it, but makes sense to allow setting both top and left offsets if useful.

@hirasso
Copy link
Member

hirasso commented Jun 9, 2025

For e2e tests - what do you think about using your playground for the fixtures? It's also astro, after all.

@daun
Copy link
Member Author

daun commented Jun 9, 2025

For e2e tests - what do you think about using your playground for the fixtures? It's also astro, after all.

Sure, we can copy them over. However the styling is a bit messy, so we might even be faster by just recreating the styles.

@daun
Copy link
Member Author

daun commented Jun 9, 2025

Added support for vertical + horizontal offsets.

src/index.ts Outdated
return offset;
} else {
const top = parseInt(String(offset ?? ''), 10) || 0;
return { top, left: top };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we treat a single number as top in scrollTo, I'd expect a single number for offset also to be treated as the top offset:

return { top, left: 0 };

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I'd expect it to have a uniform offset. Mentally, I see it a bit like the scroll-margin css property, which applies to all sides when set to a single value. Whereas scrolling always requires being explicit like in window.scrollTo(x, y)...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. This would break expectations with how the offset used to work, at least for me.

But let's take a step back - would it make sense to start respecting the actual scroll-margin css value instead of the js version we provide right now? And only provide the offset callback? I don't know, happy to give this some more thought in a talk.

Copy link
Member

@hirasso hirasso Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From MDN:

The scroll-margin-top property defines the top margin of the scroll snap area that is used for snapping this box to the snapport.

So scroll-margin only seems to be related to scroll snapping, not to anchor scrolling, if I read this right? I have never used scroll-margin-* myself. Scratch that, it applies to anchor scrolling without any snapping as well – tested it myself just now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to use scroll-margin exclusively, however it's not perfect. The package we're using to compute scroll movements does calculate it in, so it's good to go in that sense. The issue is with the actual spec/browser implementation. There's no way of defining cumulative offsets when nesting scroll containers. So I can't define an offset for scrolling the body (e.g. the header height) and a different offset for scrolling inside overflowing containers (e.g. 1em). Happy to show you in a call how it behaves in the playground.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All that to say: it's a great idea to use scroll-margin for 90% of use cases. Just when targeting elements inside scroll containers, it gets tricky and requires some custom JS logic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, scroll margin applies to all scrolling. Took me a while to figure that out myself :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love to use scroll-margin exclusively, however it's not perfect. The package we're using to compute scroll movements does calculate it in, so it's good to go in that sense. The issue is with the actual spec/browser implementation. There's no way of defining cumulative offsets when nesting scroll containers. So I can't define an offset for scrolling the body (e.g. the header height) and a different offset for scrolling inside overflowing containers (e.g. 1em). Happy to show you in a call how it behaves in the playground.

Yes please, I would love that!

@hirasso
Copy link
Member

hirasso commented Jun 9, 2025

For e2e tests - what do you think about using your playground for the fixtures? It's also astro, after all.

Sure, we can copy them over. However the styling is a bit messy, so we might even be faster by just recreating the styles.

Ok, maybe recreating the styles is the better idea. I used tailwind for the fixtures, which helps a lot with creating multiple independent pages / one-off styles.

src/index.ts Outdated
offset:
| number
| ScrollPosition
| ((scrollTarget?: Element, scrollContainer?: Element) => number | ScrollPosition);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just stumbled upon this: Are scrollTarget and scrollContainer not always defined? getOffset is only being called by scrollElementIntoView(), where both are always defined.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm currently confused – is the offset to be applied when doing a normal scroll to top (without a scrollTarget)? If so, getOffset should be changed to this:

((scrollContainer: Element, scrollTarget?: Element) => number | ScrollPosition)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, these should always be defined. We can probably safely change the signature.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The offset is only applied when scrolling to an element. When scrolling to the top, it wouldn't have an effect. Subtracting the offset would end up with a scroll position below zero, so still technically the top.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea why we made these arguments optional. Maybe I bungled it while adding the feature :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for clearing that up with the negative scroll offset 😅🫠

@daun
Copy link
Member Author

daun commented Jun 10, 2025

Note from recent call: make offset option function args required, and fall back to left: 0 for scalar offset values.

@hirasso
Copy link
Member

hirasso commented Jun 10, 2025

...and: clamp offsets so that the scroll target will be revealed completely, even if at the far edges of the scroll container.

@hirasso hirasso mentioned this pull request Jun 10, 2025
5 tasks
@netlify
Copy link

netlify bot commented Jun 11, 2025

Deploy Preview for swup-scroll-plugin ready!

Name Link
🔨 Latest commit 363b3cd
🔍 Latest deploy log https://app.netlify.com/projects/swup-scroll-plugin/deploys/684956a8bbf7c40008ed9806
😎 Deploy Preview https://deploy-preview-93--swup-scroll-plugin.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

hirasso added 3 commits June 11, 2025 11:36
This makes it possible for consumers what to do when scrolling to the far edges of a container (should the an offset be substracted or not)
@hirasso
Copy link
Member

hirasso commented Jun 11, 2025

I've implemented the last missing pieces and added them to the top comment:

  • Normalize a number returned from OffsetCallback to { top: number, left: 0}
  • Communicate that the OffsetCallback arguments are never undefined
  • Pass the target position to OffsetCallback to provide fine-grained control
  • Export the type OffsetCallback to make it easier to decouple the code from the plugin instantiation

The third is the best I could come up with to enable users to clamp offsets so that the scroll target will be revealed completely, even if at the far edges of the scroll container. I think it's best to not make any assumptions about what everybody wants here. In the playground code, I could then return 0 if the target position was at the max available scroll height:

/**
 * Do not offset the scroll if scrolling to the very end
 */
const maxTop = container.scrollHeight - container.clientHeight;
if (top === maxTop) return 0;

What do you think?

@hirasso hirasso merged commit b631bce into next Jun 12, 2025
6 checks passed
@hirasso hirasso deleted the feat/horizontal-scrolling branch June 12, 2025 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support horizontal scrolling

3 participants