# The URL, structure and scroll position

> Keep what the user sees in the URL so links and the back button work, arrange screens the way people think, and stop lists jumping when content loads.

Module: Layout and pages · Level: intermediate · 8 min · https://school.agents.org.in/design/layout/url-and-structure

Pick a tab, filter to "Assigned to me", change the sort. Then press "Open link in a new tab". Try it in both modes.

*[Interactive demo: State in memory resets when shared. State in the URL is the view.]*

When the view lived in component state, the new tab forgot everything. When it lived in the URL, the link *was* the view: same tab, same filter, same sort. And Back stepped through your changes.

## The URL is the source of truth

The rule from Vercel's interface guidelines: **anything a user can see right now should be rebuildable from the URL.** Paste it in a new tab and you land on the same view. That's URL as state (Keeping what the user is looking at (tab, filters, sort, open panel) in the web address, so a link, a bookmark or the back button brings back exactly the same view).

| State | Where | Example |
|---|---|---|
| The page | path | `/projects/acme/issues` |
| Tab | query parameter (The part of a URL after the ?, like ?tab=closed&sort=created. It is where page state such as filters lives) | `?tab=closed` |
| Filters | query | `?status=open,blocked&assignee=me` |
| Sort | query | `?sort=-updated_at` |
| Pagination | query | `?cursor=eyJpZCI6...` |
| Open panel or modal | query | `?modal=invite` |
| Search | query | `?q=auth+bug` |

What does **not** go in the URL: form drafts (local storage), login state (cookies), whether a toast is showing, hover and focus.

> **In plain words.** If you could send someone a screenshot of what you're looking at, the link should take them to the same thing. A link you can share, bookmark and go back to is a feature you get almost for free.

Three rules make it work:

- **Deep-link every modal.** `?modal=invite-team` should open the invite dialog straight from the link. That's a deep link (A link that opens a specific view inside an app, such as one open modal or one filtered list, instead of the home page).
- **One source.** Read state from the URL (`useSearchParams` in Next.js) and write through the router. Copying URL state into `useState` creates two versions that drift.
- **Back restores scroll.** Normal links and most routers do this; a custom scroll lock or an intercepted click often breaks it. Check yours.

## Structure the way people think

Information architecture (How a product's screens and features are grouped, named and connected, so people can find things. It is decided before any pixel is styled) decides what people can find before any pixel is styled. The rule under all of it: arrange and name things the way users think about them, not the way the code or the org chart is split.

- **Long settings get sections.** Past about a dozen options, split them under a side nav with named groups (Account, Billing, Notifications), each a short page.
- **Progressive disclosure (Showing what most people need first and revealing advanced options only when asked or when they become relevant)**: show what most people need; put rare options behind "Advanced", or reveal a feature when it becomes relevant. Benji Taylor describes how Family kept the basics at hand and revealed the rest by context.
- **Name by the user's words.** "Send messages in the background", not "Enable async relay mode".
- **Tabs are views of one thing** (this week / month / year), not unrelated destinations.
- **Two levels of menu, then stop.** Deeper than that, use pages with breadcrumbs and a URL.
- **Search logs are rename requests.** If people keep searching "invoices" and the nav says "Billing", rename it.

  **Dead end (avoid):**

"Something went wrong." and a Back button.

  **A way forward (do this):**

"That link expired. Request a new one." with the button that does it.

**Renaming beats restructuring.** Before redrawing the sitemap, check whether the labels are the problem; a label is a one-line fix.

## Keep the reader's place

One more way a page loses people: you're reading a chat log, older messages load above, and the message you were on jumps away. Try it with anchoring off, then on.

*[Interactive demo: Load older messages with anchoring off, then on. The highlighted message is where you were reading.]*

Scroll anchoring (Keeping the row you are reading in the same spot on screen when content above it loads or changes size, instead of letting the page jump) means tying the view to a row, not to a pixel offset. Cheng Lou's pattern:

  **Step: Pick an anchor when the user scrolls.** The first row whose bottom is below the top of the view. Record how far it is from the top of the view.

  **Step: When content changes, restore that distance.** After the new rows render, set the scroll position so the anchor row sits exactly where it was.

  **Step: Detect user scrolling by value.** Compare the scroll position with the one you stored. Don't trust "a scroll event fired": your own scroll fires one too.

  **Step: Store what the browser actually set.** Read the scroll position back after setting it, because browsers round. Storing the value you asked for makes the next frame think the user scrolled.

If you anchor in code, set `overflow-anchor: none` on that scroller so the browser's built-in anchoring doesn't move it a second time. For a simple page, the browser's own `overflow-anchor` is the cheaper option. *(A chat that was showing the latest message should keep showing it after a new one arrives.)*

**URL state and stable lists (paste into your agent):**

```text
Refactor this list page:
- Tab, filters, sort, search, pagination cursor and any open panel or modal live in URL query params. Read with useSearchParams, write through the router; no mirrored useState.
- Every modal is deep-linkable (?modal=...). Back must restore the previous view and its scroll position.
- When older items load above the view, keep the row in view anchored: record the first visible row's offset before the update and restore it after; set overflow-anchor: none on the scroller.
- Check the labels against what users search for and suggest renames before restructuring the nav.
```

**Check yourself:** A user filters a table and sends the link to a colleague, who sees the unfiltered table. What's the fix?

- [ ] Store the filters in localStorage
- [x] Put the filters in the URL query and read them from there
- [ ] Add a Share button that emails a screenshot
- [ ] Cache the filters on the server per user

*Why:* Visible state belongs in the URL, so a shared or bookmarked link rebuilds the same view.

## In the skill

- https://school.agents.org.in/skill/layout/url-as-state.md
- https://school.agents.org.in/skill/layout/information-architecture.md
- https://school.agents.org.in/skill/layout/scroll-anchoring.md
