> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cskn.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Themes

> Render your app's user page with the theme its owner selected.

Owners can pick a theme for your app's public page. The SDK provides the data half: which theme applies, where its bundle lives, and the controls to change it. The rendering half is [`<ThemeHost>`](/ui/themes) in @cskn/ui, which runs the theme in a sandbox and draws the page tree it returns.

```tsx theme={null}
import { useThemeSelection } from '@cskn/sdk';
import { ThemeHost } from '@cskn/ui';

function UserPage({ username, feed, builtIn, holding }: Props) {
  const { selection, bundleUrl, isResolving, expectsTheme, prerendered, reportResult } =
    useThemeSelection(username);

  // Until the selection is known, hold a neutral frame if a theme is expected,
  // otherwise draw the built-in design. Drawing both in turn makes the page jump.
  if (isResolving) return expectsTheme ? holding : builtIn;
  if (!selection) return builtIn;

  return (
    <ThemeHost
      bundleUrl={bundleUrl}
      bundleHash={selection.theme.bundleHash}
      declaredTokens={selection.theme.tokens}
      settings={selection.settings}
      profile={selection.context?.profile ?? { name: username, username }}
      slots={{ content: feed }}
      fallback={builtIn}
      pending={holding}
      prerendered={prerendered}
      onThemeResult={reportResult}
    />
  );
}
```

## useThemeSelection(username, options?)

Reads are public; a visitor needs no session. When the host already resolved the theme before the page loaded, the hook starts with that answer and nothing is fetched.

<ResponseField name="selection" type="ThemeSelection | null">
  `{ theme, settings, context? }`. `null` when no theme is chosen, or the chosen theme was withdrawn.
</ResponseField>

<ResponseField name="bundleUrl" type="string | null">
  Version-pinned bundle path for `ThemeHost`. Do not build it yourself: the sandbox checks the bytes against the selection's hash, and an unpinned URL fails that check as soon as the theme is updated.
</ResponseField>

<ResponseField name="isResolving" type="boolean">The selection is still loading.</ResponseField>

<ResponseField name="expectsTheme" type="boolean">
  Whether a theme is expected to paint. Before the real answer arrives, it remembers what this browser saw last time.
</ResponseField>

<ResponseField name="prerendered" type="object | null">
  The page tree the edge already produced for this exact theme version. Pass it to `ThemeHost` to skip the sandbox on first paint.
</ResponseField>

<ResponseField name="selectTheme(slug, settings?) / clearTheme()" type="Promise">
  Change the signed-in owner's choice. Clearing keeps their tuning in case they return.
</ResponseField>

<ResponseField name="reportResult" type="(result) => void">
  Pass to `ThemeHost`'s `onThemeResult` to record render outcomes and timing.
</ResponseField>

`options.appSlug` defaults to the current app.

## Appearance screen

`useThemeGallery()` lists the themes published for your app: `{ themes, isLoading }`, each with `slug`, `name`, `description`, `image?`, `version` and the token paths the owner may tune. Pair it with `selectTheme` and @cskn/ui's [`ThemeSettings`](/ui/themes#themesettings) for the tuning controls.

## Plain functions

`getThemes`, `getThemeSelection`, `saveThemeSelection`, `clearThemeSelection`, `themeBundleUrl`, `reportThemeResult`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.