Rules
Every check svelte-vitals runs, grouped by category.
Every rule svelte-vitals can report, grouped by category. Each rule links to a reference page describing what it checks, why it matters, and how to fix it.
The severities below are the defaults. See Configuration to change a rule’s severity or turn it off.
SEO
Resolved <head> metadata, structured data, and crawlability: what search engines actually see.
Performance
Images, render-blocking assets, imports, and load waterfalls: what makes a route slow.
Correctness
Svelte 5 runes and lifecycle misuse: code that compiles but behaves wrong.
Security
Unescaped HTML, unsafe URLs, and server state that leaks across requests.
Architecture
Component size, prop count, and the import boundaries a project declares: signals that code is placed or shaped wrong.
Accessibility
ARIA validity, landmark structure, accessible names, and semantic markup: what makes a site usable for everyone.
SEO
| Rule | Severity | Summary |
|---|---|---|
seo/title-presence |
🔴 critical | Every route should resolve a non-empty <title>. |
seo/canonical-url |
🟡 warning | Every route should include a <link rel=“canonical”> tag. |
seo/charset |
🟡 warning | The page should declare a character encoding with <meta charset>. |
seo/description-presence |
🟡 warning | Every route should include a <meta name=“description”>. |
seo/duplicate-description |
🟡 warning | Each route should have a unique meta description. |
seo/duplicate-title |
🟡 warning | Each route should have a unique <title>. |
seo/hreflang |
🟡 warning | hreflang alternates should use valid codes; x-default is recommended for selector/redirect pages. |
seo/html-lang |
🟡 warning | Your project’s app.html should set the lang attribute on <html>. |
seo/image-alt |
🟡 warning | Every <img> should have an alt attribute. |
seo/json-ld-relative-url |
🟡 warning | URLs in JSON-LD should be absolute. |
seo/json-ld-required-props |
🟡 warning | A recognized @type should include the properties its rich result requires. |
seo/json-ld-validity |
🟡 warning | A page’s JSON-LD must be valid JSON with @context and @type. |
seo/og-image |
🟡 warning | Every route should include a <meta property=“og”> tag. |
seo/og-title |
🟡 warning | Every route should include a <meta property=“og”> tag. |
seo/og-url |
🟡 warning | Every route should include an og with its canonical address. |
seo/robots-txt |
🟡 warning | Your project should provide a robots.txt file. |
seo/single-h1 |
🟡 warning | Each page should have exactly one <h1>. |
seo/sitemap-xml |
🟡 warning | Your project should provide a sitemap.xml file. |
seo/ssr-disabled |
🟡 warning | export const ssr = false makes a route’s content invisible to non-JS crawlers and slower to first paint. |
seo/viewport |
🟡 warning | Pages should declare a responsive viewport meta tag. |
seo/description-length |
🔵 info | The meta description should be 70–160 characters. |
seo/heading-level-skip |
🔵 info | Heading levels should not be skipped. |
seo/indexability |
🔵 info | A route should not be accidentally set to noindex. |
seo/json-ld |
🔵 info | Every route should include JSON-LD structured data. |
seo/json-ld-date-format |
🔵 info | Date properties in JSON-LD should be ISO-8601. |
seo/json-ld-deprecated-type |
🔵 info | Some schema types had their Google rich result dropped or restricted. |
seo/json-ld-placeholder |
🔵 info | JSON-LD should not contain unreplaced placeholder text. |
seo/og-description |
🔵 info | Every route should include an og. |
seo/sitemap-in-robots |
🔵 info | robots.txt should point crawlers at your sitemap. |
seo/title-length |
🔵 info | The document title should be 30–60 characters. |
seo/twitter-card |
🔵 info | Pages should declare a twitter for rich sharing on X/Twitter. |
Performance
| Rule | Severity | Summary |
|---|---|---|
performance/font-preload-crossorigin |
🟡 warning | A font preload must set crossorigin so the preloaded file is actually used. |
performance/image-dimensions |
🟡 warning | Every <img> should have explicit width and height attributes. |
performance/lcp-image |
🟡 warning | The first (likely LCP) image should not be lazy-loaded. |
performance/load-waterfall |
🟡 warning | Dependent sequential awaits in a universal load cost a network round trip from the browser per hop. |
performance/minify-disabled |
🟡 warning | A build.minify left in vite.config ships unminified JS/CSS to production. |
performance/preload-missing-as |
🟡 warning | Every <link rel=“preload”> should declare an as attribute. |
performance/render-blocking-script |
🟡 warning | A head <script src> should not block parsing. |
performance/heavy-import |
🔵 info | Avoid importing large, non-tree-shakeable packages. |
performance/iframe-loading |
🔵 info | An offscreen iframe eagerly loads an entire third-party document of scripts, fonts and media, usually costing more than an offscreen image; loading=“lazy” defers it. |
performance/image-loading-hint |
🔵 info | Every <img> should have an explicit loading attribute. |
performance/namespace-import |
🔵 info | Prefer named imports over import * as for tree-shaking. |
performance/preconnect |
🔵 info | Preconnect to third-party origins used by the page. |
performance/responsive-image |
🔵 info | Large images should provide a srcset. |
performance/sequential-awaits |
🔵 info | Awaits that don’t use each other’s results still run one after another; start them together. |
performance/state-raw |
🔵 info | Object/array $state that is only ever reassigned pays for deep reactivity it never uses; $state.raw skips the proxy. |
Correctness
| Rule | Severity | Summary |
|---|---|---|
correctness/orphan-effect |
🔴 critical | An $effect created outside component initialisation throws effect_orphan at runtime. |
correctness/orphan-lifecycle |
🔴 critical | onMount, getContext and friends called outside component initialisation throw lifecycle_outside_component at runtime. |
correctness/server-browser-global |
🔴 critical | window, document, localStorage accessed in module scope or a load/handler crash SSR with a ReferenceError. |
correctness/autoplay-muted |
🟡 warning | Browsers block autoplay with audio, and a blocked autoplay does not error, so a <video autoplay> without muted silently never starts playing for real visitors. |
correctness/base-path-navigation |
🟡 warning | A hardcoded root-relative link resolves against the domain root, not kit.paths.base, so under a base path it lands outside the app and 404s in production. |
correctness/checkable-bind-value |
🟡 warning | bind on a checkbox or radio input binds the DOM value property, which checkbox/radio interaction never changes, so the bound state silently never updates. |
correctness/each-index-key |
🟡 warning | Keying an {#each} block by its index gives items position-based identity, the same bug as no key, only masked. |
correctness/each-key |
🟡 warning | An {#each} block over dynamic data should have a key. |
correctness/effect-as-derived |
🟡 warning | Use $derived instead of an $effect that only assigns state. |
correctness/effect-as-onmount |
🟡 warning | An $effect that reads no reactive value belongs in an event handler, {@attach}, or onMount instead. |
correctness/instance-browser-global |
🟡 warning | A component’s instance script runs on the server during SSR, so window/document reads at its top level crash the render. |
correctness/nonreactive-builtin-state |
🟡 warning | A plain Map, Set, Date, URL, or URLSearchParams in $state is not proxied, so its mutations are invisible to reactivity and the UI silently stops updating. |
correctness/prop-mutation |
🟡 warning | Don’t mutate a prop from $props() unless it is declared $bindable. |
correctness/stale-prop-derivation |
🟡 warning | A value computed from a prop without $derived is evaluated once, so the UI silently stops tracking the parent. |
correctness/unmutated-state |
🔵 info | Use const (or $state.raw) for a $state that is never mutated. |
Security
| Rule | Severity | Summary |
|---|---|---|
security/handler-state-write |
🔴 critical | A load function or action writes to imported module state, shared across all requests on the server. |
security/javascript-url |
🟡 warning | Avoid javascript: URLs in attributes. |
security/raw-html |
🟡 warning | Sanitize the value; {@html} renders unescaped HTML. |
security/server-module-state |
🟡 warning | A module-scope variable in a Kit route or hooks file is reassigned from a function, shared across all requests on the server. |
security/shared-state-import |
🟡 warning | A Kit server/universal file imports a .svelte.ts module holding module-scope $state, one shared instance per server process. |
Architecture
| Rule | Severity | Summary |
|---|---|---|
architecture/component-size |
🔵 info | Very large components should be split up. |
architecture/directory-naming |
🔵 info | A directory should be named in the casing its location declares. |
architecture/doc-link-target |
🔵 info | A documentation link written in a comment must still point at something that exists. |
architecture/private-scope-import |
🔵 info | A unit inside a private directory should not be imported from outside it. |
architecture/prop-count |
🔵 info | Components taking many props are doing too much. |
architecture/reserved-directory-names |
🔵 info | A directory’s subdirectories should only take names declared for that position. |
architecture/reserved-name-placement |
🔵 info | A reserved directory name may appear only in the places declared for it. |
architecture/route-component-import |
🔵 info | A SvelteKit route entry is rendered by the framework, not imported by other components. |
architecture/unit-entry-file |
🔵 info | A directory declared to be a unit should contain a file named after it. |
Accessibility
| Rule | Severity | Summary |
|---|---|---|
a11y/accessible-name |
🟡 warning | A button, link, image button, or iframe needs a way to compute its accessible name. |
a11y/aria-hidden-focus |
🟡 warning | A keyboard-focusable element must not be hidden from assistive technology with aria-hidden=“true”. |
a11y/disallowed-aria-props |
🟡 warning | An aria-* attribute the element’s role does not support is ignored; one the role prohibits, such as a name on a bare div, is a name that is not exposed. |
a11y/disallowed-element |
🟡 warning | Reports every occurrence of the elements a project declares it does not want, and nothing until something is declared. |
a11y/duplicate-landmark |
🟡 warning | A route should have at most one main, banner, and contentinfo landmark. |
a11y/id-duplication |
🟡 warning | Every id in a route should be unique. |
a11y/interactive-nesting |
🟡 warning | An interactive element should not sit inside another interactive element. |
a11y/invalid-aria-value |
🟡 warning | An aria-* attribute’s value should match the type the WAI-ARIA spec defines for it. |
a11y/invalid-role |
🟡 warning | A role attribute should name a concrete WAI-ARIA role, not a typo or an abstract role. |
a11y/label-has-control |
🟡 warning | A label needs a for attribute or a wrapped control to be associated with the field it names. |
a11y/no-accesskey |
🟡 warning | The accesskey attribute assigns a page-level shortcut key whose actual combination varies by browser and OS, is undiscoverable, and conflicts with assistive-technology bindings. |
a11y/no-autofocus |
🟡 warning | autofocus moves focus on page load without the user asking, so screen reader users lose the page context they were building and keyboard users are dropped mid-page. |
a11y/no-missing-id-ref |
🟡 warning | An id reference should point to an id that exists somewhere in the composed route. |
a11y/permitted-contents |
🟡 warning | Every element must be permitted content of its parent, per the HTML content models. |
a11y/placeholder-label-option |
🟡 warning | A required, single-selection select needs an empty first option so users can’t submit it unchanged. |
a11y/positive-tabindex |
🟡 warning | A tabindex above 0 puts the element ahead of every naturally-ordered element on the page, so a single tabindex=“1” reorders keyboard navigation globally. |
a11y/required-aria-props |
🟡 warning | A role that requires state or property attributes needs them present, unless native host semantics already supply them. |
a11y/required-element |
🟡 warning | Every route must contain the elements a project declares, judged on the composed route, so a layout’s <main> counts. |
a11y/top-level-landmark |
🟡 warning | A banner, main, complementary, or contentinfo landmark should not be nested inside another landmark. |
a11y/unknown-aria-attribute |
🟡 warning | An aria-* attribute should name a real WAI-ARIA attribute, not a typo. |
a11y/abbr-title |
🔵 info | An <abbr> without a title gives readers no expansion. Visual users get no tooltip and assistive technology has nothing to expand. |
a11y/deprecated-aria |
🔵 info | A role or aria-* attribute ARIA 1.3 has deprecated, globally or on the role it sits on, still works today and is no longer defined there. |
a11y/deprecated-attr |
🔵 info | An attribute the HTML spec data marks deprecated on this element has its behavior defined by legacy compatibility, not by the standard. |
a11y/deprecated-element |
🔵 info | An element in the HTML standard’s obsolete-features list is non-conforming and has a conforming replacement. |
a11y/doctype |
🔵 info | src/app.html should open with <!doctype html>. |
a11y/no-duplicate-dt |
🔵 info | Within a single dl element there should not be more than one dt element for each name; a duplicated term usually means two descriptions were meant to share one dt. |
a11y/pattern-title |
🔵 info | When an input has a pattern, the spec says authors should include a title describing the expected format; without it a failed submit tells the user only that the value is wrong. |
a11y/require-datetime |
🔵 info | A time element’s text should be machine-readable, or a datetime attribute should supply the machine-readable value. |
a11y/unverified-id-ref |
🔵 info | Opt-in: flags id references that cannot be verified on routes whose composition is not fully resolved. |
a11y/use-list |
🔵 info | A bullet character typed into plain text should be a real list element instead. |