Communitygithub.com

web-prototype

Use this skill whenever the user wants to build, scaffold, or prototype a web app or website. Triggers on: "build a web app", "create a prototype", "scaffold a SvelteKit project", "make a webapp", "I need a prototype for user testing", "build a frontend", "create a web interface", "set up SvelteKit", "make a clickable prototype", "build a web interface", or any request to create an interactive web-based UI. This skill produces a user-testing-ready SvelteKit SPA using Designsystemet (digdir) components, TypeScript 6+, and Nunito Sans. Always use this skill when the user mentions prototyping, user testing, SvelteKit, web app scaffolding, or wants a deployable static site.

What is web-prototype?

web-prototype is a Claude Code agent skill that use this skill whenever the user wants to build, scaffold, or prototype a web app or website. Triggers on: "build a web app", "create a prototype", "scaffold a SvelteKit project", "make a webapp", "I need a prototype for user testing", "build a frontend", "create a web interface", "set up SvelteKit", "make a clickable prototype", "build a web interface", or any request to create an interactive web-based UI. This skill produces a user-testing-ready SvelteKit SPA using Designsystemet (digdir) components, TypeScript 6+, and Nunito Sans. Always use this skill when the user mentions prototyping, user testing, SvelteKit, web app scaffolding, or wants a deployable static site.

Works with✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/havardd/claude-code-skills/tree/main/plugins/web-prototype/skills/web-prototype

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Web Prototype Skill

Scaffold and build clean, user-testing-ready web app prototypes using SvelteKit as a SPA (no SSR), TypeScript 6+, and the Norwegian Designsystemet component library.


Technology Stack

LayerChoiceVersion
FrameworkSvelteKit2.x (latest)
LanguageTypeScript6.x (latest)
UI Components@digdir/designsystemet-web1.x (latest)
CSS/Tokens@digdir/designsystemet-css1.x (latest)
FontNunito Sans (Google Fonts)—
BuildVitebundled with SvelteKit
Adapter (local)@sveltejs/adapter-autobundled
Adapter (deploy)@sveltejs/adapter-staticseparate install

Package discipline: Only install packages if strictly necessary. Prefer the packages above. Never install newly-released or obscure packages — require at minimum several months of community usage and active maintenance history.


Project Structure

my-app/
├── src/
│   ├── app.html               # HTML shell — load Google Fonts here
│   ├── app.css                # Global styles, Designsystemet CSS import
│   ├── routes/
│   │   ├── +layout.ts         # export const ssr = false; export const prerender = true (if static)
│   │   ├── +layout.svelte     # App shell, nav, global imports
│   │   └── +page.svelte       # Home page
│   └── lib/                   # Shared components and utilities
├── static/                    # Static assets
├── vite.config.ts             # SvelteKit config lives here (adapter, compilerOptions); sv no longer writes svelte.config.js
├── tsconfig.json
└── package.json

Scaffolding Steps

1. Generate the project

Empty directory (a standalone prototype):

npx sv create my-app
# Choose: SvelteKit minimal, TypeScript, no add-ons
cd my-app
npm install

Existing project directory — the normal case after /faseflyt:nytt-prosjekt, where kunnskap/, CLAUDE.md, .claude/ and .gitignore already exist:

npx sv@latest create --template minimal --types ts --no-add-ons --no-dir-check --install npm .

--no-dir-check skips the "Directory not empty. Continue?" prompt, which is what makes the command runnable from Claude Code. It also means the overwrite below happens silently. sv create overwrites every file it also generates, and it does not merge. In a faseflyt project the collision is .gitignore: the project's own rules (comments, dist/, .claude/settings.local.json, data folders) are replaced by the SvelteKit template. Therefore: before sv create, read .gitignore and keep its contents. After, read the new file and append every line from the old one that is missing, under # Project rules (restored after sv create). Show the user before/after. The SvelteKit entries stay (the vite.config.*.timestamp-* lines are new and wanted); the project's rules are added back, never swapped in. Observed 2026-09-04 with exactly this command in a faseflyt project: the committed .gitignore lost its comments and its dist/ line, and was restored by hand. Confirmed in the sv 0.17.0 source: the template .gitignore is copied straight over the existing file, and even with the prompt on, a directory holding only .git and .gitignore counts as empty.

2. Install Designsystemet packages

npm install @digdir/designsystemet-web @digdir/designsystemet-css @digdir/designsystemet-types

3. Disable SSR (SPA mode)

Create src/routes/+layout.ts:

// src/routes/+layout.ts
export const ssr = false;

4. Configure adapter

sv create (0.17.0, measured 2026-09-04) no longer writes svelte.config.js. The SvelteKit config is passed to sveltekit() in vite.config.ts, generated like this:

// vite.config.ts — as generated by sv
import adapter from '@sveltejs/adapter-auto';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    sveltekit({
      compilerOptions: {
        runes: ({ filename }) =>
          filename.split(/[/\\]/).includes('node_modules') ? undefined : true
      },
      adapter: adapter()
    })
  ]
});

Leave it as is for local development. For a static deploy, install @sveltejs/adapter-static and change only the import and the adapter line in this same file:

import adapter from '@sveltejs/adapter-static';
// ...
adapter: adapter({ fallback: 'index.html' })

Older projects: if a svelte.config.js exists (created with an earlier sv), the adapter lives there. Edit that file; do not create a second config.

5. Set up fonts in src/app.html

Only Google Fonts is allowed as external CDN. Load Nunito Sans here:

<!doctype html>
<html lang="nb">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <!-- Google Fonts: Nunito Sans only -->
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,300..900;1,6..12,300..900&display=swap"
      rel="stylesheet"
    />
    %sveltekit.head%
  </head>
  <body data-sveltekit-preload-data="hover">
    <div style="display: contents">%sveltekit.body%</div>
  </body>
</html>

6. Global CSS (src/app.css)

/* Set Nunito Sans as the global font */
:root {
  --ds-font-family: 'Nunito Sans', system-ui, sans-serif;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  font-family: var(--ds-font-family);
  margin: 0;
  padding: 0;
}

7. Layout (src/routes/+layout.svelte)

<script lang="ts">
  // Import Designsystemet web components (registers custom elements)
  import '@digdir/designsystemet-web';
  // Import Designsystemet CSS (tokens and base styles) and make it hot reload
  import "@digdir/designsystemet-css";
  import "@digdir/designsystemet-css/theme"; // default theme; step 9 swaps this for a generated one
  import '../app.css';
  // sv ships the favicon as src/lib/assets/favicon.svg and links it from the
  // layout, not from app.html — keep that, or the tab shows a broken icon.
  import favicon from '$lib/assets/favicon.svg';

  const { children } = $props();
</script>

<svelte:head>
  <link rel="icon" href={favicon} />
</svelte:head>

{@render children()}

8. TypeScript config

Ensure tsconfig.json targets modern TypeScript 6 features:

{
  "extends": "./.svelte-kit/tsconfig.json",
  "compilerOptions": {
    "strict": true,
    "moduleResolution": "bundler",
    "target": "ES2022",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "types": ["@digdir/designsystemet-web"]
  }
}

9. Brand colors (only when the default Designsystemet look is not acceptable)

The default theme is fine for most prototypes. When the prototype must carry the organisation's own colors, do not override --ds-color-* variables: each color is a scale of 16 hardcoded values with no base-color variable, so a single override changes one shade and leaves the other fifteen (hover, active, borders, text) untouched (measured 2026-08-30). Generate a theme with Designsystemet's own tooling instead:

  1. Add designsystemet.config.json in the project root. The CLI flags for colors are deprecated; the config file is the supported way:

    {
      "$schema": "./node_modules/@digdir/designsystemet/dist/config.schema.json",
      "outDir": "./design-tokens",
      "clean": true,
      "themes": {
        "theme": {
          "colors": {
            "accent": "#1E98F5",   // the brand color; scale is generated from it
            "neutral": "#1E2B3C"
          },
          "borderRadius": 4
        }
      }
    }
    

    Ask the user for the hex values; never invent brand colors. Additional keys under colors become additional data-color values. The theme builder at https://theme.designsystemet.no/ produces a valid config if the user prefers a visual tool.

  2. Generate tokens and build the CSS (both are idempotent; clean keeps the output directories tidy):

    npx @digdir/designsystemet tokens create
    npx @digdir/designsystemet tokens build
    

    Output: design-tokens-build/theme.css (plus .d.ts files). Measured 2026-09-05 with @digdir/designsystemet 1.21.0.

  3. In src/routes/+layout.svelte, replace the default theme import with the built one. Keep the base CSS import as is:

    import "@digdir/designsystemet-css";
    // import "@digdir/designsystemet-css/theme";   <- default theme, replaced by:
    import '../../design-tokens-build/theme.css';
    
  4. Commit designsystemet.config.json, design-tokens/ and design-tokens-build/. The generated files are build artifacts: never edit them by hand, re-run both commands after upgrading @digdir/* packages.


CSS Discipline — Non-Negotiables

Before writing ANY HTML element or CSS rule: look up the DS reference files. If a DS class or component exists for it, you MUST use it — no exceptions.

Mandatory DS class lookup

ALWAYS see - references/components.md — DS classes for usage of semantic elements, custom html elements, and common patterns

Web components vs CSS-only — exhaustive whitelist

@digdir/designsystemet-web registers exactly these custom elements:

Custom elementAlso needs CSS class?
<ds-breadcrumbs>yes — class="ds-breadcrumbs"
<ds-error-summary>yes — class="ds-error-summary"
<ds-field>yes — class="ds-field"
<ds-pagination>yes — class="ds-pagination"
<ds-suggestion>no
<ds-tabs>, <ds-tablist>, <ds-tab>, <ds-tabpanel><ds-tabs> needs class="ds-tabs"

Everything else is CSS-only. If the component name is not in the table above, you MUST use a standard HTML element with the DS class — never invent a <ds-X> custom element for it. Examples:

<!-- CORRECT -->
<div class="ds-alert" data-color="info" role="alert">...</div>
<button class="ds-button">...</button>
<div class="ds-card">...</div>

<!-- WRONG — these custom elements do not exist -->
<ds-alert class="ds-alert">...</ds-alert>
<ds-button class="ds-button">...</ds-button>
<ds-card class="ds-card">...</ds-card>

What you must NEVER do

  • No naked semantic elements — never write <h2>, <p>, <label>, <a>, <button>, or a card <div> without the corresponding DS class from the table above.
  • No custom color variables — never define --color-* or --brand-* in :root, and never override --ds-color-* either. Use DS tokens; brand colors go through step 9 (a generated theme), not through CSS.
  • No hardcoded hex/rgb colors — no #3b82f6, rgba(0,0,0,0.5), etc. Use DS color tokens instead.
  • No font-family declarations in component styles — it's already inherited from :root via --ds-font-family. Every repeated declaration is noise.
  • No custom font-size for text — use data-size on .ds-heading / .ds-paragraph instead. data-size is not universal: it works on .ds-heading, .ds-paragraph, .ds-avatar and .ds-spinner, and on a container to set the inherited size mode. Anywhere else it does nothing.
  • No custom button, card, alert, tag, or tab styling — use the DS components.
  • Never guess an attribute name — Designsystemet fails silently. An attribute the class does not know is ignored, the element renders unstyled, and svelte-check reports nothing. Two measured cases: data-size outside the four classes above, and data-variant="primary" on .ds-button, where primary is the base style and has no attribute at all. Check references/components.md first — it documents the attributes per class — and the class's own CSS in node_modules/@digdir/designsystemet-css/dist/src when the reference does not answer or seems to contradict what you observe.

Custom <style> blocks are for layout only

The only CSS you should write in a component's <style> block:

  • display: flex / grid and related properties
  • max-width, width, height
  • padding, margin — prefer var(--ds-size-N) tokens over hardcoded px values
  • position, z-index for sticky or overlapping elements
  • overflow, text-decoration: none (for link-as-card patterns)

If you're about to write background:, color:, border-color:, font-size:, or font-family: in a custom style — stop and use a DS class or token instead.


Svelte 5 runes in TypeScript

Use Svelte 5 runes syntax — the current standard:

// Reactive state
let count = $state(0);
let doubled = $derived(count * 2);

// Effects
$effect(() => {
  console.log('count changed:', count);
});

Prototype Design Principles

These principles keep prototypes lean and user-testing-ready:

  1. DS-first, always — reach for a DS class or designsystemet attribute before writing any CSS. If a DS component exists for what you need, use it — no exceptions. Custom styles are a last resort, only for layout.
  2. Real interactions — wire up buttons, forms, and navigation so the tester can actually click through
  3. Fake data is fine — hardcode realistic sample data; avoid real APIs unless required
  4. Accessible by default — Designsystemet components are WCAG-compliant; use semantic HTML
  5. One route per screen — use SvelteKit file-based routing to separate screens naturally
  6. Shared state — use shared reactive state in .svelte.ts files with $state for cross-page state
  7. Norwegian language — all UI text, labels, headings, and placeholder content must be written in Norwegian Bokmål (nb). Never use English as the display language.
  8. Not for production — every prototype must display a clearly visible disclaimer, e.g. a banner at the top of the page: "Dette er en prototype laget av Claude og kan ikke brukes i produksjon." Use <div class="ds-alert" data-color="info" role="alert"> for this (ds-alert is CSS-only — never <ds-alert>).
  9. Responsive out of the box — Designsystemet components and typography scale to narrow screens by themselves. A «mobil/finpuss» phase means the user opens the preview in a narrow window and looks; it does not mean writing media queries or installing test tooling. Never install browsers or screenshot tools (playwright, puppeteer, chromium) to check layout yourself — verification is the user's, and on a managed PC a browser download to the user profile is exactly what endpoint security flags.

Routing Example (Multi-screen Prototype)

src/routes/
├── +layout.ts          # ssr = false
├── +layout.svelte      # nav + and shared layout
├── +page.svelte        # home screen "/"
├── step-1/
│   └── +page.svelte    # "/step-1"
├── step-2/
│   └── +page.svelte    # "/step-2"
└── confirmation/
    └── +page.svelte    # "/confirmation"

Navigation between screens:

Prefer a link — navigation is a link, not a button:

<a href="/step-2" class="ds-button">Next</a>

Use .ds-link instead of .ds-button when it should read as text rather than as an action.

If the navigation must be programmatic, use SvelteKit's goto:

<script lang="ts">
  import { goto } from '$app/navigation';
</script>

<button class="ds-button" type="button" onclick={() => goto('/step-2')}>Next</button>

Finishing Up

After scaffolding, always:

  1. Run npm run dev to confirm the dev server starts
  2. Run npm audit and report results
  3. Tell the user how to run it locally: npm run dev
  4. Run the security review checklist below against the actual codebase
  5. Summarize the security review findings
  6. Offer to add more screens, tweak components, or add state management

Security Review Checklist

Run this checklist after scaffolding and report results to the user.

What to check

  • No external scripts or stylesheets except Google Fonts (fonts.googleapis.com, fonts.gstatic.com)
  • All npm packages are well-established (check npm info <pkg> — look for age, weekly downloads, maintainer reputation)
  • No eval() or innerHTML with user-controlled input in any .svelte or .ts file
  • No hardcoded secrets (API keys, passwords) — prototypes should use .env files with PUBLIC_ prefix for anything non-secret
  • CSP-friendly — no inline scripts outside of Svelte-compiled output
  • No dangerouslySetInnerHTML equivalent — Svelte's {@html} should only be used with trusted, sanitized content
  • Dependencies have no known CVEs — run npm audit and fix or explain any findings
  • Supply-chain hygiene — avoid packages < 3 months old, prefer packages with >10k weekly downloads

How to run

npm audit
# If issues found:
npm audit fix

Read the codebase and check for security issues. After review, tell the user:

✅ Security review complete. This prototype [passes / has the following notes]: ... It is safe to run locally / share with user testers.

Related Skills