Tools are top-level views in Sanity Studio, accessible through the main navigation. They provide dedicated interfaces for content management, debugging, and custom functionality.
Tools are React components with routing integration that appear in Studio’s navigation bar. Each tool has:
- Name - URL segment (e.g.,
/structure, /vision)
- Title - Display name in navigation
- Icon - Visual identifier
- Component - React component to render
- Router - Optional routing configuration
Sanity Studio comes with three main built-in tools:
The primary interface for content editing:
import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'
export default defineConfig({
// ...
plugins: [
structureTool(),
],
})
Features:
- Document lists and filtering
- Document editing forms
- Customizable structure
- Intent handling for create/edit
GROQ query playground for testing queries:
import {visionTool} from '@sanity/vision'
export default defineConfig({
// ...
plugins: [
visionTool({
defaultApiVersion: '2024-01-01',
defaultDataset: 'production',
}),
],
})
Features:
- GROQ syntax highlighting
- Real-time query results
- Dataset switching
- Query parameter support
Visual editing with preview integration:
import {presentationTool} from 'sanity/presentation'
export default defineConfig({
// ...
plugins: [
presentationTool({
previewUrl: 'http://localhost:3000',
allowOrigins: ['http://localhost:3000'],
}),
],
})
Features:
- Live preview of content
- Visual editing overlays
- Location resolver
- Cross-origin preview support
Create a custom tool using definePlugin:
import {definePlugin} from 'sanity'
import {RocketIcon} from '@sanity/icons'
import {MyToolComponent} from './MyToolComponent'
export const myTool = definePlugin({
name: 'my-tool-plugin',
tools: [
{
name: 'my-tool',
title: 'My Tool',
icon: RocketIcon,
component: MyToolComponent,
},
],
})
The tool component receives props including the tool configuration:
import {type Tool} from 'sanity'
export function MyToolComponent({tool}: {tool: Tool}) {
return (
<div style={{padding: '1rem'}}>
<h1>{tool.title}</h1>
<p>This is my custom tool!</p>
</div>
)
}
Add routing to your tool for deep linking:
import {definePlugin} from 'sanity'
import {route} from 'sanity/router'
import {MyToolComponent} from './MyToolComponent'
export const myTool = definePlugin({
name: 'my-tool-plugin',
tools: [
{
name: 'my-tool',
title: 'My Tool',
component: MyToolComponent,
router: route.create('/', [
route.create('/view/:id'),
route.create('/settings'),
]),
},
],
})
Using router state
import {useRouterState} from 'sanity/router'
export function MyToolComponent() {
const routerState = useRouterState()
const {id} = routerState as {id?: string}
return (
<div>
{id ? (
<ViewDetail id={id} />
) : (
<ViewList />
)}
</div>
)
}
Navigating programmatically
import {useRouter} from 'sanity/router'
export function MyToolComponent() {
const router = useRouter()
const handleClick = (id: string) => {
router.navigateUrl({path: `/my-tool/view/${id}`})
}
return (
<button onClick={() => handleClick('abc123')}>
View Details
</button>
)
}
Intent handling
Tools can handle intents like “edit” or “create”:
import {definePlugin} from 'sanity'
export const myTool = definePlugin({
name: 'my-tool-plugin',
tools: [
{
name: 'my-tool',
title: 'My Tool',
component: MyToolComponent,
canHandleIntent: (intent, params) => {
// Return true if this tool can handle the intent
if (intent === 'edit' && params.type === 'myType') {
return true
}
return false
},
getIntentState: (intent, params) => {
// Map intent to tool's router state
if (intent === 'edit') {
return {id: params.id}
}
return null
},
},
],
})
Pass options to configure tool behavior:
import {definePlugin} from 'sanity'
interface MyToolOptions {
title?: string
showSettings?: boolean
}
export const myTool = definePlugin<MyToolOptions>((options = {}) => {
const {title = 'My Tool', showSettings = true} = options
return {
name: 'my-tool-plugin',
tools: [
{
name: 'my-tool',
title,
component: (props) => (
<MyToolComponent showSettings={showSettings} {...props} />
),
},
],
}
})
Usage:
export default defineConfig({
// ...
plugins: [
myTool({
title: 'Analytics',
showSettings: false,
}),
],
})
Accessing Studio context
Tools can access Studio context for project information and utilities:
import {useClient, useProjectId, useDataset, useSchema} from 'sanity'
export function MyToolComponent() {
const client = useClient({apiVersion: '2024-01-01'})
const projectId = useProjectId()
const dataset = useDataset()
const schema = useSchema()
const fetchData = async () => {
const data = await client.fetch('*[_type == "post"]')
console.log(data)
}
return (
<div>
<p>Project: {projectId}</p>
<p>Dataset: {dataset}</p>
<button onClick={fetchData}>Fetch Posts</button>
</div>
)
}
Use Sanity UI components for consistent styling:
import {Card, Container, Heading, Stack, Text} from '@sanity/ui'
export function MyToolComponent() {
return (
<Container width={4}>
<Card padding={4}>
<Stack space={4}>
<Heading as="h1">My Tool</Heading>
<Text>
This tool uses Sanity UI components for consistent styling.
</Text>
</Stack>
</Card>
</Container>
)
}
Analytics dashboard
import {definePlugin} from 'sanity'
import {ChartUpwardIcon} from '@sanity/icons'
import {Card, Container, Heading, Stack} from '@sanity/ui'
import {useEffect, useState} from 'react'
import {useClient} from 'sanity'
function AnalyticsDashboard() {
const client = useClient({apiVersion: '2024-01-01'})
const [stats, setStats] = useState(null)
useEffect(() => {
async function fetchStats() {
const result = await client.fetch(`{
"posts": count(*[_type == "post"]),
"authors": count(*[_type == "author"]),
"published": count(*[_type == "post" && !(_id in path("drafts.**"))])
}`)
setStats(result)
}
fetchStats()
}, [])
if (!stats) return <div>Loading...</div>
return (
<Container width={4}>
<Card padding={4}>
<Stack space={4}>
<Heading>Analytics Dashboard</Heading>
<Card padding={3} tone="primary">
<Text>Total Posts: {stats.posts}</Text>
</Card>
<Card padding={3} tone="positive">
<Text>Published: {stats.published}</Text>
</Card>
<Card padding={3}>
<Text>Authors: {stats.authors}</Text>
</Card>
</Stack>
</Card>
</Container>
)
}
export const analyticsPlugin = definePlugin({
name: 'analytics-plugin',
tools: [
{
name: 'analytics',
title: 'Analytics',
icon: ChartUpwardIcon,
component: AnalyticsDashboard,
},
],
})
import {definePlugin} from 'sanity'
import {RocketIcon} from '@sanity/icons'
import {Button, Card, Container, Heading, Stack, Text} from '@sanity/ui'
import {useState} from 'react'
function DeploymentTool() {
const [deploying, setDeploying] = useState(false)
const [status, setStatus] = useState('')
const handleDeploy = async () => {
setDeploying(true)
setStatus('Deploying...')
try {
// Trigger deployment via API
await fetch('/api/deploy', {method: 'POST'})
setStatus('Deployed successfully!')
} catch (error) {
setStatus(`Error: ${error.message}`)
} finally {
setDeploying(false)
}
}
return (
<Container width={2}>
<Card padding={4}>
<Stack space={4}>
<Heading>Deploy Website</Heading>
<Text>
Trigger a new deployment of your production website.
</Text>
<Button
text="Deploy Now"
tone="primary"
onClick={handleDeploy}
disabled={deploying}
/>
{status && <Text>{status}</Text>}
</Stack>
</Card>
</Container>
)
}
export const deploymentPlugin = definePlugin({
name: 'deployment-plugin',
tools: [
{
name: 'deploy',
title: 'Deploy',
icon: RocketIcon,
component: DeploymentTool,
},
],
})
Content report tool
import {definePlugin} from 'sanity'
import {DocumentTextIcon} from '@sanity/icons'
import {Card, Container, Heading, Stack, Table} from '@sanity/ui'
import {useEffect, useState} from 'react'
import {useClient} from 'sanity'
function ContentReportTool() {
const client = useClient({apiVersion: '2024-01-01'})
const [report, setReport] = useState([])
useEffect(() => {
async function generateReport() {
const posts = await client.fetch(`
*[_type == "post"] {
_id,
title,
"wordCount": length(pt::text(content)),
"author": author->name,
publishedAt
} | order(publishedAt desc)
`)
setReport(posts)
}
generateReport()
}, [])
return (
<Container width={5}>
<Card padding={4}>
<Stack space={4}>
<Heading>Content Report</Heading>
<Table>
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Word Count</th>
<th>Published</th>
</tr>
</thead>
<tbody>
{report.map((post) => (
<tr key={post._id}>
<td>{post.title}</td>
<td>{post.author}</td>
<td>{post.wordCount}</td>
<td>{new Date(post.publishedAt).toLocaleDateString()}</td>
</tr>
))}
</tbody>
</Table>
</Stack>
</Card>
</Container>
)
}
export const contentReportPlugin = definePlugin({
name: 'content-report-plugin',
tools: [
{
name: 'report',
title: 'Content Report',
icon: DocumentTextIcon,
component: ContentReportTool,
},
],
})
Restrict tool access based on user permissions:
export default defineConfig({
// ...
tools: (prev, {currentUser}) => {
const isAdmin = currentUser?.roles.some(
(role) => role.name === 'administrator'
)
// Remove sensitive tools for non-admins
if (!isAdmin) {
return prev.filter(
(tool) => !['vision', 'deploy'].includes(tool.name)
)
}
return prev
},
})
Use Sanity UI components (@sanity/ui) for consistent styling with the rest of Studio.
Tools can access all Studio context, including the client, schema, current user, and workspace configuration.
Be cautious when performing mutations or external API calls from tools. Always validate user permissions and handle errors gracefully.