Write a part with its own Svelte logic and connect it to a primitive.
The parts from Rendering Primitives are enough until one needs its own logic. You can replace just that part and keep the same rating.ts definition from Compose Behaviors.
Write a Root Part
Here's a replacement for the generated RatingRoot. usePrimitive creates its instance, and usePart connects the root slot:
<script lang="ts">
import type { StateOf } from "@ui-sdk/core"
import {
Part,
usePart,
usePrimitive,
type PartProps,
} from "@ui-sdk/svelte/primitive"
import { setContext } from "svelte"
import { rating, type RatingOptions } from "./rating"
type RootProps = PartProps<"div", StateOf<typeof rating, "root">>
interface Props extends RatingOptions, Omit<RootProps, "defaultValue"> {}
let { value, defaultValue, onValueChange, ...props }: Props = $props()
const id = $props.id()
const handle = usePrimitive(rating, () => ({
id,
value,
defaultValue,
onValueChange,
}))
setContext(rating, handle)
const root = usePart(handle, "root")
</script>
{#if root.present}
<Part tag="div" part={root} {props} />
{/if}Pass options through a getter so they follow prop changes. $props.id() gives the instance a stable ID across server rendering and hydration. This manual root accepts value and onValueChange; it doesn't add a bind:value contract.
setContext(rating, handle) makes the instance available to child parts. A generated createParts root does this for you. usePart connects the slot, and Part renders it with the caller's render and children snippets. The instance is destroyed on unmount.
Keep the {#if root.present} check: Part doesn't hide or remove itself when a slot isn't present.
Connect a Child Part
Pass the primitive itself to usePart to find the instance shared by the root above it. This RatingStar works inside the manual root above or the generated RatingRoot:
<script lang="ts">
import { Part, usePart, type PartProps } from "@ui-sdk/svelte/primitive"
import { rating } from "./rating"
interface Props extends Omit<PartProps<"button">, "value"> {
value: string
}
let { value, ...props }: Props = $props()
const star = usePart(rating, "star", () => ({ value }))
</script>
{#if star.present}
<Part tag="button" part={star} {props} />
{/if}Use it in place of the generated star in Render the Rating. The value getter identifies the star; the remaining props go to its button. Keep it inside RatingRoot and give it an aria-label.
Merge Props
Part merges your props with the slot's. Slot attributes win, your event handlers run first, and attachments combine. An id, or a name or typing hint you pass, like aria-label or autocomplete, replaces the slot's. Put canceling handlers in the props passed to Part. Calling preventDefault() there skips the built-in behavior; a handler on an element inside a render snippet runs too late to cancel it.
For custom markup, part.slot(props) supplies the attributes and attachment. Read part.state in markup so Svelte tracks changes, and wrap the element in {#if part.present} to follow the slot's presence.
Compose a Popup
A custom component can also use an existing Popover or Dialog. Compose Ours shows that connection with an emoji picker.