# Tabs shadcn nello stato dell'URL

Tieni il valore di Tabs in un parametro dell'URL: il link diventa condivisibile, il refresh tiene la vista e il tasto indietro funziona.

> Pagina: https://ui.eneawork.it/blog/shadcn-tabs-url-state
> Markdown: https://ui.eneawork.it/blog/shadcn-tabs-url-state.md

*2026-11-01*

Risposta breve: tieni il valore di Tabs in un parametro dell'URL, non in useState. Il componente resta controllato, il link diventa condivisibile e il tasto indietro riporta la vista precedente.

Su npm, misurato il 1 novembre 2026, @radix-ui/react-tabs 1.2.1 dichiara 55.797 byte non compressi, con licenza MIT. Lo stato nell'URL non aggiunge JavaScript: aggiunge un parametro alla cronologia del browser, quindi il costo è nella riga di indirizzo, non nel bundle.

## Il componente

"use client"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; const VIEWS = ["panoramica", "attivita", "fatture"] as const; type View = (typeof VIEWS)[number]; function parseView(raw: string | null): View { return VIEWS.includes(raw as View) ? (raw as View) : "panoramica"; } export function SettingsTabs() { const router = useRouter(); const pathname = usePathname(); const params = useSearchParams(); const view = parseView(params.get("tab")); function onValueChange(next: string) { const query = new URLSearchParams(params.toString()); query.set("tab", next); router.replace(pathname + "?" + query.toString(), { scroll: false }); } return ( {VIEWS.map((item) => ( {item} ))} Contenuto della panoramica Contenuto delle attività Contenuto delle fatture ); }

Due dettagli rendono il componente stabile: value arriva sempre da parseView, quindi non è mai un valore fuori lista, e onValueChange riscrive solo il parametro tab senza perdere gli altri. Il resto della query sopravvive alla navigazione fra le viste.

## Cosa cambia rispetto a useState

## Il valore non valido

Un parametro scritto a mano può contenere qualsiasi cosa. parseView confronta il valore con la lista chiusa e ricade sul valore predefinito: senza questo controllo il componente mostra un pannello vuoto e l'utente non capisce perché.

## Il comando

npx shadcn@latest add tabs

## Record vivi nel catalogo

Per la parte visiva: la libreria shadcn, il componente expandable-tabs come variante animata, e i blocchi impostazioni che contengono schede già impaginate.

## La limitazione

Ogni cambio scheda con replace non lascia traccia nella cronologia, quindi il tasto indietro salta la scheda precedente; con push ogni scheda diventa una voce e il tasto indietro si riempie. Nell'App Router la navigazione passa dal server, quindi su una pagina pesante il cambio scheda costa una richiesta: se i pannelli sono grandi, conviene montarli una volta e nasconderli.

## Dove continuare

Il modello di distribuzione dei componenti è in che cos'è shadcn/ui; la struttura delle rotte è in struttura dell'App Router; il confronto con un'altra libreria è in shadcn/ui vs MUI. Per capire quanto costa un passaggio in più al server leggi INP e reattività.

## FAQ

### Meglio replace o push per le schede?

Replace quando le schede sono viste della stessa pagina e non vuoi riempire la cronologia; push quando ogni scheda deve essere raggiungibile con il tasto indietro. La scelta cambia quello che l'utente si aspetta dal tasto.

### Perché useSearchParams richiede Suspense?

Nell'App Router la lettura dei parametri è dinamica, quindi il componente che la usa va avvolto in Suspense se la pagina è statica, altrimenti la build segnala un errore di prerendering.

### Posso usare Tabs senza Next.js?

Sì. Con React Router la stessa idea si costruisce con useSearchParams del router: leggi il parametro, valida il valore e aggiorna la query al cambio di scheda.

### Il parametro deve avere un nome fisso?

Conviene: tab è corto, leggibile e non collide con i filtri. Un nome generico come view diventa ambiguo quando la pagina ha più di un gruppo di schede.

### Lo stato nell'URL rompe l'indicizzazione?

No, se ogni parametro produce la stessa pagina canonica e non genera duplicati. Il canonical resta l'indirizzo senza query, a meno che la vista non sia un contenuto distinto.
