Appearance
Component Composition in Svelte
Props-down, events-up. Slots for flexible content. Compound components for complex UIs. i wanted to document the pattern and see where we could use it better.
Table of Contents
Svelte Composition Patterns
Key Principles
Based on Svelte best practices:
- Props down, events up: Parent components pass data via props, children emit events to communicate back
- Component composition over prop soup: Break complex components into smaller composable pieces instead of adding endless props
- Slots for flexibility: Use slots when you need the consumer to control markup/layout
- $state() for internal, $props() for external (Svelte 5 only): Component owns its own state, receives props from parent
- Default values in destructuring (Svelte 5 only):
let { name = "default" } = $props()provides fallbacks
Props Best Practices
Use $props() rune (Svelte 5 only):
typescript
let { title, count = 0, onClick } = $props();Type your props:
typescript
let { title, count = 0 }: { title: string; count?: number } = $props();Avoid prop mutation unless using $bindable() (Svelte 5 only):
typescript
let { value = $bindable(0) } = $props(); // Two-way bindingComposition Strategies
Bad - Prop soup:
svelte
<Card
title="..."
subtitle="..."
body="..."
imageUrl="..."
badgeText="..."
buttonLabel="..."
onButtonClick={...} />Good - Compound components:
svelte
<Card>
<CardHeader title="..." subtitle="..." />
<CardImage src="..." />
<CardBody>{content}</CardBody>
<CardFooter>
<Button>Click me</Button>
</CardFooter>
</Card>When to refactor: If a component has 3-4+ props for layout/content, consider breaking into compound components.
Downside
The design of components sometimes combines concerns. This reduces the number of components, which is important, even at the cost of prop soup and difficulty in testing.
Why fewer components matters:
Every component adds overhead - file to navigate, imports to manage, mental model to maintain. Sometimes a fat component with 7-8 props is better than splitting into 4 smaller components.
Trade-offs i'm willing to make:
- Prop soup is fine if the component stays under ~200 lines
- Mixed concerns are fine if they're tightly related (e.g., banner + content in Banner_Hideable)
- Harder testing is fine if the component is stable and rarely changes
When NOT to split:
- Component is used in exactly one place
- The "sub-components" would never be used independently
- Splitting would create more boilerplate than it removes
- The concerns are tightly coupled (changing one always requires changing the other)
Example: Banner_Hideable
We could split into:
<Banner />- just the header<HideableContent />- just the show/hide logic<BannerWithContent />- composes the two
But why? The banner and content are always used together. The show/hide logic is specific to this pattern. We'd create 3 files instead of 1, and none of them would be reusable elsewhere.
Better to keep it as one component with a slot for customization.
Composition is a tool, not a religion.
Use it when it clarifies. Skip it when it adds complexity for no gain.
Svelte 4
This document references Svelte 5 features that aren't available until we upgrade. Here's what we CAN'T use in Svelte 4:
Unavailable in Svelte 4:
$props()rune - Useexport letinsteadsvelte<!-- Svelte 5 --> let { title, count = 0 } = $props(); <!-- Svelte 4 --> export let title; export let count = 0;$state()rune - Use plainletinsteadsvelte<!-- Svelte 5 --> let count = $state(0); <!-- Svelte 4 --> let count = 0;$derived()rune - Use$:reactive statements insteadsvelte<!-- Svelte 5 --> let doubled = $derived(count * 2); <!-- Svelte 4 --> $: doubled = count * 2;$bindable()rune - Use regular props with manual updatessvelte<!-- Svelte 5 --> let { value = $bindable() } = $props(); <!-- Svelte 4 --> export let value; // Parent uses bind:value as usualLowercase event handlers - Use
on:directive insteadsvelte<!-- Svelte 5 --> <button onclick={handleClick}> <!-- Svelte 4 --> <button on:click={handleClick}>
What DOES work in Svelte 4:
- ✅ Props-down, events-up pattern
- ✅ Slots (default and named)
- ✅ Component composition
- ✅
createEventDispatcher()for component events - ✅ Reactive statements with
$: - ✅ Stores with
$prefix
Bottom line:
All the composition PATTERNS in this document work in Svelte 4. Just ignore the rune syntax examples. Use export let for props, let for state, and $: for derived values.
The principles (slots, compound components, separation of concerns) are the same. Only the syntax differs.
Component Audit Checklist
Components that could benefit from better composition:
High Priority - Prop Soup
- [ ] Primary_Controls.svelte - Manages ~10 different controls inline; could break into
<Controls.Details />,<Controls.Recents />,<Controls.Search />compound pattern - [ ] D_Selection.svelte - Multiple tables (characteristics, relationships, properties); could use
<SelectionTable.Characteristics />sub-components - [ ] Radial_Cluster.svelte - Handles widgets, paging UI, rotation, all in one file; break into
<Cluster.Widgets />,<Cluster.Pager />,<Cluster.RotationHandle />
Medium Priority - Mode Switching
- [ ] Breadcrumbs.svelte - Mode selection embedded (ancestry vs recents); parent should choose mode and pass data
- [ ] Search_Results.svelte - Likely mixes result rendering with search state; extract
<SearchResultItem />component - [ ] Widget.svelte - Handles title, drag, reveal, all together; could compose
<Widget.Title />,<Widget.DragHandle />,<Widget.RevealToggle />
Low Priority - Already Good Composition
- [ ] Breadcrumb_Button.svelte - Good! Single responsibility, clear props, used compositionally by parent
- [ ] Button.svelte - Good! Reusable, clear interface, used throughout app
- [ ] Glow_Button.svelte - Good! Specialized variant with focused purpose
Slot Opportunities
- [ ] Banner_Hideable.svelte - Wraps content; perfect candidate for
<slot />to let parent control banner content - [ ] Card components (if any exist) - Classic slot use case for header/body/footer
- [ ] Modal/Dialog (if exists) - Header, body, footer slots
Parent-Child Communication
- [ ] Next_Previous.svelte - Review event emission pattern; ensure using Svelte 5 event syntax (Svelte 5 only)
- [ ] Segmented.svelte - Check if using
$bindable()(Svelte 5 only) for two-way binding where appropriate - [ ] Triangle_Button.svelte - Verify props-down, events-up pattern
Summary
Key takeaways:
- We're already doing composition fairly well (Breadcrumb_Button, Button usage)
- Biggest wins: break up large components (Primary_Controls, Radial_Cluster)
- Use slots more for customization (Banner_Hideable, separator overrides)
- Move mode/data selection to parents; children should receive data via props
- Embrace
$derived(Svelte 5 only) for computed values instead of manual update() functions
Next steps:
- Start with Breadcrumbs refactor (good learning exercise, medium complexity)
- Move to Primary_Controls (most impact)
- Extract compound components for complex UIs
- Add slots where consumers need customization
- Document the pattern for future components