# Custom Parts

Write a part with its own Vue logic and connect it to a primitive.

The parts from [Rendering Primitives](/docs/vue/rendering) are enough until one needs its own logic. You can replace just that part and keep the same `rating.ts` definition from [Compose Behaviors](/docs/core/compose-behaviors#complete-primitive).

## Write a Root Part

Here's a replacement for the generated `RatingRoot`. `usePrimitive` creates its instance, and `usePart` connects the `root` slot:

```ts title="rating-root.ts"
import { usePart, usePrimitive, type PartSlots } from "@ui-sdk/vue/primitive"
import { defineComponent, type HTMLAttributes } from "vue"
import { rating } from "./rating"

const RatingRoot = defineComponent(
  (
    props: { value?: number; defaultValue?: number } & HTMLAttributes,
    { emit }
  ) => {
    const handle = usePrimitive(rating, () => ({
      value: props.value,
      defaultValue: props.defaultValue,
      onValueChange(value, details) {
        emit("value-change", value, details)
        if (!details.canceled) {
          emit("update:value", value)
        }
      },
    }))
    return usePart(handle, "root", undefined, undefined, "div")
  },
  {
    inheritAttrs: false,
    props: ["value", "defaultValue"],
    emits: ["value-change", "update:value"],
    slots: Object as PartSlots<typeof rating, "root">,
  }
)

export { RatingRoot }
```

Call both helpers in `setup`. The options getter keeps values current; `usePrimitive` also shares the instance with child parts. `usePart` returns a render function that forwards attrs and slots. `inheritAttrs: false` avoids applying attrs twice.

`HTMLAttributes` makes native attrs available to TypeScript callers; only `value` and `defaultValue` are declared component props, so the other attrs still reach `usePart`.

This root supports `v-model:value` by emitting `update:value` after the change callback has had a chance to cancel. `PartSlots` types the default and `#render` slots. The adapter manages the instance's lifetime.

## 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`:

```ts title="rating-star.ts"
import { usePart, type PartSlots } from "@ui-sdk/vue/primitive"
import { defineComponent, type ButtonHTMLAttributes } from "vue"
import { rating } from "./rating"

const RatingStar = defineComponent(
  (props: { value: string } & Omit<ButtonHTMLAttributes, "value">) => {
    return usePart(
      rating,
      "star",
      () => ({ value: props.value }),
      undefined,
      "button"
    )
  },
  {
    inheritAttrs: false,
    props: ["value"],
    slots: Object as PartSlots<typeof rating, "star">,
  }
)

export { RatingStar }
```

Use it in place of the generated star in [Render the Rating](/docs/vue/rendering#render-the-rating). `ButtonHTMLAttributes` types its native attrs. 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

`usePart` merges the component's attrs with the slot's. Slot attributes win, classes and styles combine, and your listeners run first. An `id`, or a name or typing hint you pass, like `aria-label` or `autocomplete`, replaces the slot's. Call `preventDefault()` in a listener to skip the built-in handler.

The last argument can be a tag, or an object with `tag`, `children`, and `extend`. Use `extend` for extra props after the slot's props, such as attributes supplied by a Field. Keep the slot's roles, IDs, and ARIA state intact.

## Compose a Popup

A custom component can also use an existing Popover or Dialog. [Compose Ours](/docs/core/behaviors/popups#compose-ours) shows that connection with an emoji picker.
