The router lives in @kolektiv/keel. @kolektiv/keel-svelte wraps it, and
React, Vue, Solid, Preact, Lit, and Angular adapters expose the same surface
through their own idioms. The table lists the core exports; the examples below
show each adapter’s spelling. See
Framework adapters
for the full matrix.
| Export | Kind | Notes |
|---|---|---|
router |
core | visit, get/post/put/patch/delete, reload, prefetch, cancel, on |
action |
core | POST /__keel/action/{id} — typed write, not a page |
Link |
component | Renders <a> for GET. Non-GET is a button. |
keel |
action | use:keel on an existing anchor |
page() |
function | Reactive seed |
Form |
component | Progressive enhancement over router.visit |
useForm |
function | Field state, errors, processing, progress |
useAction |
function | TanStack mutation: action, then reload the seed |
Head |
component | Reads page().head; props override |
<script lang="ts">
import { Link, page, router } from "@kolektiv/keel-svelte"
import type { HomePage } from "@app/page-types"
const ctx = page<HomePage>()
</script>
<Link href="/u/ada" prefetch="hover">{ctx.data.viewer?.displayName}</Link>
<a href="/u/ada" use:keel={{ prefetch: "hover" }}>Ada</a>
<button type="button" onclick={() => router.visit("/settings", { preserveScroll: true })}>
Settings
</button><script>
import { Link, page, router } from "@kolektiv/keel-svelte"
const ctx = page()
</script>
<Link href="/u/ada" prefetch="hover">{ctx.data.viewer?.displayName}</Link>
<a href="/u/ada" use:keel={{ prefetch: "hover" }}>Ada</a>
<button type="button" onclick={() => router.visit("/settings", { preserveScroll: true })}>
Settings
</button>import { Link, router, useKeelAnchor, usePage } from "@kolektiv/keel-react"
import type { HomePage } from "@app/page-types"
export default function Viewer() {
const seed = usePage<HomePage>()
const anchor = useKeelAnchor({ href: "/u/ada", prefetch: "hover" })
return (
<>
<Link href="/u/ada" prefetch="hover">{seed.data.viewer?.displayName}</Link>
<a {...anchor}>Ada</a>
<button type="button" onClick={() => router.visit("/settings", { preserveScroll: true })}>
Settings
</button>
</>
)
}import { Link, router, useKeelAnchor, usePage } from "@kolektiv/keel-react"
export default function Viewer() {
const seed = usePage()
const anchor = useKeelAnchor({ href: "/u/ada", prefetch: "hover" })
return (
<>
<Link href="/u/ada" prefetch="hover">{seed.data.viewer?.displayName}</Link>
<a {...anchor}>Ada</a>
<button type="button" onClick={() => router.visit("/settings", { preserveScroll: true })}>
Settings
</button>
</>
)
}<script setup lang="ts">
import { Link, router, useKeelAnchor, usePage } from "@kolektiv/keel-vue"
import type { HomePage } from "@app/page-types"
const seed = usePage<HomePage>()
const anchor = useKeelAnchor({ href: "/u/ada", prefetch: "hover" })
</script>
<template>
<Link href="/u/ada" prefetch="hover">{{ seed.data.viewer?.displayName }}</Link>
<a v-bind="anchor">Ada</a>
<button type="button" @click="router.visit('/settings', { preserveScroll: true })">
Settings
</button>
</template><script setup>
import { Link, router, useKeelAnchor, usePage } from "@kolektiv/keel-vue"
const seed = usePage()
const anchor = useKeelAnchor({ href: "/u/ada", prefetch: "hover" })
</script>
<template>
<Link href="/u/ada" prefetch="hover">{{ seed.data.viewer?.displayName }}</Link>
<a v-bind="anchor">Ada</a>
<button type="button" @click="router.visit('/settings', { preserveScroll: true })">
Settings
</button>
</template>import { Link, router, useKeelAnchor, usePage } from "@kolektiv/keel-solid"
import type { HomePage } from "@app/page-types"
export default function Viewer() {
const seed = usePage<HomePage>()
const anchor = useKeelAnchor({ href: "/u/ada", prefetch: "hover" })
return (
<>
<Link href="/u/ada" prefetch="hover">{seed().data.viewer?.displayName}</Link>
<a {...anchor}>Ada</a>
<button type="button" onClick={() => router.visit("/settings", { preserveScroll: true })}>
Settings
</button>
</>
)
}import { Link, router, useKeelAnchor, usePage } from "@kolektiv/keel-solid"
export default function Viewer() {
const seed = usePage()
const anchor = useKeelAnchor({ href: "/u/ada", prefetch: "hover" })
return (
<>
<Link href="/u/ada" prefetch="hover">{seed().data.viewer?.displayName}</Link>
<a {...anchor}>Ada</a>
<button type="button" onClick={() => router.visit("/settings", { preserveScroll: true })}>
Settings
</button>
</>
)
}import { Link, router, useKeelAnchor, usePage } from "@kolektiv/keel-preact"
import type { HomePage } from "@app/page-types"
export default function Viewer() {
const seed = usePage<HomePage>()
const anchor = useKeelAnchor({ href: "/u/ada", prefetch: "hover" })
return (
<>
<Link href="/u/ada" prefetch="hover">{seed.data.viewer?.displayName}</Link>
<a {...anchor}>Ada</a>
<button type="button" onClick={() => router.visit("/settings", { preserveScroll: true })}>
Settings
</button>
</>
)
}import { Link, router, useKeelAnchor, usePage } from "@kolektiv/keel-preact"
export default function Viewer() {
const seed = usePage()
const anchor = useKeelAnchor({ href: "/u/ada", prefetch: "hover" })
return (
<>
<Link href="/u/ada" prefetch="hover">{seed.data.viewer?.displayName}</Link>
<a {...anchor}>Ada</a>
<button type="button" onClick={() => router.visit("/settings", { preserveScroll: true })}>
Settings
</button>
</>
)
}import { html } from "lit"
import { KeelElement, keelAnchor, router } from "@kolektiv/keel-lit"
import type { HomePage } from "@app/page-types"
export default class Viewer extends KeelElement<HomePage> {
render() {
return html`
<keel-link href="/u/ada" prefetch="hover">${this.page.data.viewer?.displayName}</keel-link>
<a ${keelAnchor({ href: "/u/ada", prefetch: "hover" })}>Ada</a>
<button type="button" @click=${() => router.visit("/settings", { preserveScroll: true })}>
Settings
</button>
`
}
}import { html } from "lit"
import { KeelElement, keelAnchor, router } from "@kolektiv/keel-lit"
export default class Viewer extends KeelElement {
render() {
return html`
<keel-link href="/u/ada" prefetch="hover">${this.page.data.viewer?.displayName}</keel-link>
<a ${keelAnchor({ href: "/u/ada", prefetch: "hover" })}>Ada</a>
<button type="button" @click=${() => router.visit("/settings", { preserveScroll: true })}>
Settings
</button>
`
}
}import { Component } from "@angular/core"
import { KeelAnchorDirective, KeelLink, injectKeelPage, router } from "@kolektiv/keel-angular"
import type { HomePage } from "@app/page-types"
@Component({
selector: "app-viewer",
standalone: true,
imports: [KeelAnchorDirective, KeelLink],
template: `
<keel-link href="/u/ada" prefetch="hover">{{ page().data.viewer?.displayName }}</keel-link>
<a [keelAnchor]="{ href: '/u/ada', prefetch: 'hover' }">Ada</a>
<button type="button" (click)="navigate.visit('/settings', { preserveScroll: true })">
Settings
</button>
`,
})
export default class Viewer {
readonly page = injectKeelPage<HomePage>()
readonly navigate = router
}import { Component } from "@angular/core"
import { KeelAnchorDirective, KeelLink, injectKeelPage, router } from "@kolektiv/keel-angular"
@Component({
selector: "app-viewer",
standalone: true,
imports: [KeelAnchorDirective, KeelLink],
template: `
<keel-link href="/u/ada" prefetch="hover">{{ page().data.viewer?.displayName }}</keel-link>
<a [keelAnchor]="{ href: '/u/ada', prefetch: 'hover' }">Ada</a>
<button type="button" (click)="navigate.visit('/settings', { preserveScroll: true })">
Settings
</button>
`,
})
export default class Viewer {
readonly page = injectKeelPage()
readonly navigate = router
}<script lang="ts">
import { Form, useForm } from "@kolektiv/keel-svelte"
const comment = useForm({ body: "" })
</script>
<Form action="/p/hello/comments" method="post" resetOnSuccess>
<textarea name="body"></textarea>
<button type="submit">Post</button>
</Form><script>
import { Form, useForm } from "@kolektiv/keel-svelte"
const comment = useForm({ body: "" })
</script>
<Form action="/p/hello/comments" method="post" resetOnSuccess>
<textarea name="body"></textarea>
<button type="submit">Post</button>
</Form>import { Form, useForm } from "@kolektiv/keel-react"
export default function CommentForm() {
const comment = useForm({ body: "" })
return (
<Form action="/p/hello/comments" method="post" resetOnSuccess>
<textarea
name="body"
value={comment.data.body}
onInput={(event) => comment.set("body", event.currentTarget.value)}
/>
<button type="submit">Post</button>
</Form>
)
}import { Form, useForm } from "@kolektiv/keel-react"
export default function CommentForm() {
const comment = useForm({ body: "" })
return (
<Form action="/p/hello/comments" method="post" resetOnSuccess>
<textarea
name="body"
value={comment.data.body}
onInput={(event) => comment.set("body", event.currentTarget.value)}
/>
<button type="submit">Post</button>
</Form>
)
}<script setup lang="ts">
import { Form, useForm } from "@kolektiv/keel-vue"
const comment = useForm({ body: "" })
</script>
<template>
<Form action="/p/hello/comments" method="post" reset-on-success>
<textarea
name="body"
:value="comment.data.body"
@input="comment.set('body', $event.target.value)"
></textarea>
<button type="submit">Post</button>
</Form>
</template><script setup>
import { Form, useForm } from "@kolektiv/keel-vue"
const comment = useForm({ body: "" })
</script>
<template>
<Form action="/p/hello/comments" method="post" reset-on-success>
<textarea
name="body"
:value="comment.data.body"
@input="comment.set('body', $event.target.value)"
></textarea>
<button type="submit">Post</button>
</Form>
</template>import { Form, useForm } from "@kolektiv/keel-solid"
export default function CommentForm() {
const comment = useForm({ body: "" })
return (
<Form action="/p/hello/comments" method="post" resetOnSuccess>
<textarea
name="body"
value={comment.data.body}
onInput={(event) => comment.set("body", event.currentTarget.value)}
/>
<button type="submit">Post</button>
</Form>
)
}import { Form, useForm } from "@kolektiv/keel-solid"
export default function CommentForm() {
const comment = useForm({ body: "" })
return (
<Form action="/p/hello/comments" method="post" resetOnSuccess>
<textarea
name="body"
value={comment.data.body}
onInput={(event) => comment.set("body", event.currentTarget.value)}
/>
<button type="submit">Post</button>
</Form>
)
}import { Form, useForm } from "@kolektiv/keel-preact"
export default function CommentForm() {
const comment = useForm({ body: "" })
return (
<Form action="/p/hello/comments" method="post" resetOnSuccess>
<textarea
name="body"
value={comment.data.body}
onInput={(event) => comment.set("body", event.currentTarget.value)}
/>
<button type="submit">Post</button>
</Form>
)
}import { Form, useForm } from "@kolektiv/keel-preact"
export default function CommentForm() {
const comment = useForm({ body: "" })
return (
<Form action="/p/hello/comments" method="post" resetOnSuccess>
<textarea
name="body"
value={comment.data.body}
onInput={(event) => comment.set("body", event.currentTarget.value)}
/>
<button type="submit">Post</button>
</Form>
)
}import { html } from "lit"
import { KeelElement, form, useForm } from "@kolektiv/keel-lit"
export default class CommentForm extends KeelElement {
readonly comment = useForm(this, { body: "" })
render() {
return form(
{ action: "/p/hello/comments", method: "post", resetOnSuccess: true },
html`
<textarea
name="body"
.value=${this.comment.data.body}
@input=${(event: Event) =>
this.comment.set("body", (event.target as HTMLTextAreaElement).value)}
></textarea>
<button type="submit">Post</button>
`,
)
}
}import { html } from "lit"
import { KeelElement, form, useForm } from "@kolektiv/keel-lit"
export default class CommentForm extends KeelElement {
readonly comment = useForm(this, { body: "" })
render() {
return form(
{ action: "/p/hello/comments", method: "post", resetOnSuccess: true },
html`
<textarea
name="body"
.value=${this.comment.data.body}
@input=${(event) => this.comment.set("body", event.target.value)}
></textarea>
<button type="submit">Post</button>
`,
)
}
}import { Component } from "@angular/core"
import { KeelFormDirective, injectKeelForm } from "@kolektiv/keel-angular"
@Component({
selector: "app-comment-form",
standalone: true,
imports: [KeelFormDirective],
template: `
<form [keelForm]="{ action: '/p/hello/comments', method: 'post', resetOnSuccess: true }">
<textarea
name="body"
[value]="comment.data.body"
(input)="comment.set('body', $any($event.target).value)"
></textarea>
<button type="submit">Post</button>
</form>
`,
})
export default class CommentForm {
readonly comment = injectKeelForm({ body: "" })
}import { Component } from "@angular/core"
import { KeelFormDirective, injectKeelForm } from "@kolektiv/keel-angular"
@Component({
selector: "app-comment-form",
standalone: true,
imports: [KeelFormDirective],
template: `
<form [keelForm]="{ action: '/p/hello/comments', method: 'post', resetOnSuccess: true }">
<textarea
name="body"
[value]="comment.data.body"
(input)="comment.set('body', $any($event.target).value)"
></textarea>
<button type="submit">Post</button>
</form>
`,
})
export default class CommentForm {
readonly comment = injectKeelForm({ body: "" })
}Visit options
Mirrored on router.visit, Link, and form helpers.
| Option | Meaning |
|---|---|
replace |
Swap history (login, pack switch) |
preserveScroll |
true, or "errors" after 422 |
preserveState |
Same page id: update(), do not remount |
only / except |
Partial data reload |
prefetch |
hover, mousedown, or mount |
viewTransition |
Optional document.startViewTransition |
Prefetch warms both the seed and the entry module. Link prefetches only when
you opt in (prefetch="hover", "mousedown", "mount", or true); the
default is off. Guards never run for a prefetch.