Opening the door to flexible component rendering
One of the most powerful patterns in modern component frameworks is the ability to render different components dynamically based on runtime conditions. Dynamic components in Svelte 5: best practices explained gives us the tools to build flexible, data-driven UIs where the component itself becomes a value we can pass around, store, and swap. If you've worked with render props or component registry patterns in React or Vue, you'll recognize the pattern—but Svelte 5 makes it feel natural and performant.
Components are values in Svelte 5
The fundamental shift in Svelte 5 is understanding that components are just values. They're not special syntax constructs; they're objects that can be assigned to variables, passed as props, stored in data structures, and returned from functions. This is a significant departure from earlier versions where dynamic component rendering required the svelte:component directive.
Think of a component like you'd think of a function in JavaScript. You can reference it, pass it around, and invoke it. In Svelte 5, this becomes explicit and type-safe.
import SuccessAlert from './SuccessAlert.svelte';
import ErrorAlert from './ErrorAlert.svelte';
import WarningAlert from './WarningAlert.svelte';
let AlertComponent = SuccessAlert;
// Later, we can reassign based on logic
if (error) {
AlertComponent = ErrorAlert;
} else if (warning) {
AlertComponent = WarningAlert;
}Once you've assigned a component to a variable, you render it in your template just as you would any regular component—but the type of component being rendered is determined at runtime.
Using <Component /> with a variable
In Svelte 5, rendering a component from a variable uses the same template syntax as rendering a static component. The magic is that Svelte's compiler recognizes when a component reference is a variable and handles the dynamic dispatch automatically.
// Alert.svelte
<script>
import SuccessAlert from './SuccessAlert.svelte';
import ErrorAlert from './ErrorAlert.svelte';
import WarningAlert from './WarningAlert.svelte';
let status = 'success';
let message = 'Operation completed successfully';
$: AlertComponent = status === 'error' ? ErrorAlert
: status === 'warning' ? WarningAlert
: SuccessAlert;
</script>
<AlertComponent {message} />This approach is cleaner than the old svelte:component pattern. You pass props exactly as you would to any other component—Svelte handles passing them through to the dynamically selected component. The reactivity system ensures that when status changes, the component swaps and re-renders with the new message.
For a real-world example, consider a form builder where different field types need to render different components based on a configuration object. Your field data might look like this:
const fields = [
{ type: 'text', label: 'Name', name: 'name' },
{ type: 'email', label: 'Email', name: 'email' },
{ type: 'select', label: 'Country', name: 'country', options: [...] },
{ type: 'checkbox', label: 'Subscribe', name: 'subscribe' }
];Instead of a large conditional block in your template, you map field types to components and let Svelte's reactivity handle the rendering:
// FormBuilder.svelte
<script>
import TextField from './fields/TextField.svelte';
import EmailField from './fields/EmailField.svelte';
import SelectField from './fields/SelectField.svelte';
import CheckboxField from './fields/CheckboxField.svelte';
export let fields;
function getFieldComponent(type) {
return type === 'email' ? EmailField
: type === 'select' ? SelectField
: type === 'checkbox' ? CheckboxField
: TextField;
}
</script>
{#each fields as field (field.name)}
{@const FieldComponent = getFieldComponent(field.type)}
<FieldComponent {...field} />
{/each}Notice we're using {@const} to compute the component for each field in the loop. This keeps the template readable and the logic testable.
Map names to components
For larger applications with many possible components, maintaining a mapping object is more scalable than writing conditional logic everywhere. This pattern is especially useful when you're loading component choices from configuration, a CMS, or an API.
// componentRegistry.ts
import SuccessAlert from './SuccessAlert.svelte';
import ErrorAlert from './ErrorAlert.svelte';
import InfoAlert from './InfoAlert.svelte';
import WarningAlert from './WarningAlert.svelte';
export const alertRegistry = {
success: SuccessAlert,
error: ErrorAlert,
info: InfoAlert,
warning: WarningAlert
};
export type AlertType = keyof typeof alertRegistry;Then in your component, you simply look up the component by key:
// Alert.svelte
<script>
import { alertRegistry } from './componentRegistry';
export let type;
export let message;
$: AlertComponent = alertRegistry[type];
</script>
{#if AlertComponent}
<AlertComponent {message} />
{:else}
<p>Unknown alert type: {type}</p>
{/if}This registry pattern scales well. When a designer or product manager wants to add a new alert style, you simply add it to the registry. Your component code doesn't change. This separation of concerns makes testing and maintenance easier.
For a dashboard application we built, we had a similar pattern for widget types. Users could configure which widgets appeared on their dashboard, and we mapped widget type names to their components:
// widgetRegistry.ts
import BarChart from './widgets/BarChart.svelte';
import LineChart from './widgets/LineChart.svelte';
import KPICard from './widgets/KPICard.svelte';
import Table from './widgets/Table.svelte';
import CustomText from './widgets/CustomText.svelte';
export const widgetComponents = {
'bar-chart': BarChart,
'line-chart': LineChart,
'kpi-card': KPICard,
'data-table': Table,
'text-block': CustomText
};
export type WidgetType = keyof typeof widgetComponents;The dashboard then renders widgets from user configuration by looking them up in the registry. This makes the system extensible—new widget types can be added without touching the dashboard rendering logic.
Replaces svelte:component
In Svelte 4 and earlier, the standard way to render dynamic components was the svelte:component directive. Svelte 5 moves away from this special syntax in favor of treating components as regular values. While svelte:component still works for backward compatibility, the new approach is more idiomatic and aligns Svelte's design philosophy.
The old way looked like this:
// Old Svelte 4 pattern
<svelte:component this={AlertComponent} {message} />The new way is simpler and more explicit:
// New Svelte 5 pattern
<AlertComponent {message} />No special directive needed. If AlertComponent is a component, Svelte renders it. This reduces cognitive load—you use the same syntax whether the component is static or dynamic.
The transition also improves TypeScript support. Static component references have type information Svelte can reason about. Dynamic component references stored in variables maintain that type safety when you use TypeScript, making refactoring safer and autocomplete more effective.
Common mistakes
Forgetting to handle undefined components. When building dynamic component systems, always validate that your registry lookup returns a valid component. Add a guard check or a fallback component:
{#if AlertComponent}
<AlertComponent {...props} />
{:else}
<div class="error">Component not found: {type}</div>
{/if}Over-complicating the registry. Keep your component mappings simple and flat. Nested or conditional registries make debugging harder. If you find yourself writing complex logic to determine which component to use, move that logic to a separate function that returns a component key, then look it up in the registry.
Not using TypeScript for registry keys. When you export a registry as a const and use keyof typeof, TypeScript ensures you can only pass valid keys. This catches typos at compile time:
export const registry = { foo: Foo, bar: Bar };
export type ComponentKey = keyof typeof registry;
export let type: ComponentKey; // Type-safe
Passing incompatible props. Remember that different components may expect different props. Use TypeScript interfaces to document what props each component accepts, or create a union type of all possible prop combinations.
Key takeaways
- In Svelte 5, components are values—assign them to variables and pass them around like any other data
- Render dynamic components with standard component syntax; no special directive needed
- Build a component registry (a simple object mapping names to components) for maintainable, scalable dynamic rendering
- Always validate that your dynamic component lookup succeeded before rendering
- Use TypeScript to ensure type safety in your component mappings and prop passing
- This pattern replaces the old
svelte:componentdirective with a more idiomatic and flexible approach






Comments
Loading comments…