Limits

Svelte Scoped Props is an experiment. These boundaries are intentional for the alpha.

Not a Svelte core feature

The package is a preprocessor that runs before Svelte parses the component. Native compiler support would have access to Svelte’s already-computed CSS scope hash and would not need to mirror private hash behavior.

Private hash mirror

Svelte does not currently export its default cssHash helper as a public API. This package mirrors that implementation. If Svelte changes the private default hash, projects should update this package or pass their own cssHash.

scopedProps({
    cssHash: ({ css, filename, hash }) => `svelte-${hash(filename ?? css)}`
})
scopedProps({
    cssHash: ({ css, filename, hash }) => `svelte-${hash(filename ?? css)}`
})

No child type graph

The preprocessor does not inspect the imported child component or its TypeScript prop types. scoped:class is an explicit usage-site decision by the parent.

That keeps the transform small, but it also means scoped: cannot automatically know which child props are ClassValue props.

Component tags only

Only uppercase and dotted component tags are supported.

<Child scoped:class="parent-owned" />
<motion.div scoped:class="parent-owned" />
<Child scoped:class="parent-owned" />
<motion.div scoped:class="parent-owned" />

Native elements are rejected because normal Svelte class behavior already works there.

Scope before spread

Object spreads cannot express Svelte directive syntax. Use scoped: before props are collected into an object, then forward the transformed prop normally.

Marker snippet

By default, the transform adds an uncalled snippet marker so Svelte keeps the parent CSS selectors alive during CSS analysis. The marker is synthesized from the component’s own selectors: for each scoped selector that has a combinator it emits a chain of <svelte:element this={'x'}> nodes that mirrors the selector’s structure (nested elements for descendant and child combinators, adjacent siblings for + and ~, pseudo-classes stripped), plus a flat fallback <svelte:element> that carries every scoped class at once. Because the marker nodes are dynamically typed, Svelte’s pruner matches them against any element-qualified selector and they carry no element content-model or void constraints, so single-compound selectors are defended by the fallback alone. The marker does not render, but it can leave a small unused function in compiled output.

scopedProps({ marker: false })
scopedProps({ marker: false })

Only disable the marker when you are running transform-level tests and are not asking Svelte to compile scoped CSS.

Selector support

The scanner is intentionally small and is not a full CSS parser. For scoped classes it synthesizes marker structure — so these selector forms survive Svelte’s unused-CSS pruning and keep the correct scope hash:

  • Compound classes on one element, such as .page-rows.paged.
  • Descendant, child, and sibling combinators, such as .a .b, .a > .b, and span.a + button.b.
  • Any element-qualified selector, including void and custom elements, such as button.foo, input.bar, or my-widget.baz — the marker element is dynamically typed, so it matches any type and has no void or content-model constraints.

These forms are not matched — only the flat fallback element defends them, so their structural shape can still be pruned:

  • Selectors containing attribute selectors, such as .a[data-open].
  • A :global(...) compound sitting inside a chain.
  • Functional or positional pseudo-classes such as :has(), :nth-child(), and :not() — pseudos are stripped, not matched.

Style block is exempt from unused-CSS pruning

A component that uses scoped: effectively opts its <style> block out of Svelte’s unused-CSS pruning for class-based rules: the marker gives every scoped class rule a local usage, so Svelte never reports those rules as unused. The tradeoff is that genuinely dead class CSS in that component will no longer be flagged by svelte-check, so keep an eye on rules you remove.