Skip to main content
Custom tools let you add new top-level views to Sanity Studio. Tools appear in the main navigation and can provide any functionality you need.

Tool Interface

Define a custom tool in your Studio configuration:
import {defineConfig} from 'sanity'
import {MyCustomTool} from './tools/MyCustomTool'
import {RocketIcon} from '@sanity/icons'

export default defineConfig({
  // ...
  tools: [
    {
      name: 'my-tool',
      title: 'My Tool',
      icon: RocketIcon,
      component: MyCustomTool,
    },
  ],
})

Tool Configuration

name
string
required
Unique identifier for the tool (used in URL: /tool-name)
title
string
required
Display title shown in navigation
icon
ComponentType
Icon component (from @sanity/icons or custom)
component
ComponentType
required
React component to render when the tool is active
options
object
Custom options passed to the component
canHandleIntent
function
Function to determine if this tool can handle intents
canHandleIntent: (intent, params) => {
  if (intent === 'custom-action') return true
  return false
}
getIntentState
function
Map intent parameters to tool state
getIntentState: (intent, params) => {
  return {documentId: params.id}
}
router
Router
Custom router for the tool’s URL structure
controlsDocumentTitle
boolean
Whether this tool controls the browser document title. Default: false

Creating a Custom Tool

Basic custom tool component:
// tools/MyCustomTool.tsx
import {Card, Container, Heading, Stack} from '@sanity/ui'
import {useClient, useDataset, useProjectId} from 'sanity'

export function MyCustomTool() {
  const client = useClient({apiVersion: '2024-01-01'})
  const dataset = useDataset()
  const projectId = useProjectId()
  
  return (
    <Container width={2} padding={4}>
      <Stack space={4}>
        <Heading as="h1">My Custom Tool</Heading>
        <Card padding={4} radius={2} shadow={1}>
          <Stack space={3}>
            <p>Project: {projectId}</p>
            <p>Dataset: {dataset}</p>
          </Stack>
        </Card>
      </Stack>
    </Container>
  )
}

Tool with Options

Pass configuration to your tool:
// sanity.config.ts
import {defineConfig} from 'sanity'
import {MyTool} from './tools/MyTool'

export default defineConfig({
  tools: [
    {
      name: 'analytics',
      title: 'Analytics',
      component: MyTool,
      options: {
        apiKey: process.env.SANITY_STUDIO_ANALYTICS_KEY,
        refreshInterval: 60000,
      },
    },
  ],
})

// tools/MyTool.tsx
import {useEffect, useState} from 'react'

interface MyToolOptions {
  apiKey: string
  refreshInterval: number
}

interface MyToolProps {
  tool: {
    options: MyToolOptions
  }
}

export function MyTool(props: MyToolProps) {
  const {apiKey, refreshInterval} = props.tool.options
  const [data, setData] = useState(null)
  
  useEffect(() => {
    const interval = setInterval(() => {
      // Fetch analytics data
    }, refreshInterval)
    
    return () => clearInterval(interval)
  }, [refreshInterval])
  
  return <div>Analytics Tool</div>
}

Tool with Routing

Add custom routing to your tool:
// sanity.config.ts
import {route} from 'sanity/router'
import {MyTool} from './tools/MyTool'

export default defineConfig({
  tools: [
    {
      name: 'dashboard',
      title: 'Dashboard',
      component: MyTool,
      router: route.create('/:view', [route.create('/:id')]),
    },
  ],
})

// tools/MyTool.tsx
import {useRouter} from 'sanity/router'

export function MyTool() {
  const router = useRouter()
  const {view, id} = router.state
  
  return (
    <div>
      <nav>
        <button onClick={() => router.navigate({view: 'overview'})}>
          Overview
        </button>
        <button onClick={() => router.navigate({view: 'details'})}>
          Details
        </button>
      </nav>
      
      {view === 'overview' && <OverviewView />}
      {view === 'details' && <DetailsView id={id} />}
    </div>
  )
}

Tool with Intent Handling

Make your tool respond to Studio intents:
export default defineConfig({
  tools: [
    {
      name: 'reports',
      title: 'Reports',
      component: ReportsTool,
      canHandleIntent: (intent, params) => {
        if (intent === 'view-report' && params.reportId) {
          return true
        }
        return false
      },
      getIntentState: (intent, params) => {
        if (intent === 'view-report') {
          return {
            reportId: params.reportId,
            from: params.from,
            to: params.to,
          }
        }
        return {}
      },
    },
  ],
})

// Link to this tool from elsewhere:
import {IntentLink} from 'sanity/router'

function ReportLink() {
  return (
    <IntentLink
      intent="view-report"
      params={{reportId: 'monthly', from: '2024-01-01', to: '2024-01-31'}}
    >
      View Report
    </IntentLink>
  )
}

Example: Analytics Dashboard

// tools/AnalyticsDashboard.tsx
import {Card, Container, Grid, Heading, Stack, Text} from '@sanity/ui'
import {useClient} from 'sanity'
import {useEffect, useState} from 'react'

interface Stats {
  totalDocuments: number
  publishedDocuments: number
  draftDocuments: number
}

export function AnalyticsDashboard() {
  const client = useClient({apiVersion: '2024-01-01'})
  const [stats, setStats] = useState<Stats | null>(null)
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    async function fetchStats() {
      const [total, published, drafts] = await Promise.all([
        client.fetch('count(*[!(_id in path("_.**"))])'),
        client.fetch('count(*[!(_id in path("drafts.**")) && !(_id in path("_.**"))])'),
        client.fetch('count(*[_id in path("drafts.**")])'),
      ])
      
      setStats({
        totalDocuments: total,
        publishedDocuments: published,
        draftDocuments: drafts,
      })
      setLoading(false)
    }
    
    fetchStats()
  }, [client])
  
  if (loading) return <div>Loading...</div>
  
  return (
    <Container width={4} padding={4}>
      <Stack space={4}>
        <Heading as="h1" size={3}>Analytics Dashboard</Heading>
        
        <Grid columns={3} gap={4}>
          <Card padding={4} radius={2} shadow={1} tone="primary">
            <Stack space={3}>
              <Text size={4} weight="bold">{stats?.totalDocuments}</Text>
              <Text size={1} muted>Total Documents</Text>
            </Stack>
          </Card>
          
          <Card padding={4} radius={2} shadow={1} tone="positive">
            <Stack space={3}>
              <Text size={4} weight="bold">{stats?.publishedDocuments}</Text>
              <Text size={1} muted>Published</Text>
            </Stack>
          </Card>
          
          <Card padding={4} radius={2} shadow={1} tone="caution">
            <Stack space={3}>
              <Text size={4} weight="bold">{stats?.draftDocuments}</Text>
              <Text size={1} muted>Drafts</Text>
            </Stack>
          </Card>
        </Grid>
      </Stack>
    </Container>
  )
}

// sanity.config.ts
import {ChartUpwardIcon} from '@sanity/icons'
import {AnalyticsDashboard} from './tools/AnalyticsDashboard'

export default defineConfig({
  tools: [
    {
      name: 'analytics',
      title: 'Analytics',
      icon: ChartUpwardIcon,
      component: AnalyticsDashboard,
    },
  ],
})

definePlugin

Create reusable tool plugins:
// plugins/analyticsPlugin.ts
import {definePlugin} from 'sanity'
import {ChartUpwardIcon} from '@sanity/icons'
import {AnalyticsDashboard} from './AnalyticsDashboard'

export interface AnalyticsPluginOptions {
  refreshInterval?: number
}

export const analyticsPlugin = definePlugin<AnalyticsPluginOptions>((options) => {
  return {
    name: 'analytics-plugin',
    tools: [
      {
        name: 'analytics',
        title: 'Analytics',
        icon: ChartUpwardIcon,
        component: AnalyticsDashboard,
        options,
      },
    ],
  }
})

// sanity.config.ts
import {analyticsPlugin} from './plugins/analyticsPlugin'

export default defineConfig({
  plugins: [
    analyticsPlugin({
      refreshInterval: 60000,
    }),
  ],
})

Available Hooks

Hooks you can use in custom tools:
  • useClient() - Get Sanity client
  • useDataset() - Get current dataset
  • useProjectId() - Get project ID
  • useSchema() - Access schema
  • useCurrentUser() - Get current user
  • useWorkspace() - Get workspace config
  • useRouter() - Access router state
  • useColorScheme() - Get/set color scheme
See Studio Hooks for complete documentation.

Build docs developers (and LLMs) love