Type something to search...
How to Theme and Brand Sanity Studio

How to Theme and Brand Sanity Studio

To theme Sanity Studio, generate a theme object and pass it to the theme key in defineConfig. The current way is buildTheme() from @sanity/themer, which takes an accent colour, a text colour and a background for light and dark mode and builds a full Sanity UI theme from them. Older projects use buildLegacyTheme() from sanity, which still works but is deprecated. Branding goes beyond colour: set each workspace's title, subtitle and icon, and replace or wrap parts of the interface such as the navbar through studio.components.

A Studio that looks like your product feels like part of it, and small touches such as a red banner on the production dataset prevent real mistakes. This guide covers what you can customise, building a theme with Themer, the legacy theme API and how to migrate from it, workspace icons, custom navbars and layouts, per-workspace themes, and writing components that respect the theme and colour scheme.

What You Can Customise

Studio branding is deliberately limited to supported extension points, so upgrades don't break it. Here's what's available:

WhatWhereNotes
Colourstheme in defineConfigBuilt with buildTheme() from @sanity/themer
Studio nameWorkspace title and subtitleShown in the navbar and workspace switcher
Logo or iconWorkspace iconReplaces the deprecated studio.components.logo
Navbarstudio.components.navbarWrap or replace it, for example with a banner
Whole layoutstudio.components.layoutWrap the Studio in providers or extra UI
Tool menustudio.components.toolMenuChange how the list of tools renders
Fonts, spacing, radiiA full Sanity UI themeAdvanced; rarely worth it

There's no supported way to inject arbitrary CSS into the Studio. Styling internal class names might work today and break on the next release, so stick to the options above.

Build a Theme With Themer

Install the package:

npm install @sanity/themer

Then build a theme from a few colours and pass it to your config:

// theme.ts
import { buildTheme } from '@sanity/themer'

export const theme = buildTheme({
  light: {
    accent: '#e4572e',
    text: '#5b6170',
    background: '#ffffff',
  },
  dark: {
    accent: '#ff7a52',
    background: '#101217',
  },
})
// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { schemaTypes } from './schemaTypes'
import { theme } from './theme'

export default defineConfig({
  name: 'default',
  title: 'Acme Content',
  projectId: 'your-project-id',
  dataset: 'production',
  theme,
  plugins: [structureTool()],
  schema: { types: schemaTypes },
})

Each option controls a part of the palette:

  • accent: primary buttons, focus rings and links. This is your brand colour.
  • text: text, icons, borders and neutral surfaces. Leave it out and Themer derives a subtle tint from the accent.
  • background: the colour everything else blends onto.
  • contrast: a number from 15 to 100 that controls how much of the accent tints text and borders. The default is 85, and 100 removes the tint entirely.

Every option is optional, and so is each scheme. If you only set light, dark mode keeps the default Studio colours. Themer also adjusts colours that would make the interface unreadable, for example by darkening a dark background until text has enough contrast, so a brand colour that's too pale won't wreck the Studio.

Start From a Preset

Themer ships with ready-made palettes:

import { buildTheme, presets } from '@sanity/themer'

const verdant = presets.find((preset) => preset.slug === 'verdant')
export const theme = buildTheme(verdant?.options)

Try Colours Inside the Studio

Picking hex values blind is slow. Add the Themer tool during development and you get a sidebar that previews themes live across the Studio:

// sanity.config.ts
import { themerTool } from '@sanity/themer/tool'

export default defineConfig({
  // ...
  plugins: [structureTool(), ...(process.env.NODE_ENV === 'development' ? [themerTool()] : [])],
})

Once you're happy, copy the options into theme.ts and remove the tool, or keep it in development only as shown. @sanity/themer is still below version 1.0, so pin the version and check its changelog when upgrading.

The Legacy Theme API

Many Studios built on v3 use buildLegacyTheme(), which takes CSS-variable-style keys:

// theme.ts (legacy)
import { buildLegacyTheme } from 'sanity'

const props = {
  '--my-white': '#ffffff',
  '--my-black': '#101217',
  '--my-brand': '#e4572e',
  '--my-red': '#db4437',
  '--my-yellow': '#f4b400',
  '--my-green': '#0f9d58',
}

export const theme = buildLegacyTheme({
  '--black': props['--my-black'],
  '--white': props['--my-white'],
  '--gray': '#666',
  '--gray-base': '#666',
  '--component-bg': props['--my-white'],
  '--component-text-color': props['--my-black'],
  '--brand-primary': props['--my-brand'],
  '--default-button-color': '#666',
  '--default-button-primary-color': props['--my-brand'],
  '--default-button-success-color': props['--my-green'],
  '--default-button-warning-color': props['--my-yellow'],
  '--default-button-danger-color': props['--my-red'],
  '--state-info-color': props['--my-brand'],
  '--state-success-color': props['--my-green'],
  '--state-warning-color': props['--my-yellow'],
  '--state-danger-color': props['--my-red'],
  '--main-navigation-color': props['--my-black'],
  '--main-navigation-color--inverted': props['--my-white'],
  '--focus-color': props['--my-brand'],
})

Despite the names, these aren't real CSS variables. They're inputs that the function maps onto a Sanity UI theme. The function is marked deprecated and will be removed in a future major version, so plan a move to Themer.

Migrating to buildTheme

The mapping is mostly direct:

Legacy keyThemer option
--brand-primary, --focus-coloraccent
--gray, --gray-basetext
--white / --component-bglight.background
--blackdark.background
--state-*, button coloursNo direct equivalent; Studio defaults are used

If you imported a theme from the old themer.sanity.build URLs, @sanity/themer/legacy provides helpers to convert it. Check both light and dark mode after migrating, because the legacy API produced one palette and inverted it, while Themer lets you tune each scheme.

Workspace Title, Subtitle and Icon

The navbar shows the workspace icon and title. Set them in the config:

// sanity.config.tsx
import { defineConfig } from 'sanity'

function AcmeIcon() {
  return (
    <svg viewBox="0 0 32 32" width="100%" height="100%" aria-hidden="true">
      <rect width="32" height="32" rx="6" fill="#e4572e" />
      <path d="M9 23 16 8l7 15h-4l-3-7-3 7z" fill="#fff" />
    </svg>
  )
}

export default defineConfig({
  name: 'default',
  title: 'Acme Content',
  subtitle: 'Marketing site',
  icon: AcmeIcon,
  // ...
})

The config file needs a .tsx extension once it contains JSX. An inline SVG is the most reliable choice for the icon: it scales cleanly, works in an embedded Studio and in a hosted one, and needs no extra request.

The older studio.components.logo option still exists but is deprecated in favour of the workspace icon, so don't build anything new on it.

Customise the Navbar

studio.components.navbar receives the default navbar through props.renderDefault. Wrapping it is safer than replacing it, because you keep search, the tool menu and the user menu. A common use is a banner that warns editors which dataset they're in:

// components/EnvironmentNavbar.tsx
import { Card, Flex, Stack, Text } from '@sanity/ui'
import { useWorkspace, type NavbarProps } from 'sanity'

export function EnvironmentNavbar(props: NavbarProps) {
  const { dataset } = useWorkspace()
  const isProduction = dataset === 'production'

  return (
    <Stack>
      <Card padding={2} tone={isProduction ? 'critical' : 'caution'}>
        <Flex justify="center">
          <Text size={1} weight="medium">
            {isProduction ? 'Live content: changes appear on the website' : `Editing the ${dataset} dataset`}
          </Text>
        </Flex>
      </Card>
      {props.renderDefault(props)}
    </Stack>
  )
}
// sanity.config.tsx
import { EnvironmentNavbar } from './components/EnvironmentNavbar'

export default defineConfig({
  // ...
  studio: {
    components: {
      navbar: EnvironmentNavbar,
    },
  },
})

Using tone on a Card instead of hard-coded colours means the banner follows your theme and works in dark mode. If you run separate datasets per environment, Sanity datasets and environments shows how to set that up.

Wrap the Whole Layout

studio.components.layout wraps everything. Use it for providers your custom components share, or for something that should appear on every screen:

// components/StudioLayout.tsx
import type { LayoutProps } from 'sanity'
import { AnalyticsProvider } from './AnalyticsProvider'

export function StudioLayout(props: LayoutProps) {
  return <AnalyticsProvider>{props.renderDefault(props)}</AnalyticsProvider>
}

toolMenu and activeToolLayout work the same way. toolMenu receives the list of tools and the active tool name, so you can reorder or restyle them. Most Studios never need to touch it.

Plugins can set these components too, and they compose: each plugin's navbar wraps the next. If you're packaging branding for several Studios, put the theme and components in a plugin with definePlugin, which is covered in creating a Sanity plugin.

Different Themes per Workspace

theme, title and icon can be set per workspace. When one Studio serves several brands or sites, give each a distinct look so editors always know where they are:

// sanity.config.tsx
import { defineConfig } from 'sanity'
import { buildTheme } from '@sanity/themer'
import { structureTool } from 'sanity/structure'
import { schemaTypes } from './schemaTypes'

const shared = {
  projectId: 'your-project-id',
  plugins: [structureTool()],
  schema: { types: schemaTypes },
}

export default defineConfig([
  {
    ...shared,
    name: 'acme',
    title: 'Acme',
    basePath: '/acme',
    dataset: 'acme',
    theme: buildTheme({ light: { accent: '#e4572e' }, dark: { accent: '#ff7a52' } }),
  },
  {
    ...shared,
    name: 'globex',
    title: 'Globex',
    basePath: '/globex',
    dataset: 'globex',
    theme: buildTheme({ light: { accent: '#2e7de4' }, dark: { accent: '#5aa2ff' } }),
  },
])

A staging workspace in a muted colour next to a production workspace in your brand colour is a cheap and effective guard against editing the wrong dataset. Running multiple sites from one Sanity project covers workspaces in more depth.

Build Components That Follow the Theme

Custom inputs, views and dashboard widgets should look like they belong. The rule is simple: build them with @sanity/ui and avoid hard-coded colours.

Use Tones Instead of Colours

Sanity UI components take a tone prop (default, primary, positive, caution, critical and others). The theme decides what each tone looks like in light and dark mode:

import { Badge, Card, Stack, Text } from '@sanity/ui'

export function StatusNote({ ok }: { ok: boolean }) {
  return (
    <Card padding={3} radius={2} tone={ok ? 'positive' : 'critical'} border>
      <Stack space={2}>
        <Badge tone={ok ? 'positive' : 'critical'}>{ok ? 'Ready' : 'Needs work'}</Badge>
        <Text size={1} muted>
          This document {ok ? 'passes' : 'fails'} the checklist.
        </Text>
      </Stack>
    </Card>
  )
}

Read Theme Tokens Directly

When you're styling something Sanity UI doesn't cover, such as a chart or an SVG, read the tokens with useTheme_v2():

import { useTheme_v2 } from '@sanity/ui'

export function Sparkline({ points }: { points: number[] }) {
  const { color, space } = useTheme_v2()
  const max = Math.max(...points, 1)
  const path = points
    .map((p, i) => `${i === 0 ? 'M' : 'L'}${i * 10},${40 - (p / max) * 40}`)
    .join(' ')

  return (
    <svg viewBox={`0 0 ${(points.length - 1) * 10} 40`} style={{ padding: space[2] }}>
      <path d={path} fill="none" stroke={color.accent.fg} strokeWidth={2} />
    </svg>
  )
}

color gives you the current card's values, such as bg, fg, border, muted.fg and accent.fg, so the sparkline picks up your Themer accent and switches automatically in dark mode. space and radius are arrays of pixel values that match the scale Sanity UI uses.

Respect the Colour Scheme

Editors can choose light, dark or system mode from the user menu. If a component needs to know which is active, use useColorSchemeValue() from sanity, which returns 'light' or 'dark'. Prefer tones and tokens where possible, because they handle the switch for you.

Practical Tips

  • Check contrast in both schemes: a brand orange that works on white can be unreadable on near-black. Set a separate dark.accent.
  • Keep the accent for actions: if everything is brand-coloured, primary buttons stop standing out.
  • Brand the environment, not just the product: an obvious difference between staging and production prevents more problems than a perfect logo.
  • Wrap, don't replace: call props.renderDefault(props) in custom components so you keep future Studio features.
  • Redeploy after changes: theme and component changes are part of the Studio bundle, so run your usual Studio deployment to roll them out.

FAQ: Theming Sanity Studio

It still works, but it's marked deprecated and will be removed in a future major version. New projects should use buildTheme from @sanity/themer, and existing ones can map their legacy keys to the accent, text and background options.

Set the icon property on your workspace config to a React component, such as an inline SVG. The older studio.components.logo option is deprecated in favour of workspace icons.

There's no supported way to inject global CSS. Use the theme for colours, studio.components for structural changes and Sanity UI components with tones for your own UI, so upgrades don't break your styling.

Editors choose light, dark or system mode from the user menu, and the theme controls what each mode looks like. You can make both schemes look similar with Themer, but the choice belongs to each user.

Yes. theme, title, subtitle and icon are all workspace options, so a multi-workspace config can give each site or environment a different look.

No. The theme only changes the Studio interface. Your content and front end are unaffected, whether the Studio is hosted by Sanity or embedded in your own app.


Conclusion

Theming Sanity Studio takes a few lines of config. buildTheme() from @sanity/themer turns an accent, a text colour and a background into a full theme for light and dark mode, and the workspace title, subtitle and icon handle the branding in the navbar. Projects on buildLegacyTheme() can migrate by mapping a handful of keys.

For more than colour, wrap the navbar or layout through studio.components and keep calling renderDefault so you don't lose built-in features. Build your own components with Sanity UI tones and tokens, and they'll match the theme automatically. That pairs naturally with a custom desk structure and custom input components, which together make the Studio feel built for your team.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper