FIG-DOC-001 · GET STARTED
// docs / docs

scoped props.

Svelte Scoped Props is an experimental preprocessor that lets a parent component intentionally pass one of its scoped CSS classes through a child component prop.

FIG-DOC-001
SHEET 01 / 01

Svelte Scoped Props adds an explicit scoped:<prop> directive for component props. It is for the narrow case where the parent owns a scoped CSS selector, but the element that needs the class lives inside a child component.

<ChildCard scoped:class="parent-owned" />
<MotionBox scoped:class={['panel', { active }]} />
<FancyCard scoped:internalClass="inner-panel" />
<ChildCard scoped:class="parent-owned" />
<MotionBox scoped:class={['panel', { active }]} />
<FancyCard scoped:internalClass="inner-panel" />

Before Svelte compiles the component, the preprocessor rewrites the directive into a normal prop value with the parent component’s CSS hash attached.

<ChildCard class="parent-owned svelte-abc123" />
<ChildCard class="parent-owned svelte-abc123" />

The child still controls what happens next. If it applies class to an internal element, that internal element receives the parent-scoped class. If it treats class as data, Svelte Scoped Props does not force it to become CSS.

Install

pnpm add -D @humanspeak/svelte-scoped-props
pnpm add -D @humanspeak/svelte-scoped-props

Configure Svelte

Add the preprocessor after your style preprocessors so it sees the same CSS Svelte will hash.

import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
import { scopedProps } from '@humanspeak/svelte-scoped-props'

export default {
    preprocess: [
        vitePreprocess(),
        scopedProps({
            runtimeModule: '@humanspeak/svelte-scoped-props/runtime'
        })
    ]
}
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
import { scopedProps } from '@humanspeak/svelte-scoped-props'

export default {
    preprocess: [
        vitePreprocess(),
        scopedProps({
            runtimeModule: '@humanspeak/svelte-scoped-props/runtime'
        })
    ]
}

The runtimeModule option points dynamic ClassValue props at the published scoped package runtime. Literal props do not import the runtime helper.

First scoped prop

<script lang="ts">
    import ChildCard from './ChildCard.svelte'
</script>

<ChildCard scoped:class="parent-owned" />

<style>
    .parent-owned {
        color: purple;
    }
</style>
<script lang="ts">
    import ChildCard from './ChildCard.svelte'
</script>

<ChildCard scoped:class="parent-owned" />

<style>
    .parent-owned {
        color: purple;
    }
</style>
<script lang="ts">
    import type { ClassValue } from 'svelte/elements'

    interface Props {
        class?: ClassValue
    }

    let props: Props = $props()
</script>

<article class={props.class}>
    Child content
</article>
<script lang="ts">
    import type { ClassValue } from 'svelte/elements'

    interface Props {
        class?: ClassValue
    }

    let props: Props = $props()
</script>

<article class={props.class}>
    Child content
</article>

The class is authored in Parent.svelte, but the child can decide where it lands. The generated prop contains the parent scope hash, so the parent style can match the child’s internal element.

Dynamic values

Dynamic scoped props accept the same ClassValue shapes used by Svelte class attributes: strings, arrays, and object maps.

<script lang="ts">
    import ChildCard from './ChildCard.svelte'

    let active = $state(true)
    const classes = $derived(['parent-owned', { active }])
</script>

<ChildCard scoped:class={classes} />
<script lang="ts">
    import ChildCard from './ChildCard.svelte'

    let active = $state(true)
    const classes = $derived(['parent-owned', { active }])
</script>

<ChildCard scoped:class={classes} />

The preprocessor routes dynamic values through a tiny runtime helper that normalizes the value and appends the scope hash.

Prop aliases

The prop name comes after scoped:.

<FancyCard scoped:internalClass="inner-panel" />
<FancyCard scoped:internalClass="inner-panel" />

That rewrites to an internalClass prop. The child can expose whatever class-like prop names make sense for its API.

Spread forwarding

scoped: intent must be expressed before a spread object exists.

<MiddleChild scoped:class="parent-owned" />
<MiddleChild scoped:class="parent-owned" />

After the preprocessor rewrites the prop, a middle component can forward it like any other prop.

<script lang="ts">
    let props = $props()
</script>

<FinalChild {...props} />
<script lang="ts">
    let props = $props()
</script>

<FinalChild {...props} />

A raw object spread cannot contain scoped: syntax, so scope first and spread later.

What this is not

  • It is not a Svelte core feature.
  • It does not make every component class prop special.
  • It does not inspect child component TypeScript types.
  • It does not cross language boundaries where Svelte markup is no longer available.

For the full package surface, see the API Reference. For the current caveats, see Limits.