svelte-vitals

Use when writing or reviewing SvelteKit routes/components — svelte-vitals rule knowledge (SEO, performance, correctness, security, architecture, accessibility) and how to run the scanner.

oekazuma/svelte-vitals1 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: svelte-vitals
description: Use when writing or reviewing SvelteKit routes/components — svelte-vitals rule knowledge (SEO, performance, correctness, security, architecture, accessibility) and how to run the scanner.
license: MIT
---

<!-- Generated by `pnpm --filter svelte-vitals run gen:skills` — do not edit by hand. -->

# svelte-vitals

## When to use

Use this whenever you are writing or reviewing SvelteKit route files (`+page.svelte`, `+layout.svelte`) or components in this project — svelte-vitals statically checks SEO, performance, correctness, security, architecture, and accessibility patterns.

## Playbook

1. After writing or editing code, run `npx svelte-vitals . --diff --reporter agent` and fix any findings it reports.
2. Before committing, run `npx svelte-vitals . --staged` as a pre-commit gate.
3. For a rule's full rationale, configurable options and fix examples, run `npx svelte-vitals explain <rule-id>` (add `--json` for a structured object) or open its docs link below. The fix text for a specific finding comes from the run itself, not from `explain` — see the note above the rule digest.
4. When a finding is correct by design — a reviewed `{@html}` that is already sanitized, a deliberate `$effect` — clear it with an inline directive instead of changing the code: `<!-- svelte-vitals-disable-next-line <rule-id> -->` on the line above in markup, or `// svelte-vitals-disable-next-line <rule-id>` inside `<script>`. Review it first; never suppress a finding you have not read.
5. Exit codes: `0` nothing failing, `1` a critical finding (or your `--fail-on`/`--min-health` threshold), `2` the run itself failed — a `2` says nothing about the code, so never read it as a clean or dirty result.
6. For anything else — reporters, the config file, scoping to a change, CI, monorepos — run `npx svelte-vitals docs list` and then `npx svelte-vitals docs show <name>`. Those guides ship inside the CLI, so they match the version installed here; prefer them over searching the web.

## Rule digest

A `Fix:` below is the rule's canonical fix, the same for every occurrence. A line without one is not a rule without a fix — those rules word their fix per finding, so take it from the finding itself: `recommendation` on each issue in `--reporter json`, printed as `Fix:` by `--reporter agent`.

### SEO

- **seo/title-presence — Title presence** (critical): A unique, non-empty <title> is the single strongest on-page SEO signal and the text shown in search results and browser tabs. Fix: Add a <title> inside <svelte:head> (a dynamic title is fine). ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/title-presence))
- **seo/description-presence — Description presence** (warning): A meta description is the snippet search engines show under your title; without one they invent one from page text, often poorly. Fix: Add a <meta name="description"> inside <svelte:head>, or set description on your meta component. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/description-presence))
- **seo/canonical-url — Canonical URL** (warning): A canonical URL tells search engines which URL is authoritative, preventing duplicate-content dilution across query-string variants of the same page. Fix: Add <link rel="canonical"> inside <svelte:head>, or set the canonical prop on your meta component. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/canonical-url))
- **seo/og-image — Open Graph image** (warning): og:image is the preview thumbnail shown when the page is shared on social platforms; without it links render bare and get fewer clicks. Fix: Add <meta property="og:image">, or set openGraph.images on your meta component. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/og-image))
- **seo/og-title — Open Graph title** (warning): og:title controls the headline shown when the page is shared on social platforms, independent of the document <title>. Fix: Add <meta property="og:title">, or set openGraph.title on your meta component. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/og-title))
- **seo/robots-txt — robots.txt** (warning): robots.txt tells crawlers which paths they may fetch and points them to your sitemap; missing it leaves crawl behaviour to defaults. Fix: Create static/robots.txt (or a src/routes/robots.txt/+server endpoint). ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/robots-txt))
- **seo/sitemap-xml — sitemap.xml** (warning): A sitemap.xml lists your URLs so search engines can discover and prioritise them, especially pages not well linked internally. Fix: Create static/sitemap.xml (or a src/routes/sitemap.xml/+server endpoint). ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/sitemap-xml))
- **seo/json-ld — JSON-LD structured data** (info): JSON-LD structured data lets search engines render rich results (breadcrumbs, articles, products) for the page. Fix: Add a JSON-LD <script> inside <svelte:head> with literal JSON (Svelte emits the script body as-is). ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/json-ld))
- **seo/html-lang — <html lang>** (warning): The <html lang> attribute tells screen readers how to pronounce the page, browsers whether to offer translation, and other assistive tools how to handle the content — Google has said it does not use lang for ranking. Fix: Set the lang attribute on <html> in src/app.html. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/html-lang))
- **seo/indexability — Indexability** (info): A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it. Fix: If this route should be indexed, drop noindex from its <meta name="robots">. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/indexability))
- **seo/twitter-card — Twitter Card** (info): twitter:card selects how the page renders when shared on X/Twitter; without it the platform falls back to a basic link (Open Graph tags are used as fallbacks for the rest). Fix: Add a twitter:card meta tag in <svelte:head>. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/twitter-card))
- **seo/og-description — Open Graph description** (info): og:description is the summary shown under the title in social previews; without it platforms guess or show nothing, lowering click-through. The Open Graph protocol lists it as an optional property. Fix: Add an og:description meta tag in <svelte:head>. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/og-description))
- **seo/og-url — Open Graph URL** (warning): og:url tells social platforms the canonical address to attribute shares and likes to, consolidating engagement on one URL. The Open Graph protocol lists it as a required property. Fix: Add an og:url meta tag in <svelte:head>. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/og-url))
- **seo/viewport — Viewport** (warning): Without a viewport meta tag mobile browsers render the page at a fixed ~980px layout viewport and scale it to fit, so text and controls end up too small to read or tap without pinch-zooming. Fix: Add the viewport meta tag (typically in src/app.html <head>). ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/viewport))
- **seo/sitemap-in-robots — Sitemap referenced in robots.txt** (info): A Sitemap: line in robots.txt helps crawlers discover your sitemap; without it discovery relies on manual submission. Fix: Add a Sitemap: line to static/robots.txt. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/sitemap-in-robots))
- **seo/json-ld-validity — JSON-LD validity** (warning): Invalid JSON-LD — unparseable, missing @context/@type, or declaring a @type that is not a real schema.org type — is silently ignored by search engines, so the structured data does nothing. Fix: Make the JSON-LD valid: parseable JSON with both @context (schema.org) and @type. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/json-ld-validity))
- **seo/json-ld-deprecated-type — Deprecated structured-data type** (info): Some schema types no longer produce rich results, so the markup adds weight without the SERP benefit. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/json-ld-deprecated-type))
- **seo/json-ld-relative-url — JSON-LD relative URL** (warning): Search engines need absolute URLs in structured data; a relative URL cannot be resolved reliably. Fix: Replace relative URLs in JSON-LD with absolute URLs. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/json-ld-relative-url))
- **seo/json-ld-date-format — JSON-LD date format** (info): Schema.org date properties expect ISO-8601; other formats may be ignored or misparsed. Fix: Format JSON-LD date properties as ISO-8601. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/json-ld-date-format))
- **seo/json-ld-placeholder — JSON-LD placeholder text** (info): Leftover placeholder text (e.g. "Your Company Name", "lorem ipsum") ships misleading structured data. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/json-ld-placeholder))
- **seo/json-ld-required-props — JSON-LD required properties** (warning): A recognized @type missing its required properties is ineligible for the corresponding rich result. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/json-ld-required-props))
- **seo/title-length — Title length** (info): A title that is too short wastes the strongest on-page signal; one that is too long is truncated in the SERP. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/title-length))
- **seo/description-length — Description length** (info): A description that is too short under-uses the SERP snippet; one that is too long is truncated by search engines. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/description-length))
- **seo/charset — Character encoding** (warning): Without a declared character encoding the browser must guess, which can render text as mojibake; <meta charset="utf-8"> is the standard declaration. Fix: Add the charset meta tag (typically the first line of <head> in src/app.html). ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/charset))
- **seo/image-alt — Image alt text** (warning): An <img> with no alt attribute is invisible to image search and assistive technology; a descriptive alt is an image-SEO signal. Fix: Add a descriptive alt attribute to the <img> (or alt="" if purely decorative). ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/image-alt))
- **seo/hreflang — hreflang validity** (warning): A malformed hreflang code breaks international targeting outright. A missing x-default is a Google recommendation for language-selector or auto-redirecting pages, not a defect on every multilingual site. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/hreflang))
- **seo/single-h1 — Heading hierarchy** (warning): A page should have a primary heading naming its main topic. Zero <h1> leaves the page without one; a single, clear <h1> is the conventional signal, though multiple <h1>s are tolerated by modern heading algorithms. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/single-h1))
- **seo/duplicate-title — Duplicate title** (warning): Duplicate titles across pages make them compete in search results and weaken each page’s relevance signal. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/duplicate-title))
- **seo/duplicate-description — Duplicate description** (warning): Duplicate meta descriptions give search engines no per-page summary, so they are often ignored or rewritten. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/duplicate-description))
- **seo/heading-level-skip — Heading order** (info): Skipping a heading level breaks the document outline that assistive tech relies on to navigate page structure, and that search engines use as a structural signal. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/heading-level-skip))
- **seo/ssr-disabled — SSR disabled** (warning): SvelteKit's SEO guidance is to leave SSR on unless there is a good reason not to: server-rendered content is indexed more frequently and reliably, and SPA mode costs an extra network round trip before anything renders. ([docs](https://oekazuma.github.io/svelte-vitals/rules/seo/ssr-disabled))

### Performance

- **performance/image-dimensions — Image dimensions** (warning): An <img> without explicit width and height can trigger layout shift (CLS) as it loads, hurting Core Web Vitals and visual stability — unless the box is reserved another way, e.g. CSS aspect-ratio. Fix: Add explicit width and height attributes to the <img>. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/image-dimensions))
- **performance/image-loading-hint — Image loading hint** (info): A loading attribute lets the browser defer offscreen images; without it images load eagerly and can delay more important content. Static analysis cannot tell which image is the LCP, so this is advisory. Fix: Add loading="lazy" to offscreen <img> elements (leave the LCP/hero image eager). ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/image-loading-hint))
- **performance/preload-missing-as — Preload missing as** (warning): A `<link rel="preload">` without an `as` attribute is ignored by the browser (or fetched a second time), wasting the preload. Fix: Add an `as` attribute matching the resource type to the preload link. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/preload-missing-as))
- **performance/font-preload-crossorigin — Font preload missing crossorigin** (warning): A font preload without `crossorigin` does not match the actual (CORS) font request, so the preloaded file is never used and the font downloads twice. Fix: Add the `crossorigin` attribute to the font preload link. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/font-preload-crossorigin))
- **performance/lcp-image — LCP image eager loading** (warning): Lazy-loading the LCP (first/above-the-fold) image delays the largest paint and hurts Core Web Vitals. The first image is the best static proxy for the LCP candidate. Fix: Remove loading="lazy" from the first/LCP image; consider fetchpriority="high". ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/lcp-image))
- **performance/responsive-image — Responsive image** (info): An <img> without srcset ships one fixed-size asset to every device, wasting bytes on small screens. Static analysis cannot measure intended display size, so this is advisory. Fix: Add a srcset (and sizes) to the <img> for responsive delivery. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/responsive-image))
- **performance/render-blocking-script — Render-blocking script** (warning): A synchronous <script src> in <head> blocks HTML parsing until it downloads and runs, delaying first paint. defer, async, or type="module" avoids the block. Fix: Add defer (or type="module") / async to the head <script>. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/render-blocking-script))
- **performance/preconnect — Preconnect third-party origin** (info): Connecting to a third-party origin (DNS + TCP + TLS) is costly; a preconnect/dns-prefetch hint starts it early so the resource arrives sooner. Fix: Add a preconnect hint for the third-party origin. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/preconnect))
- **performance/heavy-import — Heavy dependency import** (info): Importing a large, non-tree-shakeable package pulls its whole weight into the bundle even when only a fraction is used, slowing load. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/heavy-import))
- **performance/namespace-import — Namespace import** (info): A namespace import (import * as X) is only tree-shakeable while every access to X stays static; passing X around or indexing it dynamically forces the bundler to keep the whole module. Named imports are reliably shakeable and make the dependency surface explicit. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/namespace-import))
- **performance/minify-disabled — Minification disabled** (warning): Disabling minification ships unminified JS/CSS to production, inflating bundle size several-fold and slowing every page load; the override is usually a leftover from debugging. Fix: Remove the minify: false override from vite.config (Vite minifies by default), or scope it to non-production builds. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/minify-disabled))
- **performance/load-waterfall — Load waterfall** (warning): In a universal load, every await that depends on a previous result costs a full network round trip from the browser on client-side navigation; chains multiply latency on every page visit. A server load runs the same hops server-side. Fix: Move the dependent await chain into a server load (+page.server.ts), where hops run server-to-server. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/load-waterfall))
- **performance/sequential-awaits — Sequential independent awaits** (info): Awaits that do not use each other's results still run one after another, adding their latencies; starting them together costs nothing and bounds the wait to the slowest request. Fix: Start the independent requests together and await them with Promise.all. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/sequential-awaits))
- **performance/state-raw — Raw state opportunity** (info): Objects and arrays in $state are made deeply reactive through proxying, which taxes every property access. A binding that is only ever reassigned — API responses are the canonical case — never uses that machinery; Svelte's own guidance is to use $state.raw for it. Fix: Replace $state(...) with $state.raw(...); keep the same initializer. ([docs](https://oekazuma.github.io/svelte-vitals/rules/performance/state-raw))

### Correctness

- **correctness/each-key — Keyed each block** (warning): An unkeyed {#each} adds/removes nodes at the end and rewrites the data of the DOM nodes in between when the list reorders, so element state/focus sticks to positions instead of items; a key lets Svelte insert, move, and delete the right nodes instead. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/each-key))
- **correctness/each-index-key — Index used as each key** (warning): Svelte's guidance is explicit: the key must uniquely identify the object — do not use the index. An index key gives items position-based identity, so element state (focus, inputs, transitions) sticks to positions when the list reorders or items are inserted or removed, exactly like an unkeyed block — but the visible key masks the problem. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/each-index-key))
- **correctness/effect-as-derived — Effect used to derive state** (warning): An $effect whose body only assigns to $state is the "useEffect → $effect" anti-pattern: it reruns after render and can cause extra passes or loops. $derived expresses the same dependency declaratively. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/effect-as-derived))
- **correctness/effect-as-onmount — Effect used as onMount** (warning): An $effect whose body reads no reactive value visible to this analysis runs once after mount and never re-runs on the paths it can see — usually a sign the code belongs in an event handler, {@attach}, or onMount instead of $effect. This can't see a reactive value reached only through a plain function's return value, so a genuinely reactive effect built that way can still be flagged. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/effect-as-onmount))
- **correctness/unmutated-state — Unmutated $state** (info): A $state that is never mutated pays for reactivity (deep proxying, tracking) it never uses; const (or $state.raw) is clearer and cheaper. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/unmutated-state))
- **correctness/prop-mutation — Mutated non-bindable prop** (warning): Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. In legacy mode, mutating methods like .push()/.splice() never trigger an update on their own — Svelte's reactivity there is based on assignments, not mutations. Neither case is caught by the compiler, so this rule catches both statically. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/prop-mutation))
- **correctness/stale-prop-derivation — Stale prop derivation** (warning): Svelte's guidance is to treat props as though they will change: a plain `let color = type === 'danger' ? 'red' : 'green'` freezes the first render's value, so the UI silently stops tracking the parent when the prop changes. In runes mode, $derived keeps the computation live at no cost; in legacy mode (export let props), a $: reactive statement does the same job. Fix: Wrap the prop-derived computation in $derived(...) (or $derived.by(() => ...) for a function body) in runes mode, or prefix the assignment with $: in legacy mode, keeping the same expression. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/stale-prop-derivation))
- **correctness/nonreactive-builtin-state — Non-reactive built-in in $state** (warning): $state deep-proxies plain objects and arrays only; built-in collection, date, and URL instances stay untracked, so property-level changes never reach effects, deriveds, or the template. Svelte's own answer is the drop-in classes in svelte/reactivity. Fix: Import Svelte<Type> from 'svelte/reactivity' and replace new <Type>(...) with new Svelte<Type>(...) — the API is identical. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/nonreactive-builtin-state))
- **correctness/checkable-bind-value — bind:value on a checkable input** (warning): bind:value binds the DOM value property. A checkbox/radio's user interaction toggles checkedness, which bind:value never observes. On a checkbox this throws bind_invalid_checkbox_value in a development build; in production the check is skipped and the binding silently tracks the value attribute instead of checkedness. On a radio it throws nothing in either build — it renders once with the initial value, then silently never updates. Svelte's checked/grouped bindings (bind:checked, bind:group) are built for exactly this. Fix: For a single checkbox, replace bind:value={x} with bind:checked={x} (x becomes a boolean). For a checkbox list or radio group, replace bind:value={x} with bind:group={x} on every input sharing the group, keeping each input's static value attribute to identify the option. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/checkable-bind-value))
- **correctness/orphan-effect — Orphan $effect** (critical): An $effect created outside component initialisation throws effect_orphan at runtime. The compiler does not catch it — the server compiler deletes $effect calls entirely, so SSR renders without error — and the crash happens client-side, when the module evaluates in the browser, breaking hydration rather than producing a server error. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/orphan-effect))
- **correctness/orphan-lifecycle — Lifecycle call outside component initialisation** (critical): Svelte lifecycle and context functions require an active component context; called at module scope, in a shared-state class constructor, or in a load/handler they throw lifecycle_outside_component at runtime — the compiler does not catch it, and it surfaces as a production crash. Exception: in a Kit module that only ever runs on the server (+page.server.ts, +server.ts, hooks.server.ts), onMount/beforeUpdate/afterUpdate/createEventDispatcher are silent no-ops there instead, and onDestroy throws a plain TypeError rather than lifecycle_outside_component — only getContext/setContext/hasContext/getAllContexts still throw in that channel. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/orphan-lifecycle))
- **correctness/base-path-navigation — Root-relative navigation under a base path** (warning): A root-relative literal resolves against the domain root, not kit.paths.base, so navigation lands outside an app served from a sub-path. The break only appears once the app is deployed under its base — locally base is usually empty, so every such link works. Fix: Import { resolve } from '$app/paths' and wrap the path: href={resolve('/about')}, goto(resolve('/about')), redirect(303, resolve('/login')). ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/base-path-navigation))
- **correctness/server-browser-global — Browser global in server module code** (critical): window, document, localStorage and friends do not exist on the server; a read in module scope or a load/handler crashes SSR with a ReferenceError — the compiler does not catch it, and it surfaces as a production 500. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/server-browser-global))
- **correctness/instance-browser-global — Browser global during component initialisation** (warning): A component instance script runs on the server on every SSR render, where window/document/localStorage do not exist. Warning, not critical: a component rendered only behind a parent {#if browser} (or a client-only dynamic import) is a legitimate pattern that static analysis cannot prove cross-file. ([docs](https://oekazuma.github.io/svelte-vitals/rules/correctness/instance-browser-global))

### Security

- **security/raw-html — Raw HTML render** (warning): {@html} renders its value as unescaped HTML; if the value can contain user input and is not sanitized, it is a cross-site-scripting (XSS) vector. ([docs](https://oekazuma.github.io/svelte-vitals/rules/security/raw-html))
- **security/javascript-url — javascript: URL** (warning): A javascript: URL in href/src/action/formaction breaks under a strict Content-Security-Policy and turns what should be a real navigation into inline script execution on activation — use an event handler on a <button> instead (the same shape is also a classic XSS vector, though detection here is literal-only, so every flagged URL is author-written, not injected). ([docs](https://oekazuma.github.io/svelte-vitals/rules/security/javascript-url))
- **security/handler-state-write — Handler writes imported state** (critical): SvelteKit's docs mark this NEVER-DO-THIS: the server is one long-lived process shared by every user, so module state written during a request is visible to ALL later requests. ([docs](https://oekazuma.github.io/svelte-vitals/rules/security/handler-state-write))
- **security/server-module-state — Server module-scope state** (warning): Module scope on the server is one shared, long-lived instance (SvelteKit docs: "Avoid shared state on the server"): a value reassigned during one user's request is served to every other user, and it silently resets on every deploy or restart. ([docs](https://oekazuma.github.io/svelte-vitals/rules/security/server-module-state))
- **security/shared-state-import — Shared runes-state import on the server** (warning): A .svelte.ts module with module-scope $state is one shared instance on the server: mutated, it leaks data between users; read-only, every request sees the same boot-time value instead of per-user data. ([docs](https://oekazuma.github.io/svelte-vitals/rules/security/shared-state-import))

### Architecture

- **architecture/component-size — Component size** (info): A very large component is hard to read, test, and reuse, and is a common sign that several responsibilities should be split out. ([docs](https://oekazuma.github.io/svelte-vitals/rules/architecture/component-size))
- **architecture/prop-count — Prop count** (info): A component taking many props is usually doing too much; grouping or splitting keeps its API understandable. ([docs](https://oekazuma.github.io/svelte-vitals/rules/architecture/prop-count))
- **architecture/private-scope-import — Private-scope import** (info): A unit placed inside a private directory is written for one owner; importing it from elsewhere couples two parts of the tree that were meant to move independently, and the unit belongs higher up instead. Fix: Move this unit out of its private scope, to the directory shared by all of its importers, and update this import. (inert until configured) ([docs](https://oekazuma.github.io/svelte-vitals/rules/architecture/private-scope-import))
- **architecture/unit-entry-file — Unit entry file** (info): A directory named after a unit but missing that unit's entry file is either an incomplete unit or a grouping wearing the wrong name; either way the tree no longer says what it means, and tooling that resolves by convention starts guessing. Fix: Make the directory and its entry file agree — add the entry file, or stop declaring this directory a unit. (inert until configured) ([docs](https://oekazuma.github.io/svelte-vitals/rules/architecture/unit-entry-file))
- **architecture/directory-naming — Directory naming** (info): A directory whose name breaks the convention its location declares stops carrying the meaning the convention gave it, and every reader — human or agent — has to open the directory to learn what it is. Fix: Rename the directory to the declared casing, or narrow the declaration that governs it. (inert until configured) ([docs](https://oekazuma.github.io/svelte-vitals/rules/architecture/directory-naming))
- **architecture/reserved-directory-names — Reserved directory names** (info): A closed set of directory names is only worth writing down if it stays closed: one directory outside it and the table stops describing the tree, so every reader has to open a directory to learn what it holds. Fix: Rename the directory to a declared name, move it under one of them, or add its name to the declaration. (inert until configured) ([docs](https://oekazuma.github.io/svelte-vitals/rules/architecture/reserved-directory-names))
- **architecture/reserved-name-placement — Reserved name placement** (info): A name reserved for one kind of place stops carrying that meaning the moment it appears somewhere else: a reader who has met one exception has to open the directory to learn what it holds. Fix: Move the directory to one of the places declared for its name, rename it, or declare this place for the name. (inert until configured) ([docs](https://oekazuma.github.io/svelte-vitals/rules/architecture/reserved-name-placement))
- **architecture/route-component-import — Route component import** (info): A route entry is written on the assumption that SvelteKit renders it: Kit hands a page its data and params, and an error page its page.error and page.status. Imported from somewhere else it receives none of that and renders against nothing, or against the importing page data standing in for its own. ([docs](https://oekazuma.github.io/svelte-vitals/rules/architecture/route-component-import))
- **architecture/doc-link-target — Documentation link target** (info): A documentation link written in a comment is invisible to type checking, module resolution and the test runner, so a convention-driven rename leaves it pointing at nothing and only human review notices. (inert until configured) ([docs](https://oekazuma.github.io/svelte-vitals/rules/architecture/doc-link-target))

### Accessibility

- **a11y/invalid-role — Invalid ARIA role** (warning): A role that does not exist in WAI-ARIA (or is abstract, reserved for the spec itself) is ignored or misread by assistive technology, silently breaking the element’s announced semantics. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/invalid-role))
- **a11y/unknown-aria-attribute — Unknown ARIA attribute** (warning): An `aria-*` name that does not exist in WAI-ARIA is not recognized by assistive technology, so the attribute is silently ignored instead of doing what the author intended. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/unknown-aria-attribute))
- **a11y/required-aria-props — Missing required ARIA props** (warning): Some WAI-ARIA roles are unusable to assistive technology without their required state/property attributes — a role="checkbox" with no way to know checked/unchecked announces a control with no discoverable state. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/required-aria-props))
- **a11y/invalid-aria-value — Invalid ARIA attribute value** (warning): An `aria-*` attribute whose value does not match its spec-defined type (e.g. a boolean given a non-`true`/`false` literal) is misread or ignored by assistive technology. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/invalid-aria-value))
- **a11y/interactive-nesting — Interactive element nested in an interactive element** (warning): A control nested inside another interactive element is announced and operated inconsistently across browsers and assistive technology, and inside an `<a href>` or a `<button>` it also violates the HTML content model, which forbids interactive descendants. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/interactive-nesting))
- **a11y/accessible-name — Interactive element has no accessible name** (warning): A button, link, or image button with no accessible name is announced by assistive technology as its bare role ("button", "link") with nothing to distinguish it from any other control on the page. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/accessible-name))
- **a11y/label-has-control — <label> has no associated control** (warning): A `<label>` with no associated control is announced by assistive technology as plain text — clicking or tapping it does not focus the field, and a screen reader gives no relationship between the label and its control. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/label-has-control))
- **a11y/use-list — Bullet text should be a list** (info): A screen reader announces a real `<ul>`/`<ol>` as a list — item count, position, and boundaries. A bullet character typed into plain text carries none of that, so the visual structure is lost on assistive technology. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/use-list))
- **a11y/placeholder-label-option — Missing placeholder label option** (warning): A required, single-selection `<select>` initially shows its first option as the chosen value — if that option is not an empty placeholder, users can submit the form without ever having made a real choice, and assistive technology announces a value as already selected. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/placeholder-label-option))
- **a11y/require-datetime — Missing datetime attribute** (info): A `<time>` element with no `datetime` attribute exposes its text content as the machine-readable value, and the HTML spec requires that text to be a valid date/time string. Text like "last Tuesday" reads fine but is not one, so the element exposes no standardized date — a consumer that wants it is left guessing at prose instead of reading a value. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/require-datetime))
- **a11y/deprecated-element — Obsolete HTML element** (info): Elements in the HTML standard's obsolete-features list (`<center>`, `<font>`, `<strike>`, …) are non-conforming: browsers keep rendering them for legacy pages, but their semantics are unspecified for assistive technology and each has a conforming replacement. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/deprecated-element))
- **a11y/deprecated-attr — Deprecated HTML attribute** (info): An attribute the HTML spec data marks deprecated (`iframe[frameborder]`, `td[width]`, `body[bgcolor]`, …) has its behavior defined by legacy browser compatibility rather than by the standard, and each has a CSS or modern-attribute replacement. Coverage is what the dataset marks deprecated or obsolete on that element, and its `deprecated` flag tracks MDN's status; attributes the dataset does not list at all — WHATWG-obsolete ones MDN never documented, like `p[align]` — are not reported. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/deprecated-attr))
- **a11y/disallowed-aria-props — ARIA attribute not allowed on this role** (warning): An `aria-*` attribute the element's role does not support is ignored by assistive technology, and one the role prohibits — a name on a `generic` `<div>` or `<span>`, on a `<p>`, on `<label>` — is worse than ignored: it is a name the author believes is exposed and is not. Judged against the ARIA 1.3 role tables, on the explicit role when there is one and otherwise on every implicit role the element can have. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/disallowed-aria-props))
- **a11y/deprecated-aria — Deprecated ARIA role or attribute** (info): ARIA 1.3 deprecates one role (`directory`), two global attributes (`aria-dropeffect`, `aria-grabbed`), and a number of attributes on particular roles — `aria-haspopup` on `checkbox`, `aria-disabled` on `generic`, and so on. Each still works today and each has been removed from the role's definition, so its meaning there is no longer guaranteed. The Svelte compiler reports the per-role cases on explicit roles as unsupported, since its ARIA data dropped them rather than flagging them; on a bare `<div>`/`<span>` it says nothing. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/deprecated-aria))
- **a11y/disallowed-element — Disallowed element** (warning): A project can decide that some elements have no place in its markup — `<iframe>` in content pages, `<font>` anywhere, a legacy custom element mid-migration — and this rule reports every occurrence of the tags it declares. It has no opinion of its own: with nothing declared it does nothing. (inert until configured) ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/disallowed-element))
- **a11y/permitted-contents — Permitted contents** (warning): An element outside its parent's permitted content — a `<div>` directly inside `<ul>`, a heading inside a `<button>` — is markup assistive technology mis-announces: list semantics break, headings lose or pollute their outline role. Judged per child against the HTML content models, membership only. Fix: Move the child to an element its parent permits (e.g. wrap list content in <li>), or change the container to one that admits it (a <div> instead of a misused <ul>, a <span> instead of a block child inside a <button>). ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/permitted-contents))
- **a11y/required-element — Required element** (warning): A project can decide that every page must carry certain elements — a `<main>` landmark, an `<h1>`, a `<nav>` — and this rule reports a route that composes without one. It has no opinion of its own: with nothing declared it does nothing. Presence is judged across the whole composed route, so an element supplied by a layout, a resolved component or `app.html` counts. (inert until configured) ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/required-element))
- **a11y/doctype — Doctype** (info): Without a doctype browsers render in quirks mode, which applies different layout and box-model rules than the standards mode a page is otherwise laid out under. Fix: Add <!doctype html> as the first line of src/app.html. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/doctype))
- **a11y/duplicate-landmark — Duplicate landmark** (warning): Assistive tech users jump between landmarks to skip repeated content; more than one main, banner, or contentinfo per page leaves them guessing which one is the real one. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/duplicate-landmark))
- **a11y/top-level-landmark — Top-level landmark** (warning): Assistive tech landmark navigation expects banner/main/complementary/contentinfo at the top level; nesting one inside another hides it from that navigation. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/top-level-landmark))
- **a11y/id-duplication — Id duplication** (warning): A duplicate id breaks label/aria-labelledby associations and in-page fragment navigation: assistive tech resolves the first match, which may not be the one the author intended. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/id-duplication))
- **a11y/no-missing-id-ref — No missing id ref** (warning): An id reference — `for`, `list`, `headers`, `form`, `popovertarget`, `commandfor`, the ARIA id-reference properties (`aria-labelledby`, `aria-describedby`, `aria-controls`, `aria-owns`, …), or a same-page `href="#…"` — pointing at an id that does not exist leaves assistive tech with a broken association or the browser with a dead in-page link. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/no-missing-id-ref))
- **a11y/unverified-id-ref — Unverified id reference** (info): Opt-in: on routes a11y/no-missing-id-ref must skip (composition not fully resolved), an id reference that matches no literal id anywhere analyzed is reported as unverifiable — a real dangling reference and an id hidden inside an unresolved component look the same, so findings need manual confirmation. ([docs](https://oekazuma.github.io/svelte-vitals/rules/a11y/unverified-id-ref))

More Accessibility skills

← All Accessibility skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY