Type-safe URL query state for Vue 3 — like ref, but stored in the URL.
A Vue 3 port of nuqs by François Best.
pnpm add @andabove/vuqsPeer dependencies — install these if you don't already have them:
pnpm add vue@^3.5 vue-router@^4 @vueuse/routeruseQueryState returns a Ref bound to a URL query parameter. Reading the ref gives you the current parsed value; writing the ref updates the URL.
import{useQueryState,parseAsString}from"@andabove/vuqs";// Ref<string | null> — null when ?q is absentconstsearch=useQueryState("q",parseAsString);// Readconsole.log(search.value);// 'hello' or null// Write — updates the URL automaticallysearch.value="hello";// Remove the param from the URLsearch.value=null;Use .withDefault(value) to get a Ref<T> (never null) and keep URLs clean when the value matches the default:
import{useQueryState,parseAsInteger}from"@andabove/vuqs";// Ref<number> — URL is clean when page === 1 (no ?page=1 in the URL)constpage=useQueryState("page",parseAsInteger.withDefault(1));page.value++;// ?page=2page.value=1;// removes ?page from the URLpage.value=null;// also removes ?page, resets to default (1)By default, query updates replace the current history entry. Use mode: "push" to add a new entry so the back button restores the previous value:
import{useQueryState,parseAsString,parseAsStringLiteral}from"@andabove/vuqs";consttab=useQueryState("tab",parseAsString.withOptions({mode: "push"}));// Or chain it with a default:constsort=useQueryState("sort",parseAsStringLiteral(["asc","desc"]asconst).withDefault("asc").withOptions({mode: "push"}),);| Parser | Type | Notes |
|---|---|---|
parseAsString | string | Identity — accepts any string |
parseAsInteger | number | parseInt base 10, rounds on serialize |
parseAsFloat | number | parseFloat, full precision |
parseAsBoolean | boolean | Strict: only "true" / "false" |
parseAsStringLiteral(values) | Literal | Validates against a readonly array of string literals |
parseAsStringEnum(values) | Enum | Validates against a TypeScript string enum |
parseAsArrayOf(parser, sep?) | T[] | Comma-separated by default; separator is URI-encoded inside values |
parseAsJson(parseFn) | T | JSON with validator-agnostic parse function |
import{useQueryState,parseAsStringLiteral}from"@andabove/vuqs";constcolors=["red","green","blue"]asconst;constcolor=useQueryState("color",parseAsStringLiteral(colors).withDefault("red"));// Ref<'red' | 'green' | 'blue'>import{useQueryState,parseAsStringEnum}from"@andabove/vuqs";enumDirection{Up="up",Down="down",}constdir=useQueryState("dir",parseAsStringEnum<Direction>(Object.values(Direction)).withDefault(Direction.Up),);import{useQueryState,parseAsArrayOf,parseAsInteger}from"@andabove/vuqs";// ?ids=1,2,3 → [1, 2, 3]constids=useQueryState("ids",parseAsArrayOf(parseAsInteger).withDefault([]));parseAsJson is validator-agnostic — you supply any (raw: unknown) => T | null function. This works with Zod, Valibot, or plain conditionals.
import{useQueryState,parseAsJson}from"@andabove/vuqs";import*asvfrom"valibot";constschema=v.object({q: v.string(),page: v.number()});constfilters=useQueryState("filters",parseAsJson((raw)=>{constr=v.safeParse(schema,raw);returnr.success ? r.output : null;}),);// With Zodimport{z}from"zod";constschema=z.object({q: z.string()});constfilters=useQueryState("filters",parseAsJson((raw)=>{constr=schema.safeParse(raw);returnr.success ? r.data : null;}),);Use createParser to build a custom parser that gets the full .withDefault() / .withOptions() builder API:
import{createParser,useQueryState}from"@andabove/vuqs";constparseAsHexColor=createParser({parse(query){return/^[0-9a-f]{6}$/i.test(query) ? query : null;},serialize(value){returnvalue;},});constcolor=useQueryState("color",parseAsHexColor.withDefault("ff0000"));vuqs covers the core use case. Some nuqs features that are React/Next.js specific are not applicable in Vue.
| Feature | vuqs | nuqs |
|---|---|---|
useQueryState (single key) | yes | yes |
useQueryStates (batched multi-key) | planned | yes |
Custom parsers via createParser | yes | yes |
parseAsJson (validator-agnostic) | yes | yes (generic cast) |
| History push / replace | yes | yes |
shallow / server notify | n/a | Next.js only |
Server cache / createSearchParamsCache | n/a | Next.js only |
createLoader / createSerializer | planned | yes |
useTransition loading states | n/a | React only |
| Testing adapter | planned | yes |
This repo is a pnpm workspace:
| Path | Description |
|---|---|
packages/vuqs | Publishable library |
apps/playground | Interactive demo (Vite + Vue Router) |
pnpm install
pnpm dev # start playground at http://localhost:5173
pnpm test# run library unit tests
pnpm typecheck # typecheck library + playground
pnpm build # build library to packages/vuqs/dist
pnpm lint # oxlint
pnpm fmt:check # oxfmt (use pnpm fmt to fix)The playground imports the library source directly (via Vite alias) so changes hot-reload without rebuilding.
Inspired by nuqs by François Best (MIT). This is an independent Vue 3 port.
MIT — see LICENSE