Style parts and their state, then add entry and exit transitions.

Parts render without styles. The component demos use plain CSS, and the same state attributes and CSS variables work with Tailwind CSS or any other stylesheet.

Style a Part

Every part has a data-slot attribute. State adds attributes such as data-pressed and data-disabled:

<script lang="ts">
  import { Toggle } from "@ui-sdk/svelte/toggle"
</script>

<Toggle
  class="rounded-md border border-neutral-300 px-2.5 py-1.5 text-sm data-pressed:bg-neutral-100 data-disabled:opacity-50"
  >Bold</Toggle
>

Most state attributes are present or absent. Others have values, such as data-side="top". Each component's Styling section lists its attributes and CSS variables. Use those rather than generated IDs.

CSS Modules

Import a CSS Module and pass its class to the part. State selectors work the same way:

<script lang="ts">
  import { Toggle } from "@ui-sdk/svelte/toggle"
  import styles from "./toggle.module.css"
</script>

<Toggle class={styles.toggle}>Bold</Toggle>
toggle.module.css
.toggle {
  cursor: default;
  border: 1px solid var(--input, #e5e5e5);
  border-radius: 1.125rem;
  background: transparent;
  padding: 0.375rem 0.625rem;
  color: inherit;
  font: inherit;
  font-size: 0.875rem;
}

.toggle[data-pressed] {
  background: var(--muted, #f5f5f5);
}

.toggle[data-disabled] {
  opacity: 0.5;
}

.toggle:focus-visible {
  outline: 2px solid var(--ring, #a3a3a3);
  outline-offset: 2px;
}

You can also use Svelte's scoped styles. When a selector targets markup inside a component, use :global, such as :global([data-slot="toggle"]). CSS Modules avoid that scoping boundary.

Animate Opening and Closing

Popup content gets data-starting-style before it enters and data-ending-style while it exits. It stays mounted until the exit transition or animation finishes:

<script lang="ts">
  import {
    Popover,
    PopoverClose,
    PopoverContent,
    PopoverTitle,
    PopoverTrigger,
  } from "@ui-sdk/svelte/popover"
</script>

<Popover>
  <PopoverTrigger>Details</PopoverTrigger>
  <PopoverContent
    class="origin-(--transform-origin) transition-[opacity,scale] duration-150 data-starting-style:scale-96 data-starting-style:opacity-0 data-ending-style:scale-96 data-ending-style:opacity-0 motion-reduce:transition-none"
  >
    <PopoverTitle>Project Details</PopoverTitle>
    <p>Last updated today.</p>
    <PopoverClose>Done</PopoverClose>
  </PopoverContent>
</Popover>

Put exit styles on data-ending-style. data-closed describes the state but doesn't keep the element around for an exit.

Svelte Transitions

Svelte's own transitions work too. Put one on the element you render, with |global, since the block that adds and removes the element is inside the component. A popup stays visible above the page until its outro ends:

<script lang="ts">
  import {
    Popover,
    PopoverContent,
    PopoverTrigger,
  } from "@ui-sdk/svelte/popover"
  import { fade } from "svelte/transition"
</script>

<Popover>
  <PopoverTrigger>Details</PopoverTrigger>
  <PopoverContent aria-label="Project details">
    {#snippet render({ props })}
      <div {...props} transition:fade|global={{ duration: 150 }}>
        Last updated today.
      </div>
    {/snippet}
  </PopoverContent>
</Popover>

Popups provide --anchor-width and --available-height for sizing. Popups covers mounting and placement.

The ui-sdk Layer

A few behaviors need a little CSS, like keeping a closed popup hidden or dimming the page behind a sheet, so ui-sdk adds it to the page in a cascade layer named ui-sdk. Declare that layer at the top of your global stylesheet, like the app.css your root +layout.svelte imports, so your own styles win over it wherever the page puts them. With Tailwind, put it above @import "tailwindcss":

@layer ui-sdk;

A component's <style> block loads with the component, too late to come first, so keep the line in the global stylesheet.

ui-sdk adds this CSS once a component mounts, so HTML rendered on the server doesn't have it until the page hydrates. Closed content kept in the page, like a tooltip's, carries hidden, and a class that sets display shows it until then. Tailwind's base styles already hide hidden elements. With other CSS, add the same rule to your global stylesheet:

@layer ui-sdk {
  :where([data-slot][hidden]:not([hidden="until-found"])) {
    display: none !important;
  }
}

It leaves hidden="until-found" alone, so find in page can still open a closed panel.

Content Security Policy

A Content Security Policy without 'unsafe-inline' in style-src blocks the CSS ui-sdk adds. Give ui-sdk your policy's nonce with a meta tag in the page's <head>:

<meta property="csp-nonce" nonce="your-nonce" />

If your policy has no nonce for styles, add the tag without the nonce attribute. ui-sdk then adds its CSS as constructed style sheets, which no policy blocks. If your code replaces document.adoptedStyleSheets, ui-sdk adds its sheets back the next time a component mounts.

In SvelteKit, put the tag without a nonce in src/app.html. It works with any kit.csp mode, on prerendered pages too, as long as @layer ui-sdk; comes first in your global stylesheet:

<meta property="csp-nonce" />

SvelteKit's nonce="%sveltekit.nonce%" works only when style-src carries that nonce, and SvelteKit adds it there only for CSS it inlines itself, which kit.inlineStyleThreshold turns on. The placeholder also stops a page from being prerendered.

With Vite alone, set html.cspNonce in your Vite config to a placeholder, and Vite writes the tag for you. Your server replaces the placeholder with a new nonce on each request.

Keep Svelte's default css: "external" under a strict policy, since css: "injected" adds each component's styles as a <style> without a nonce.

Pages rendered on the server also carry a few style attributes, like the one that hides a select's native input, so the policy needs style-src-attr 'unsafe-inline' too.