Files
upstream-rocketchat/docs/anchor-navigation.md
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

4.2 KiB

Anchor Navigation

This document describes the pattern for navigating directly to a specific field within a page using URL hash fragments (e.g., /account/accessibility-and-appearance#clockMode).

Problem

Rocket.Chat uses a custom SPA router that intercepts link clicks via e.preventDefault() and navigates with history.pushState(). This means the browser's native hash-based scrolling does not work for cross-page navigation. When a user clicks a link like /page-b#field, the router renders the new page but does not scroll to the target element.

Solution

Scrolling to hash targets is handled automatically by RouterProvider via useRouterScrollToHash. Any element with an id matching the URL hash will be scrolled into view on navigation.

For pages that need to adjust layout based on the hash (e.g., expanding an accordion section), useLocationHash() from @rocket.chat/ui-contexts provides the current hash value.

Key files

File Purpose
apps/meteor/client/hooks/useRouterScrollToHash.ts Scroll logic, called by RouterProvider
packages/ui-contexts/src/hooks/useLocationHash.ts Generic useLocationHash() hook from @rocket.chat/ui-contexts

How it works

  1. A source page links to a target page with a hash fragment in the href
  2. The target page renders elements with matching id attributes
  3. useRouterScrollToHash (in RouterProvider) scrolls to the element matching the hash via scrollIntoView
  4. If the target page needs to adjust layout (e.g., expand a collapsed section), it reads useLocationHash() to check if a hash is present
PreferencesMessagesSection                      AccessibilityPage
┌──────────────────────────┐                   ┌──────────────────────────┐
│ Message TimeFormat        │                   │ Adjustable layout        │
│ [Go to accessibility] ────┼── #clockMode ───> │  <Field id="clockMode">  │
│                           │                   │                          │
│ Hide usernames            │                   │  <Field id="hideUsernames">  │
│ [Go to accessibility] ────┼── #hideUsernames ───> │                          │
└──────────────────────────┘                   └──────────────────────────┘

Usage

1. Add the id to the target element

On the destination page, set the id on the element you want to scroll to:

<Field id='myNewField'>
  <FieldLabel>{t('My_New_Field')}</FieldLabel>
  {/* ... */}
</Field>

2. Expand collapsed sections (if needed)

If the target field is inside a collapsed section, use useLocationHash() to expand it:

import { useLocationHash } from '@rocket.chat/ui-contexts';

const MyPage = () => {
  const shouldExpand = useLocationHash().length > 1;

  return (
    <Accordion>
      <AccordionItem defaultExpanded={shouldExpand} title={t('Section')}>
        <Field id='myNewField'>
          {/* ... */}
        </Field>
      </AccordionItem>
    </Accordion>
  );
};

Use the hash fragment in the href:

<FieldLink href='/account/accessibility-and-appearance#myNewField'>
  {t('Go_to_accessibility_and_appearance')}
</FieldLink>

Why scrollIntoView is needed

The browser natively scrolls to hash targets on full page loads and un-intercepted anchor clicks. However, the custom router calls e.preventDefault() on cross-page links and uses history.pushState(), which by spec does not trigger scroll. useRouterScrollToHash compensates by calling scrollIntoView after each navigation.

Same-page hash navigation (clicking #anchor when already on that page) still works natively — the router detects matching pathnames and lets the browser handle it.

Checklist

When adding a new anchor:

  • id attribute set on the target element
  • Source href includes the #anchor fragment
  • useLocationHash() can be called in target page to identify if theres hash and control collapsed content, if layout adjustment is needed