Svelte and SvelteKit course Β· Module 2: Components

Project Organization

4 min read Β·
In this lesson4

A well-organized project is like a well-organized Viking village - everyone knows where everything is!

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 template

The 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. 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

Useful articles