Build a Component
Build a toggle button, then render the same primitive in your framework.
Let's start with a toggle button. Press Mute to turn it on. Press it again to turn it off.
We'll define its behavior, connect it to your framework, and add styles.
Install
Start by installing @ui-sdk/core.
npm install @ui-sdk/coreDefine a Primitive
A primitive describes a component's behavior: its state, the functions that change it, and the parts that use it. It doesn't render anything on its own.
Ours is the mute button's. It has one piece of state, muted, which is true when mute is on, and one part.
import { definePrimitive } from "@ui-sdk/core"
const muteButton = definePrimitive({
name: "mute-button",
state: () => ({ muted: false }),
api: (context) => ({
toggle: () => context.store.set({ muted: !context.store.get().muted }),
}),
slots: (slot) => ({
root: slot({
attrs: (state) => ({
type: "button",
"aria-pressed": String(state.muted),
}),
press: ({ api }) => api.toggle(),
}),
}),
})
export { muteButton }statestartsmutedatfalse.apidefinestoggle, which switchesmutedbetweentrueandfalse.slotsdescribes the component's parts. Our only slot isroot, which becomes the button when we render it.attrsturns state into HTML attributes. Whenmutedistrue, the root getsaria-pressed="true".presscallstogglewhen the root is activated.
That's the whole behavior. Define a Primitive goes through each field in more detail. Now let's put it on the page.
Render It
An adapter connects the primitive to your framework. Choose your framework in the page header, then install its package.
npm install @ui-sdk/reactFirst, create the component from the muteButton primitive.
"use client"
import { createParts } from "@ui-sdk/react/primitive"
import { muteButton } from "@/lib/mute-button"
const { Root: MuteButton } = createParts(muteButton, { root: "button" })
export { MuteButton }{ root: "button" } says the root part of MuteButton renders a <button>. Then import it wherever you want to use it:
import { MuteButton } from "@/components/ui/mute-button"
function Example() {
return <MuteButton>Mute</MuteButton>
}Because we use a native <button>, clicks, Enter and Space all reach the primitive's press handler. Screen readers get its pressed state from aria-pressed.
The behavior stays in mute-button.ts. Changing frameworks changes how you render it, but the behavior stays the same. Render a Primitive covers options, state in your content and server rendering.
Style the Component
Parts render without styles. Here's a complete look for the button, with hover, focus and pressed states:
[data-slot="mute-button"] {
border: 1px solid #e5e5e5;
border-radius: 0.5rem;
background: white;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
&:hover {
background: #f5f5f5;
}
&:focus-visible {
outline: 2px solid #a3a3a3;
outline-offset: 2px;
}
&[aria-pressed="true"] {
background: #171717;
color: white;
}
}The pressed look comes from aria-pressed. While muted is true, the root part renders aria-pressed="true", from the attrs you wrote when you defined the primitive. In the CSS tab, the &[aria-pressed="true"] rule selects it. In the Tailwind tab, the aria-pressed: classes do the same.
data-slot identifies the part, and these attributes are the same in every framework, so this CSS works for all five versions. Style a Component covers more state attributes, CSS variables and parts that come and go.
Next
You've defined a primitive, rendered it and styled it. The Core guides take each step further:
Or start from what's already built: