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 absent
const search = useQueryState("q", parseAsString);
// Read
console.log(search.value); // 'hello' or null
// Write — updates the URL automatically
search.value = "hello";
// Remove the param from the URL
search.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)
const page = useQueryState("page", parseAsInteger.withDefault(1));
page.value++; // ?page=2
page.value = 1; // removes ?page from the URL
page.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";
const tab = useQueryState("tab", parseAsString.withOptions({ mode: "push" }));
// Or chain it with a default:
const sort = useQueryState(
"sort",
parseAsStringLiteral(["asc", "desc"] as const)
.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";
const colors = ["red", "green", "blue"] as const;
const color = useQueryState("color", parseAsStringLiteral(colors).withDefault("red"));
// Ref<'red' | 'green' | 'blue'>import { useQueryState, parseAsStringEnum } from "@andabove/vuqs";
enum Direction {
Up = "up",
Down = "down",
}
const dir = useQueryState(
"dir",
parseAsStringEnum<Direction>(Object.values(Direction)).withDefault(Direction.Up),
);import { useQueryState, parseAsArrayOf, parseAsInteger } from "@andabove/vuqs";
// ?ids=1,2,3 → [1, 2, 3]
const ids = 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 * as v from "valibot";
const schema = v.object({ q: v.string(), page: v.number() });
const filters = useQueryState(
"filters",
parseAsJson((raw) => {
const r = v.safeParse(schema, raw);
return r.success ? r.output : null;
}),
);// With Zod
import { z } from "zod";
const schema = z.object({ q: z.string() });
const filters = useQueryState(
"filters",
parseAsJson((raw) => {
const r = schema.safeParse(raw);
return r.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";
const parseAsHexColor = createParser({
parse(query) {
return /^[0-9a-f]{6}$/i.test(query) ? query : null;
},
serialize(value) {
return value;
},
});
const color = 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