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.
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,
},
],
})
Unique identifier for the tool (used in URL: /tool-name)
Display title shown in navigation
Icon component (from @sanity/icons or custom)
React component to render when the tool is active
Custom options passed to the component
Function to determine if this tool can handle intentscanHandleIntent: (intent, params) => {
if (intent === 'custom-action') return true
return false
}
Map intent parameters to tool stategetIntentState: (intent, params) => {
return {documentId: params.id}
}
Custom router for the tool’s URL structure
Whether this tool controls the browser document title. Default: false
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>
)
}
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>
}
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>
)
}
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.