Skip to content

Svelte

Install as a dev dependency. Svelte packages are compiled at build time, so they belong in devDependencies.

Pass your Portable Text value to the PortableText component. Your component receives props via the $props() rune.

<script>
import { PortableText } from '@portabletext/svelte'
let { value } = $props()
</script>
<PortableText value={value} />

Override the default rendering by passing a components object. Use types for custom block types and marks for annotations.

<script>
import { PortableText } from '@portabletext/svelte'
import CustomImage from './CustomImage.svelte'
import ExternalLink from './ExternalLink.svelte'
let { value } = $props()
</script>
<PortableText
{value}
components={{
types: {
image: CustomImage,
},
marks: {
link: ExternalLink,
},
}}
/>

Mark components receive a portableText prop typed with MarkComponentProps, and a children Snippet for the annotated text content.

Use $derived() to destructure from portableText. This keeps the variable reactive when props change. See Svelte 5 patterns below.

ExternalLink.svelte
<script lang="ts">
import type { MarkComponentProps } from '@portabletext/svelte'
import type { Snippet } from 'svelte'
interface Props {
portableText: MarkComponentProps<{
href?: string
blank?: boolean
}>
children: Snippet
}
let { portableText, children }: Props = $props()
let { value } = $derived(portableText)
</script>
{#if value.href}
<a href={value.href} target={value.blank ? '_blank' : undefined} rel="noopener">
{@render children()}
</a>
{:else}
{@render children()}
{/if}

Block type components receive a portableText prop typed with CustomBlockComponentProps. Use $derived() to destructure value.

CustomImage.svelte
<script lang="ts">
import type { CustomBlockComponentProps } from '@portabletext/svelte'
interface Props {
portableText: CustomBlockComponentProps<{
asset: { url: string }
alt?: string
caption?: string
}>
}
let { portableText }: Props = $props()
let { value } = $derived(portableText)
</script>
<figure>
<img src={value.asset.url} alt={value.alt ?? ''} />
{#if value.caption}
<figcaption>{value.caption}</figcaption>
{/if}
</figure>

@portabletext/svelte is built on Svelte 5 primitives. These patterns appear in every custom component.

PatternUsage
$props()All components receive props via the $props() rune
$derived()Use $derived() to keep variables reactive when props change
children: SnippetMark and block components receive children as a Svelte 5 Snippet
{@render children()}Renders child content (replaces Svelte 4’s <slot />)

Always use $derived() when destructuring from portableText. Destructuring directly breaks reactivity and leaves the variable stale after the first render.

<!-- ✅ Reactive: updates when props change -->
let { value } = $derived(portableText)
<!-- ❌ Not reactive: stale after first render -->
let { value } = portableText

This is the most common mistake when writing custom components. If your component renders correctly on first load but does not update when content changes, check your destructuring.

Use the context prop to share data with all components in the tree. This is useful for things like footnote numbering, theme tokens, or any value that multiple components need to read.

<PortableText
{value}
components={{ marks: { footnote: Footnote } }}
context={{ footnotes }}
/>

Inside Footnote.svelte, read context from portableText.global.context. Use $derived() here too.

let { footnotes } = $derived(portableText.global.context)
let number = $derived(
footnotes.findIndex(note => note._key === portableText.value._key) + 1
)

Use toPlainText() to extract a plain text string from a Portable Text value. Useful for meta descriptions, search indexes, and other contexts where HTML is not appropriate.

<script>
import { toPlainText } from '@portabletext/svelte'
</script>
<svelte:head>
<meta name="description" content={toPlainText(value)} />
</svelte:head>

For the complete API reference, all component prop types, and advanced usage, see the @portabletext/svelte README on GitHub.