Skip to content

feat: improve scroll-to-top behavior#1453

Open
RYGRIT wants to merge 2 commits intonpmx-dev:mainfrom
RYGRIT:feat/scroll-to-top
Open

feat: improve scroll-to-top behavior#1453
RYGRIT wants to merge 2 commits intonpmx-dev:mainfrom
RYGRIT:feat/scroll-to-top

Conversation

@RYGRIT
Copy link
Contributor

@RYGRIT RYGRIT commented Feb 13, 2026

Fixes #1379

Improves the ScrollToTop component behavior.

  • Desktop Support: Enabled the button on desktop and fixed the visibility fallback logic for browsers without scroll-state support.
  • Better UX: Normalized scroll duration (500ms) and added support for prefers-reduced-motion.
  • Interaction: The auto-scroll animation now cancels immediately upon user interaction (wheel, touch, or click).
Desktop
scroll-to-top.mp4
Mobile
freecompress-1714SVID_20260213_094933_1.mp4

cc @graphieros @MatteoGabriele

@vercel
Copy link

vercel bot commented Feb 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 13, 2026 2:03pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 13, 2026 2:03pm
npmx-lunaria Ignored Ignored Feb 13, 2026 2:03pm

Request Review

@codecov
Copy link

codecov bot commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 2.08333% with 47 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/composables/useScrollToTop.ts 0.00% 32 Missing and 6 partials ⚠️
app/components/ScrollToTop.client.vue 10.00% 5 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

A new composable useScrollToTop was added that provides scrollToTop and cancel, supports an optional duration (default 500ms), respects prefers-reduced-motion, performs a frame-driven eased animation, cancels on user interaction, and cleans up on unmount. The ScrollToTop component was refactored to use this composable, added touch-device detection, changed visibility logic to prefer CSS-driven paths when available, and updated template bindings and click handlers accordingly.

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (66 files):

⚔️ .gitignore (content)
⚔️ CONTRIBUTING.md (content)
⚔️ README.md (content)
⚔️ app/app.vue (content)
⚔️ app/components/AppFooter.vue (content)
⚔️ app/components/AppHeader.vue (content)
⚔️ app/components/CallToAction.vue (content)
⚔️ app/components/Header/AccountMenu.client.vue (content)
⚔️ app/components/Header/AuthModal.client.vue (content)
⚔️ app/components/Header/ConnectorModal.vue (content)
⚔️ app/components/Header/SearchBox.vue (content)
⚔️ app/components/Link/Base.vue (content)
⚔️ app/components/Package/Keywords.vue (content)
⚔️ app/components/Package/TrendsChart.vue (content)
⚔️ app/components/Package/VersionDistribution.vue (content)
⚔️ app/components/Package/Versions.vue (content)
⚔️ app/components/Readme.vue (content)
⚔️ app/components/ScrollToTop.client.vue (content)
⚔️ app/components/Terminal/Install.vue (content)
⚔️ app/composables/useMarkdown.ts (content)
⚔️ app/composables/usePackageAnalysis.ts (content)
⚔️ app/composables/useStructuredFilters.ts (content)
⚔️ app/pages/about.vue (content)
⚔️ app/pages/index.vue (content)
⚔️ app/pages/package/[[org]]/[name].vue (content)
⚔️ app/pages/search.vue (content)
⚔️ app/utils/install-command.ts (content)
⚔️ app/utils/prehydrate.ts (content)
⚔️ app/utils/versions.ts (content)
⚔️ i18n/locales/de-DE.json (content)
⚔️ i18n/locales/en.json (content)
⚔️ i18n/locales/es.json (content)
⚔️ i18n/locales/fr-FR.json (content)
⚔️ i18n/locales/ja-JP.json (content)
⚔️ i18n/locales/pl-PL.json (content)
⚔️ i18n/locales/zh-CN.json (content)
⚔️ i18n/schema.json (content)
⚔️ lunaria/files/de-DE.json (content)
⚔️ lunaria/files/en-GB.json (content)
⚔️ lunaria/files/en-US.json (content)
⚔️ lunaria/files/es-419.json (content)
⚔️ lunaria/files/es-ES.json (content)
⚔️ lunaria/files/fr-FR.json (content)
⚔️ lunaria/files/ja-JP.json (content)
⚔️ lunaria/files/pl-PL.json (content)
⚔️ lunaria/files/zh-CN.json (content)
⚔️ lunaria/lunaria.ts (content)
⚔️ lunaria/prepare-json-files.ts (content)
⚔️ nuxt.config.ts (content)
⚔️ package.json (content)
⚔️ pnpm-lock.yaml (content)
⚔️ scripts/compare-translations.ts (content)
⚔️ server/api/contributors.get.ts (content)
⚔️ server/middleware/canonical-redirects.global.ts (content)
⚔️ server/utils/readme-loaders.ts (content)
⚔️ server/utils/readme.ts (content)
⚔️ shared/utils/constants.ts (content)
⚔️ shared/utils/package-analysis.ts (content)
⚔️ test/nuxt/a11y.spec.ts (content)
⚔️ test/nuxt/components/PackageVersions.spec.ts (content)
⚔️ test/nuxt/composables/use-markdown.spec.ts (content)
⚔️ test/unit/app/utils/install-command.spec.ts (content)
⚔️ test/unit/app/utils/versions.spec.ts (content)
⚔️ test/unit/server/utils/readme-loaders.spec.ts (content)
⚔️ test/unit/server/utils/readme.spec.ts (content)
⚔️ test/unit/shared/utils/package-analysis.spec.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description clearly relates to the changeset: it describes improvements to ScrollToTop component with desktop support, normalised duration, and interaction-based cancellation.
Linked Issues check ✅ Passed The pull request addresses both primary objectives from #1379: enables ScrollToTop on touch devices with isTouchDevice logic and implements normalised 500ms smooth scroll animation with requestAnimationFrame.
Out of Scope Changes check ✅ Passed All changes are within scope: the new useScrollToTop composable, touch device detection, prefers-reduced-motion support, and interaction-based cancellation all directly support the linked issue objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feat/scroll-to-top
  • Post resolved changes as copyable diffs in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

@RYGRIT RYGRIT marked this pull request as draft February 13, 2026 02:40
@RYGRIT RYGRIT marked this pull request as ready for review February 13, 2026 08:25
Copy link
Member

@danielroe danielroe left a comment

Choose a reason for hiding this comment

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

these look like good changes to the scroll composable 👌

my only question is about displaying the button on desktop. it feels like an affordance needed for mobile, but users with a keyboard will have a 'Home' key and an extra button clutters the UI

@graphieros
Copy link
Contributor

these look like good changes to the scroll composable 👌

my only question is about displaying the button on desktop. it feels like an affordance needed for mobile, but users with a keyboard will have a 'Home' key and an extra button clutters the UI

Would the button appear on tablets ?

@danielroe
Copy link
Member

it should probably be gated not by window width but by isTouchDevice
https://github.com/npmx-dev/npmx.dev/blob/8a70d6e124dc3c1560a59c76334ac64867dd07b7/app/utils/responsive.ts

@RYGRIT
Copy link
Contributor Author

RYGRIT commented Feb 13, 2026

it should probably be gated not by window width but by isTouchDevice https://github.com/npmx-dev/npmx.dev/blob/8a70d6e124dc3c1560a59c76334ac64867dd07b7/app/utils/responsive.ts

Good point. I'll switch to using isTouchDevice to avoid cluttering the desktop UI.

'Home' key

Ah, TIL about the Home/End shortcuts! Thanks for the tip.

@RYGRIT
Copy link
Contributor Author

RYGRIT commented Feb 13, 2026

I've already implemented isTouchDevice() to determine if a button is hidden, but I've found that when simulating a mobile device in the browser, I have to refresh the browser for the button to appear.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Comment on lines +12 to +20
const isTouchDeviceClient = shallowRef(false)
const isVisible = shallowRef(false)
const scrollThreshold = 300
const { isSupported: supportsScrollStateQueries } = useCssSupports(
'container-type',
'scroll-state',
{ ssrValue: false },
)
const shouldShowButton = computed(() => isActive.value && isTouchDeviceClient.value)
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 13, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Desktop visibility still blocked by touch-only gating.

shouldShowButton requires isTouchDeviceClient, so the button never appears on non-touch desktops. That conflicts with issue #1379’s requirement to show the control on desktop. If the requirement still stands, drop the touch-only gate (or make it configurable). If the requirement has changed, please update the issue/PR description to match.

🛠️ Suggested fix if desktop should be supported
-const isTouchDeviceClient = shallowRef(false)
 const isVisible = shallowRef(false)
@@
-const shouldShowButton = computed(() => isActive.value && isTouchDeviceClient.value)
+const shouldShowButton = computed(() => isActive.value)
@@
 onMounted(() => {
-  isTouchDeviceClient.value = isTouchDevice()
   onScroll()
 })

Also applies to: 34-34

Copy link
Contributor

Choose a reason for hiding this comment

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

This is ok, requirements changed to only show the button on touch decices

Copy link
Contributor

Choose a reason for hiding this comment

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

@graphieros Thank you for clarifying! I understand the requirements have shifted to show the button only on touch devices. The current implementation correctly achieves that.

Since issue #1379 is titled "Back-to-top button on desktop" and the PR description mentions "Desktop Support: Enabled the button on desktop", you may want to update both to reflect this new direction—perhaps explaining that after discussion, the button was decided to remain touch-only to avoid cluttering desktop UI (where keyboard shortcuts like Home/End are available).


✏️ Learnings added
Learnt from: graphieros
Repo: npmx-dev/npmx.dev PR: 1453
File: app/components/ScrollToTop.client.vue:12-20
Timestamp: 2026-02-13T14:11:12.069Z
Learning: For the ScrollToTop button component in app/components/ScrollToTop.client.vue: the button should only be shown on touch devices (gated by `isTouchDevice()`), not on desktop, to avoid cluttering desktop UI where keyboard shortcuts are available.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@danielroe
Copy link
Member

looking great!

but one more thought - we might want to update the implementation on the package page as we already have a bottom bar - maybe (on that page) build another button into that bottom bar and hide this one?

Screen Shot 2026-02-13 at 14 47 16

@RYGRIT
Copy link
Contributor Author

RYGRIT commented Feb 13, 2026

Yes, I also think they should be merged.

However, this raises another issue: when switching to other languages, the floating buttons at the bottom extend beyond the screen, and the display changes accordingly (for example, the floating button area becomes higher). I mentioned this issue in PR #1284 before. I'm unsure whether the text content should be hidden on small screens. I haven't thought of a better solution at the moment.

Device: iPhone 14 Pro Max - browser (simulated) image image
image

Copy link
Contributor

@OrbisK OrbisK left a comment

Choose a reason for hiding this comment

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

class="scroll-to-top-css fixed bottom-4 inset-ie-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95"
:aria-label="$t('common.scroll_to_top')"
@click="scrollToTop"
@click="scrollToTop()"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
@click="scrollToTop()"
@click="()=>scrollToTop()"

class="fixed bottom-4 inset-ie-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95"
:aria-label="$t('common.scroll_to_top')"
@click="scrollToTop"
@click="scrollToTop()"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
@click="scrollToTop()"
@click="()=>scrollToTop()"

Comment on lines +64 to +68
const cleanup = [
useEventListener(window, 'wheel', cancel, { passive: true }),
useEventListener(window, 'touchstart', cancel, { passive: true }),
useEventListener(window, 'mousedown', cancel, { passive: true }),
]
Copy link
Contributor

@OrbisK OrbisK Feb 13, 2026

Choose a reason for hiding this comment

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

we should not add these inside of scrollTop(). This will lead to a memory leak, becuase the event listerners are not cleaned up after every call.
You should move them to the setup level.

rafId = requestAnimationFrame(animate)
}

onBeforeUnmount(cancel)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should prefer the tryOnScopeDispose method from Vueuse. This is because the component might be called in another scope, than the component (for example as a sharedComposable).

@danielroe
Copy link
Member

However, this raises another issue: when switching to other languages, the floating buttons at the bottom extend beyond the screen, and the display changes accordingly

@RYGRIT maybe we can switch to icon-only for the bottom bar on mobile? and long-press on the button shows what it does?

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.

Back-to-top button on desktop

4 participants