Component syntax: markup, styles and logic in one file¶
Exercise component-syntax-annex.pdf · PDF
Pick any button on any site you use. To change it completely (its text, its colour and what it does) how many files do you open?
In the code you read in Unit 2, the answer was three: an HTML file, a CSS file, and somewhere a JavaScript file. Three places for one button. If something on screen is one thing for the user, it should be one thing for you. In this page the answer becomes one.
What you will learn
- Name the three parts of a
.sveltefile and say what goes in each - Put JavaScript values in the markup with curly braces
- Show or hide markup with
{#if}and repeat it with a keyed{#each} - Explain what scoped styles are, and use tokens instead of raw colours
- Write a component that receives props with
$props(), with default values - The trap:
{#if}does not hide an element, it removes it
The brick¶
Think of a LEGO brick. It is complete: shape, colour and the way it clicks. You can use it a hundred times. You never open it to use it. And the studs on top are how it connects to other bricks.
A component is a reusable piece of interface that keeps its own markup, its own style and its own logic in one file. In Svelte that file ends in
.svelte.
The studs of a component are called props: the values it accepts from outside. We keep this brick all page.
A .svelte file has three parts:
| Part | What goes in it | Language |
|---|---|---|
<script> |
The logic: data, imports, functions | JavaScript |
| markup | The structure: what is on screen. Everything outside the two tags | HTML plus {braces} and {#blocks} |
<style> |
The look. It only affects this component | CSS |
All three are optional. The home page you wrote in Development environment has only markup, and it is a valid component.
Tokens and the frame¶
You met design tokens in Unit 2. In code they become CSS custom properties: named values that start with -- and that you read with var(--name). Create this file:
:root {
--nb-ink: #111111;
--nb-paper: #ffffff;
--nb-muted: #666666;
--nb-surface: #f1f5f9;
--nb-line: #e5e7eb;
--nb-link: #2563eb;
--nb-danger: #dc2626;
--nb-radius: 6px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Inter, system-ui, sans-serif;
font-size: 15px;
color: var(--nb-ink);
background: var(--nb-paper);
}
+layout.svelte wraps every page, so import the CSS there and give the app its phone-width column: 390 wide, padding 24 at the top and 16 at the sides. Keep the lines the scaffold wrote and add the import, the <main> and the <style>:
<script>
import '../app.css';
import favicon from '#lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
<main>
{@render children()}
</main>
<style>
main {
max-width: 390px;
margin: 0 auto;
padding: 24px 16px;
}
</style>
Save and the font changes at once. This file has all three parts. {@render children()} means "the page goes here"; you will use it properly in Routing and navigation.
Careful: “I'll type the hex colour, it's faster”
Fourteen screens later the client wants a darker grey. With a token you change one line. With hex values you search the whole project and miss two.
Curly braces¶
Empty your home page and start with the data: four notes, as an array of objects. If you think in Java, think List<Note>.
<script>
const notes = [
{ id: '1', title: 'Shopping list', tag: 'home', pinned: true },
{ id: '2', title: 'Maths homework', tag: 'school', pinned: false },
{ id: '3', title: 'Passwords', tag: 'none', pinned: false },
{ id: '4', title: 'Ideas for the app', tag: 'school', pinned: false }
];
</script>
<h1>NoteBox</h1>
<p class="hint">{notes.length} notes</p>
What is inside the braces? Any JavaScript expression: a piece of code that produces a value. Try these three: {notes.length * 2}, {notes[0].title}, {notes[0].title.toUpperCase()}.
Braces also work in attributes: <p title={notes[0].tag}>. Whenever you see them, read: "JavaScript, give me a value here".
Repeat with each¶
Writing four <li> by hand is what generated code does. Say it once instead:
Read it as for (Note note : notes). Add a fifth object to the array and a fifth row appears.
The (note.id) is the key: the label that lets Svelte tell the items apart when the list changes. What makes a good key? Something unique that never changes, such as an id. Not the position. Without a key, Svelte matches items by position, and rows get mixed up when the list changes.
Careful: this is not a mail merge
The braces are not replaced once and forgotten. Svelte compiles this into JavaScript that builds the list. That is why, in the next page, the list will update by itself when the data changes.
Show or remove with if¶
A list screen always has two faces: with items, and without. An empty white screen breaks visibility of system status from usability heuristics: is it loading, broken, or empty? So say which:
{#if notes.length === 0}
<p>No notes yet.</p>
{:else}
<ul>
{#each notes as note (note.id)}
<li>{note.title} · {note.tag}</li>
{/each}
</ul>
{/if}
Empty the array (const notes = [];) to see the message, then open DevTools in your browser (F12) and go to Elements. Is the <ul> there, hidden? No. {#if} does not hide anything: when the condition is false, the element does not exist.
Scoped styles¶
Add a <style> at the end of the page. The measurements come from the design: h1 of 28 px; a hint of 13 px in the muted colour; a list with no bullets and a top border.
<style>
h1 {
margin: 0;
font-size: 28px;
}
.hint {
margin: 8px 0 16px;
font-size: 13px;
color: var(--nb-muted);
}
ul {
margin: 0;
padding: 0;
list-style: none;
border-top: 1px solid var(--nb-line);
}
</style>
You wrote h1 { ... }. Does that affect every h1 of the app, or only this one? Inspect the <h1> in DevTools. It has an extra class like svelte-1a2b3c, and the rule has become h1.svelte-1a2b3c. Svelte added both.
Scoped styles: the CSS in a component only applies to that component. It cannot leak out, and nobody else's can leak in.
Now add .ghost { color: red; } and save. The terminal and the editor warn you: Unused CSS selector ".ghost". The compiler knows your markup, so it knows your dead CSS. Delete it.
Careful: scoped does not mean no global styles
Global things go where they belong: app.css (tokens, body). Everything else stays inside its component.
Your first brick: NoteRow¶
A row is about to get more complicated: a tag on the right, a different background when pinned. And other screens will need it. Time to make it a brick. Its design says: height 44, padding 0 8, title 15 Medium, tag 13 muted, bottom border, surface background when pinned.
<script>
let { title, tag = 'none', pinned = false } = $props();
</script>
<li class={['row', pinned && 'pinned']}>
<span class="title">{title}</span>
{#if tag !== 'none'}
<span class="tag">{tag}</span>
{/if}
</li>
<style>
.row {
display: flex;
justify-content: space-between;
align-items: center;
height: 44px;
padding: 0 8px;
border-bottom: 1px solid var(--nb-line);
}
.pinned {
background: var(--nb-surface);
}
.title {
font-weight: 500;
}
.tag {
font-size: 13px;
color: var(--nb-muted);
}
</style>
Three new things:
$props()is a rune: a word starting with$that is an instruction to the compiler. It gives the component its studs. Runes need no import, and they only work in.svelteand.svelte.jsfiles.tag = 'none'is a default value, used when nobody passes that prop.class={[...]}takes a list.pinned && 'pinned'adds the class only whenpinnedis true. (You may find the older formclass:pinned={pinned}in tutorials. It still works.)
The display: flex line puts title and tag at the two ends. Copy it for now: Layout with CSS explains it.
Here is the whole page, using the brick:
<script>
import NoteRow from '#lib/NoteRow.svelte';
const notes = [
{ id: '1', title: 'Shopping list', tag: 'home', pinned: true },
{ id: '2', title: 'Maths homework', tag: 'school', pinned: false },
{ id: '3', title: 'Passwords', tag: 'none', pinned: false },
{ id: '4', title: 'Ideas for the app', tag: 'school', pinned: false }
];
</script>
<h1>NoteBox</h1>
<p class="hint">{notes.length} notes</p>
{#if notes.length === 0}
<p>No notes yet.</p>
{:else}
<ul>
{#each notes as note (note.id)}
<NoteRow title={note.title} tag={note.tag} pinned={note.pinned} />
{/each}
</ul>
{/if}
<style>
h1 {
margin: 0;
font-size: 28px;
}
.hint {
margin: 8px 0 16px;
font-size: 13px;
color: var(--nb-muted);
}
ul {
margin: 0;
padding: 0;
list-style: none;
border-top: 1px solid var(--nb-line);
}
</style>
#lib is the short name of src/lib. A tag with a capital letter is a component; lower case is HTML. Remove tag={note.tag} and the row still works, with the default.
Commit: git add -A, then git commit -m "Add NoteRow component".
flowchart TB
P["<b>+page.svelte</b><br>owns the notes"] -->|"title, tag, pinned<br>(the studs)"| R1["<b>NoteRow</b><br>Shopping list"]
P -->|"title, tag, pinned"| R2["<b>NoteRow</b><br>Maths homework"]
P -->|"title, tag, pinned"| R3["<b>NoteRow</b><br>Passwords"]
Careful: props flow down
A prop is not a way to update the parent. Values go down, like the studs on top of a brick. To send something up you need events: Events and actions.
The syntax so far¶
| You write | It means |
|---|---|
{expression} |
Put the value of this JavaScript expression here (text or attribute) |
{#if a} ... {:else} ... {/if} |
Create this markup only when a is true |
{#each list as item (item.id)} |
Repeat the markup for every item; item.id is the key |
let { a, b = 1 } = $props(); |
This component accepts the props a and b; b defaults to 1 |
<NoteRow title="Hi" pinned /> |
Use a component. pinned alone means pinned={true} |
class={['row', x && 'on']} |
Class row always; class on only when x is true |
var(--nb-muted) |
Use a token defined in app.css |
Your turn: body and badge¶
The handout at the top of this page asks you to grow NoteRow: a second line with the note's body, a "PINNED" badge, and a page hint that also counts the pinned notes. If you get stuck:
| You see | Why |
|---|---|
You pass body, nothing shows, and there is no error |
You are not receiving it in $props(). The brick does not have that stud |
| A gap on the note with no body | {#if body} is missing, so an empty <p> is created |
The badge is hidden with display: none |
Use {#if pinned}, so it does not exist when it is not needed |
| Hex colours in your CSS | Use var(--nb-...) |
| "1 pinned" is typed by hand | Count with an expression on the array |
Then do the same for your own app: put your tokens in src/app.css, import them in the layout, and build your main list or card as a component with at least three props. The Svelte tutorial, part Basic Svelte, covers this ground in its Introduction, Logic and Props sections.
Key takeaways¶
- A component is one file with up to three parts:
<script>(logic), markup (structure) and<style>(look). - Curly braces mean "JavaScript, give me a value here", in text and in attributes.
{#if}creates or removes markup; it does not hide it.{#each}repeats markup, and its key must be unique and stable.- Scoped styles stay inside their component. Global things go in
app.css, and tokens replace raw colours. - Props are a component's studs, declared with the rune
$props(). They can have default values, and they flow down only.
Quick check¶
1. In which part of a .svelte file do you import another component?
- In the
<script> - In the markup
- In the
<style> - In
src/app.html
The <script> holds the logic: data, imports and functions. The markup is the structure on screen, and the <style> is the look.
2. What can you put between curly braces in the markup?
- Any JavaScript expression that produces a value
- Only the name of a variable from the script
- Only a CSS class from the style section
- Only a string written between two quotes
Curly braces accept any expression: {notes.length * 2}, {note.title.toUpperCase()} and so on, in text and in attributes.
3. With {#if notes.length > 0}, the array is empty. What does DevTools show for the markup inside the block?
- Nothing, because the element does not exist in the page
- The element, with
display: noneadded to its styles - The element, with the
hiddenattribute added to it - The element, with its text removed but its tags kept
{#if} does not hide. When the condition is false, Svelte does not create the element at all.
4. Which value makes the best key for an {#each} over a list of notes?
- An id that is unique and never changes
- The position of the note in the array
- The title, which the user is able to edit
- A random number generated at every draw
The key lets Svelte tell items apart when the list changes, so it must be unique and stable. A position changes when items move, and a title can be edited or repeated.
5. You write p { color: red; } in the <style> of NoteRow. Which paragraphs turn red?
- Only the paragraphs inside
NoteRow - Every paragraph in the whole app
- Every paragraph on the page using
NoteRow - None, because tag selectors are not allowed
Styles in a component are scoped: Svelte adds a class like svelte-1a2b3c to the elements and to the rule, so the rule cannot leak out.
6. A component starts with let { title, tag = 'none' } = $props();. A page uses it as <NoteRow title='Hi' />. What is the value of tag?
-
'none', because that is its default value -
undefined, because the page did not pass it -
'Hi', because it takes the value of the first prop - It does not compile, because every prop must be passed
tag = 'none' is a default value, exactly like a default parameter. It is used whenever the parent does not pass that prop.
7. A bus app shows one card per bus line, each with a number, a destination and minutes to arrival. Cards for night buses have a dark background. What is the best way to build them?
- One
BusCardcomponent with props such asline,destination,minutesandnight = false - One separate component for each bus line, with its own values written inside
- One
BusCardcomponent for day buses and a copy of it for night buses - One long page with a hand-written block of markup for each of the bus lines
A component is a brick you use many times: the values that change between cards become props, and the rare variation becomes a prop with a default value. An {#each} then draws one card per line.
Your list looks right, but it is dead: you cannot search it or add to it. Next you give it a pulse: Reactivity.