Svelte and SvelteKit course Β· Module 2: Components
Project Organization
In this lesson4
A well-organized project is like a well-organized Viking village - everyone knows where everything is!
Recommended Structure
src/
βββ lib/ # Shared code (#lib alias)
β βββ components/ # UI components
β β βββ ui/ # Basic (Button, Input)
β β β βββ Button.svelte
β β β βββ Input.svelte
β β βββ layout/ # Layouts (Header, Footer)
β β β βββ Header.svelte
β β β βββ Footer.svelte
β β βββ features/ # Feature components
β β βββ WarriorCard.svelte
β β βββ ShipList.svelte
β βββ state/ # Shared state (runes in .svelte.js files)
β β βββ warriors.svelte.js
β β βββ ships.svelte.js
β βββ utils/ # Helper functions
β β βββ formatters.js
β β βββ validators.js
β βββ types/ # TypeScript types
β βββ index.ts
βββ routes/ # Pages (SvelteKit)
β βββ +page.svelte
β βββ +layout.svelte
β βββ warriors/
β βββ +page.svelte
βββ app.html # HTML templateThe state/ directory holds state shared by many components. In Svelte 5 these are plain .svelte.js files where runes such as $state work. Older projects have a stores/ directory with Svelte stores here - you will meet them at the Well of Mimir (module 8).
Import Aliases
A project created with npx sv create has the #lib alias in package.json, pointing at the src/lib directory. It is a Node.js feature called subpath imports (the imports field):
{
"imports": {
"#lib": "./src/lib/index.js",
"#lib/*": "./src/lib/*"
}
}Thanks to it you do not have to count ../ in paths. Just remember the file extension at the end of the import (.svelte, .js):
<script>
// Instead of: import Button from '../../../lib/components/ui/Button.svelte'
import Button from '#lib/components/ui/Button.svelte';
// Shared state
import { army } from '#lib/state/warriors.svelte.js';
// Utils
import { formatDate } from '#lib/utils/formatters.js';
</script>In SvelteKit 2 the same alias was called $lib and worked without extensions - you will see it in older code. SvelteKit 3 no longer creates it.
Naming Conventions
Component Files
- PascalCase:
WarriorCard.svelte,ShipList.svelte - Name = what the component represents
Helper Files
- camelCase or kebab-case:
formatters.js,api-client.ts
Directories
- kebab-case:
viking-camp/,ui-components/
Example: Component with Full Structure
This component brings everything together. The parent passes it a warrior and an onselect function - the component calls it when someone picks the card. That is a callback prop: in Svelte 5 components talk to their parent through functions in props (more at Asgard, module 4). In a TypeScript project you also import .ts files with the .js extension - TypeScript finds the index.ts file on its own.
<!-- src/lib/components/features/WarriorCard.svelte -->
<script lang="ts">
// Imports
import Avatar from '#lib/components/ui/Avatar.svelte';
import { formatHealth } from '#lib/utils/formatters.js';
import type { Warrior } from '#lib/types/index.js';
// Props - data and a callback from the parent
interface Props {
warrior: Warrior;
selected?: boolean;
onselect?: (id: number) => void;
}
let { warrior, selected = false, onselect }: Props = $props();
// State
let showDetails = $state(false);
// Derived
let healthStatus = $derived(warrior.health > 50 ? 'healthy' : 'wounded');
</script>
<article
class="warrior-card"
class:selected
class:wounded={healthStatus === 'wounded'}
>
<Avatar src={warrior.avatar} alt={warrior.name} />
<h3>{warrior.name}</h3>
<p>Health: {formatHealth(warrior.health)}</p>
{#if showDetails}
<div class="details">
<p>Weapon: {warrior.weapon}</p>
<p>Rank: {warrior.rank}</p>
</div>
{/if}
<button onclick={() => onselect?.(warrior.id)}>Select</button>
<button onclick={() => showDetails = !showDetails}>
{showDetails ? 'Hide' : 'Details'}
</button>
</article>
<style>
.warrior-card {
padding: 1rem;
border: 2px solid #444;
border-radius: 8px;
transition: all 0.2s;
}
.warrior-card:hover {
border-color: #d4af37;
}
.warrior-card.selected {
background: rgba(212, 175, 55, 0.1);
border-color: #d4af37;
}
.warrior-card.wounded {
border-color: #8b0000;
}
</style>interface Props describes what the component accepts: a required warrior, an optional selected flag and an optional onselect function. onselect?.(warrior.id) calls the function only when the parent passed it. In Svelte 4 createEventDispatcher was used instead of callback props - in Svelte 5 it is deprecated.
Docs entries used in this lesson
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Where should shared UI components be located in a SvelteKit project?
Hands-on tasks in the game
- Click in order
Click the naming conventions in order: components, helper files, directories