Early access. Read the blog post

Usage in Sanity Studio

When building tools and custom inputs in your studio, it's important for your editor's experience to keep things consistent. With Sanity UI, you can import properly styled components directly into any new customizations you make.

The studio ships with a version of Sanity UI, so no need to install new dependencies, we can immediately import any component directly into our files.

Creating a custom input with Sanity UI  

To add Sanity UI to a custom input requires that we first set up a simple custom input. First, we need to create a schema document with the field we want to customize.

Let's make a string input that is very important for SEO. So important that we want to add a tooltip on top of the standard description.

Standard string field

export default {
  name: 'category',
  title: 'Category',
  type: 'document',
  fields: [
    {
      name: 'seoString',
      title: 'Something really important for SEO',
      type: 'string',
      inputComponent: HoverInput
    }
  ]
}

Next, we need to create this custom input component.

Let's start by just having it be a standard string field. To do this, we need a few additional tools from Sanity's bag of tricks to make the data sync properly and bring amazing features like Presence and Review Changes into the mix. We'll also import our first component from Sanity UI, the humble TextInput.

import React from 'react'

// Important items to allow form fields to work properly and patch the dataset.
import {PatchEvent, set} from 'part:@sanity/form-builder/patch-event'
import FormField from 'part:@sanity/components/formfields/default'

// Import the TextInput from UI
import { TextInput } from '@sanity/ui'


const HoverInput = React.forwardRef((props, ref) => {
  const { type, onChange } = props
  return(
    <FormField label={type.title} description={type.description}>
      <TextInput
        type="text"
        ref={ref}
        placeholder={type.placeholder}
        value={props.value}
        onChange={event => {onChange(PatchEvent.from(set(event.target.value)))}}
      /> 
    </FormField>
  )

})

export default {
  name: 'category',
  title: 'Category',
  type: 'document',
  fields: [
    {
      name: 'seoString',
      title: 'Something really important for SEO',
      type: 'string',
      inputComponent: HoverInput
    }
  ]
}

Next, we need to add our tooltip. We'll do that by importing both the Tooltip component, as well as other components to help build the visual design of the tip.

import React from 'react'

import {PatchEvent, set} from 'part:@sanity/form-builder/patch-event'
import FormField from 'part:@sanity/components/formfields/default'

import {
  Tooltip,
  Text,
  Box,
  TextInput
} from '@sanity/ui'

const HoverInput = React.forwardRef((props, ref) => {
  const { type, onChange } = props
  return(
    <FormField label={type.title} description={type.description}>
      <Tooltip
        content={(
          <Box padding={2}>
            <Text>Important Text</Text>
          </Box>
        )}
        placement="top"
      >
        <TextInput
          type="text"
          ref={ref}
          placeholder={type.placeholder}
          value={props.value}
          onChange={event => {onChange(PatchEvent.from(set(event.target.value)))}}
        /> 
      </Tooltip>
    </FormField>
  )

})
Tooltip over the input reading "Important Text"

And with that, we now have a tooltip appearing on hover for our input field. The tooltip will always read "Important Text." Let's make this dynamic for our new input.

We'll start by updating our schema with a new property: tipDescription.

export default {
  name: 'category',
  title: 'Category',
  type: 'document',
  fields: [
    {
      name: 'seoString',
      title: 'Something really important for SEO',
      description: 'Don\'t forget to make it SEO friendly!',
      type: 'string',
      tipDescription: 'Hey! Seriously, make it SEO friendly!',
      inputComponent: HoverInput
    }
  ]
}

Then, we'll use that new property in in our Tooltip.

<Tooltip
  content={(
    <Box padding={2}>
      <Text>{type.tipDescription}</Text>
    </Box>
  )}
  placement="top"
>
The tooltip now pulls dynamically from our schema

Finally, if you want to be extra protective, make sure to add a conditional in case someone forgets to add the tipDescription to their schema when using this. Check to make sure tipDescription exists before rendering a blank tooltip.

const HoverInput = React.forwardRef((props, ref) => {
  const { type, onChange } = props
  return(
    <FormField label={type.title} description={type.description}>
      {type.tipDescription ? 
        <Tooltip
          content={(
            <Box padding={2}>
              <Text>{type.tipDescription}</Text>
            </Box>
          )}
          placement="top"
        >
          <TextInput
            type="text"
            ref={ref}
            placeholder={type.placeholder}
            value={props.value}
            onChange={event => {onChange(PatchEvent.from(set(event.target.value)))}}
          /> 
        </Tooltip>
      : 
        <TextInput
          type="text"
          ref={ref}
          placeholder={type.placeholder}
          value={props.value}
          onChange={event => {onChange(PatchEvent.from(set(event.target.value)))}}
        /> 
      }
      
    </FormField>
  )

})

The full source code for this input is available on GitHub.

Building a custom tool with Sanity UI  

Finished UI layout for a comment dashboard

Custom tools are where Sanity UI has a little more room to shine. With a full page to work with, it can be overwhelming to begin designing from scratch. Not to worry, with layout components and design primitives, we'll have a layout knocked out in no time.

In order to get started building a custom tool, you'll first need to scaffold the files you'll need. The easiest way to do this is to run the following command with the Sanity CLI:

sanity init plugin

The CLI asks what type of tool to make. For our use case, we'll select "Basic, empty tool." It will ask you to name your plugin. For this example, we'll create a comment moderation dashboard, though this could be used for any type of content submitted by external users. We'll call our plugin comment-moderation.

This creates all the files we need. To hook this up to our studio, we'll need to add the plugin's name to our sanity.json file under the plugins array.

// ...
  "plugins": [
    "@sanity/base",
    "@sanity/components",
    "@sanity/default-layout",
    "@sanity/default-login",
    "@sanity/desk-tool",
    "comment-moderation"
  ],
// ...

Now, we're ready to get started.

Scaffolding the UI  

In MyTool.js, we'll refactor the starter code to export a functional React component and import a few UI components we'll need to get started.

import React from 'react'

import { Container, Card, Grid, Heading } from '@sanity/ui'
import styles from './MyTool.css'


export default function MyTool() {
  // Where our UI will render
  return (
      
  )

From here, we'll add some basic UI. Let's build out a top box, with a constrained width and a white background. We also will want a little text to describe what this tool does.

We can use the Container component to specify our centered-width box, the Card component to create a background and allow for padding and other styles, and the Heading component to create an <h1> on the page.

import React from 'react'

import { Container, Card, Grid, Heading } from '@sanity/ui'
import styles from './MyTool.css'


export default function MyTool() {
  // Where our UI will render
  return (
    <Container width={3}> 
      <Card margin={3} padding={5}>
        <Heading marginBottom={1} size={5} as={"h1"}>Comment Moderation Dashboard</Heading>
        <p>Moderate your comments here. Each box shows the latest 5 from each group.</p>
      </Card>
    </Container>
  )
}
Simple tool with static header box

From here, we can create a box for unapproved comments and list those comments out.

import React from 'react'

import { Container, Card, Grid, Heading, Stack, Box, Flex,Text, Label, Switch } from '@sanity/ui'
import styles from './MyTool.css'


export default function MyTool() {
  // Where our UI will render
  return (
    <Container width={3}> 
      <Card margin={3} padding={5} className={styles.container}>
        <Heading marginBottom={1} size={5} as={"h1"}>Comment Moderation Dashboard</Heading>
        <p>Moderate your comments here. Each box shows the latest 5 from each group.</p>
      </Card>

      <Card margin={3}>
        <Card marginBottom={1} paddingX={4} paddingTop={4} borderBottom={1} paddingBottom={0}>
          <Heading size={3} as={"h2"}>To be moderated</Heading>
          <p>Please moderate these comments</p>
        </Card>
        <Stack as={'ul'}>
          <Card borderBottom as={'li'} padding={4}>
            <Grid columns={5} justify={'space-between'} align={'center'}>
              <Box column={4}>
                <Stack space={3}>
                  <Text size={2}>This is a super amazing comment, please approve it</Text>
                  <Text muted size={1}>By: Sanity - A post to be named later</Text> 
                </Stack>
              </Box>
              <Flex justify={'center'} align={'center'}>
                <Stack space={3}>
                  <Label>Approved?</Label>
                  <Switch 
                    checked={false} 
                    indeterminate={true} 
                  />
                </Stack>
              </Flex>
            </Grid>
          </Card>
        </Stack>
      </Card>

    </Container>
  )
}
Now our tool has a "To be moderated" box with a placeholder comment

First, we'll create a new Card with similar margins above. Inside, we'll create a place for the Card to have a title and description. By using Card again, we'll get access to margin and padding, but also borders to create a section for this area and separate it from the rest.

Immediately after this nested Card, we'll create a Stack. The Stack operates as a set of stacked content, in this case, we'll use this as a ul and it's children will be list items.

Each of these comments will take place inside of a Grid component to comprise our layout. The grid will be set up with five columns and justify it's content with additional white space set to space-between and its vertical alignment set to center.

The comment text itself will exist inside a Box generic container that will set the number of columns for this to take up. Then, another Stack component to stack a little information about the comment. The text of the comment, author and post will be inside of the Text component and we can control the size and the color with properties.

Since we left enough room in our Grid, let's toss a Switch component in to act as our "approval" toggle. Since unapproved comments will come through as neither True nor False, we'll use the switch's indeterminate prop to properly center the toggle in between. An input without a label is a bit of a UX and accessibility issue, so, let's make sure to create a new stack and put both the switch and a new Label component in there.

At this point, our switch is aligned to the top of our comment, which is OK, but could be better. Let's wrap our stack in a Flex component to vertically center our components.

This shows unapproved comments, but let's take this area and make two more to show approved comments and rejected comments. First, let's componentize this code to clean things up a bit.

import React from 'react'

import { Container, Card, Grid, Heading, Stack, Box, Flex,Text, Label, Switch } from '@sanity/ui'
import styles from './MyTool.css'


function CommentCard() {

  return (
    ... ENTIRE CARD COMPONENT FROM BEFORE
  )
}


export default function MyTool() {
  // Where our UI will render
  return (
    <Container width={3}> 
      <Card margin={3} padding={5} className={styles.container}>
        <Heading marginBottom={1} size={5} as={"h1"}>Comment Moderation Dashboard</Heading>
        <p>Moderate your comments here. Each box shows the latest 5 from each group.</p>
      </Card>

      <CommentCard />
      <CommentCard />
      <CommentCard />

    </Container>
  )
}
Three identical "To be moderated" cards

This creates three identical stacked cards. Let's add some visual differences and have our approved and rejected boxes take up less visual space than our moderation queue.

To do this, we'll wrap our new CommentCard components in a Grid set to two columns.

<Grid columns={2}>
  <CommentCard />
  <CommentCard />
  <CommentCard />
</Grid>
Our identical cards are now on a 2-column grid

This gets our cards aligned in two columns, but we want our moderation queue to be full width. Let's add a prop to our custom component and have out card component go full width if it's present.

While we're at it, let's also add in a customizable title, description, and approval status.

import React from 'react'

import { Container, Card, Grid, Heading, Stack, Box, Flex,Text, Label, Switch } from '@sanity/ui'
import styles from './MyTool.css'


function CommentCard(props) {
  const { title, description, approvalStatus, fullWidth } = props
  return (
    <Card column={fullWidth ? 'full' : ''} margin={3}>
      <Card marginBottom={1} paddingX={4} paddingTop={4} borderBottom={1} paddingBottom={0}>
          <Heading size={3} as={"h2"}>{title}</Heading>
          <p>{description}</p>
        </Card>
        <Stack as={'ul'}>
          <Card radius={2} borderBottom as={'li'} padding={4}>
            <Grid columns={5} justify={'space-between'} align={'center'}>
              <Box column={4}>
                <Stack flex={1} space={3}>
                  <Text size={2}>This is a super amazing comment, please approve it</Text>
                  <Text muted size={1}>By: Sanity - A post to be named later</Text> 
                </Stack>
              </Box>
              <Flex justify={'center'} align={'center'}>
                <Stack space={3}>
                  <Label>Approved?</Label>
                  <Switch 
                    checked={approvalStatus} 
                    indeterminate={(approvalStatus === undefined) ? true : false} 
                  />
                </Stack>
              </Flex>
            </Grid>
          </Card>
        </Stack>
      </Card>
  )
}


export default function MyTool() {
  // Where our UI will render
  return (
    <Container width={3}> 
      <Card margin={3} padding={5} className={styles.container}>
        <Heading marginBottom={1} size={5} as={"h1"}>Comment Moderation Dashboard</Heading>
        <p>Moderate your comments here. Each box shows the latest 5 from each group.</p>
      </Card>

      <Grid columns={2}>
        <CommentCard fullWidth title="To Be Moderated" description="Moderate these please" approvalStatus={undefined} />
        <CommentCard title="Approved" description="These are the good ones" approvalStatus={true} />
        <CommentCard title="Unapproved" description="These are the bad ones" approvalStatus={false} />  

      </Grid>

    </Container>
  )
}
The grid with a full-width moderation queue and custom messages for each card

From here, it's a matter of pulling in live data and issuing patches in an onChange event on each comment. See the full source code in this project created from this Next.js starter. See the UI full source here.

Keep creating from here  

With just a few building blocks, you can create consistent, complex UI for your studio.

Where to go from here  

Updated Dec 18, 2020 @ 04:31
By
the platform for structured content