tanstack-router-sfc

TanStack Router for Vue with single-file routes: define the route in a block inside the .vue file that renders it.
3,685
Install
npm install tanstack-router-sfc
Latest Version:1.1.0
Vue:3
License:MIT
Last Updated:Oct 21, 2026
Links: GitHub  ·  npm
Maintainer: hanskristoffer

tanstack-router-sfc

CI npm

TanStack Router for Vue where one .vue file is the whole route: the route definition lives in a <router lang="ts"> custom block next to the component that renders it.

<template>
	<article>{{ post.title }}</article>
</template>

<script setup lang="ts">
// `Route` is in scope here - no import.
const post = Route.useLoaderData()
</script>

<router lang="ts">
import { createFileRoute, notFound } from '@tanstack/vue-router'
import { getPost } from '@/lib/posts'

export default createFileRoute('/posts/$postId')({
	loader: async ({ params }) => {
		const post = await getPost(params.postId)
		if (!post) throw notFound()
		return post
	}
})
</router>

This replaces the x.ts + x.component.vue pair. Both conventions work in the same routes directory, so migration can be gradual.

Setup

// vite.config.ts
import { tanstackRouterSfc } from 'tanstack-router-sfc'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
	plugins: [...tanstackRouterSfc(), vue()]
})
// tsconfig.json - typechecking and IDE support for the block
{
	"vueCompilerOptions": {
		"plugins": ["tanstack-router-sfc/volar"]
	}
}

tanstackRouterSfc wraps tanstackRouter from @tanstack/router-plugin, so it takes the same options (routesDirectory, generatedRouteTree, autoCodeSplitting, codeSplittingOptions, plugins, ...) plus blockType (default 'router'). target and addExtensions are managed for you.

Peer dependencies: vite, vue, @tanstack/router-plugin, @tanstack/vue-router.

Rules

  • The block must be <router lang="ts"> and must export default createFileRoute('<path>')({ ... }) - exactly one, with a string-literal path. __root.vue uses createRootRoute* instead.
  • A stale path after a rename is fixed in place by the generator, the same way it fixes .ts route files.
  • Route is available in <script setup> without an import. It is not available in <template> (it is a <script> binding, like any plain <script> value) - assign it to a const in setup if the template needs it.
  • An SFC with a <template> or <script setup> becomes the route's component, loaded lazily. A block-only .vue (redirect or pathless layout) gets no component.
  • Piece files (x.component.vue, x.errorComponent.vue, x.lazy.tsx, ...) and plain .ts route files are untouched.
  • Every non-piece .vue file under the routes directory must have a block. Use the routeFileIgnorePrefix (-) for helper files.

How it works

Route generation, file watching, formatting, code splitting and route HMR are the official tanstackRouter plugin's job - this composes it with addExtensions: true, so the generated tree imports ./routes/x.vue and TypeScript can see the block's default-exported route as Route. On top of that:

  1. a generator plugin (afterTransform) requires every single-file route to have a block, keeps its createFileRoute('...') id in sync when the file moves, and scaffolds an empty .vue as an SFC;
  2. :route-tree rewrites the tree's ./routes/x.vue imports at transform time to x.vue.tsr-router.ts - the block alone - so the route tree never pulls a component into the entry chunk;
  3. :sfc replaces the <router> block with a <script> shim that imports the default-exported route as Route from that module, preserving the file's line count so template and setup diagnostics stay accurate;
  4. :router-block serves x.vue.tsr-router.ts: the block's TypeScript, padded to the original line numbers, plus Route.update({ component: lazyRouteComponent(() => import('./x.vue')) }).

volar.cjs splices the block into the SFC's generated TypeScript so vue-tsc (and golar, and the VS Code Vue extension) typecheck it in place.

HMR

Editing <template> / <script setup> / <style> hot-updates as usual. Editing the <router> block triggers a full reload - the router is built once at startup, so a changed route definition cannot be swapped in place.

Note on autoCodeSplitting

Single-file routes are split by construction, so you do not need it for them. It is passed through to the official plugin unchanged, which is what still splits inline component: options in .ts route files.

App kit: tanstack-router-sfc/app

Runtime logic for Vue apps on TanStack Router with Nuxt UI. It ships no components: the app keeps its design-system shells and wires them to these. Every function that navigates uses useRouter() from the component it runs in, or takes a router.

Area Exports
Nuxt UI on TanStack Router vueRouterShimOptions(history) - app.use(createRouter(vueRouterShimOptions(createWebHistory())))
URL search state useQueryParam, useRouteSearchModels, useRouteSearchSync, useRouterPush, searchFlag, defineStaticData
Route loading routeLoadingMode, trackRouteLoading(router)
Dialogs opened by URL createDialogs({ dialogs, host, useOverlay, confirm }) -> useOpenDialog, useConfirmAction, useDialogDeepLink; useDialogHost for the host component
Maintenance / API down createMaintenanceMode({ probe }), parseApiError
Forms formStateFromSchema, useEntityForm, useFormSubmit, useSettingsFormSection, provideSaveScope / useSaveSection, useFormFieldModel, useFieldHighlight, formStateKey, formIdKey

Toasts and translations are the app's: provide them once.

app.provide(formFeedbackKey, () => {
	const toast = useToast()
	const { t } = useI18n()
	return {
		success: (title) => toast.add({ color: 'success', title }),
		error: (error, title) =>
			toast.add({ color: 'error', title: title ?? t('saveFailed') })
	}
})

vue-router (the shim) and zod (formStateFromSchema types) are optional peer dependencies; the kit imports neither at runtime.

Development

bun run test        # unit tests + a real `vite build` of test/fixture
bun run typecheck
bun run build       # tsdown -> dist/

Releasing

Squash PRs with conventional titles (fix:, feat:, or feat!:). Release Please keeps the version and changelog in a release PR; merge that PR to publish with release notes and npm provenance. See release and recovery instructions.

Related Packages

vite-plugin-vue-facing-decorator-hmr

Fix HMR support for Vue components with vue-facing-decorator

103,854 4
@jeresiliemoncontrat/vite-plugin-vue-facing-decorator-hmr

Fix HMR support for Vue components with vue-facing-decorator

1,005 4
vite-plugin-vue-tailwind-auto-reference

Automatically @reference your Tailwind CSS file in Vue SFC blocks.

51,792 34
@miyaoka/vite-plugin-doc-block

Vite plugin to strip custom blocks from Vue SFC

6,898 0
vite-plugin-vue-i18n-sfc-auto-import

Enable component-scoped i18n with Option API when using vue-i18n in composition...

1,835 1

Version History

Version Released License
1.1.0 MIT
1.0.2 MIT
1.0.1 MIT
1.0.0 MIT