Create a document form progress component

Summarize form progression by decorating the entire editing form for a document with a component loaded at the root level.

Summarize form progression by decorating the entire editing form for a document with a component loaded at the root level.

What you need to know

This guide assumes that you know how to set up and configure a Sanity Studio and have basic knowledge about defining a schema with document and field types. Basic knowledge of React and TypeScript is also useful, although you should be able to copy-paste the example code to get a runnable result. The examples use the gap prop on the Stack component from @sanity/ui, which requires @sanity/ui 3.2.0 or later.

Custom form components by example

One of Sanity Studio’s most powerful features is custom drop-in replacements for form fields. This guide is one in a series of code examples.

You can get more familiar with the Form Components API in the documentation.

What you’ll learn

In this guide, you will learn how to:

  • Customize the document form and interact with its values
  • Make a form customization that’s composable using render methods
  • Use Sanity UI in combination with a third-party library to make a custom form progress bar UI

Prepare the schema

The imaginary scenario is that your studio contains preflight documents that contain a checklist to complete before getting approval to proceed. Users of this studio could benefit from clearly showing how close to completion the current form is.

To complete this guide, you’ll need to add a new document type first. Create the following file in your studio and make sure to import it into the schema in sanity.config.tsx:

// ./schema/preflight/preflightType.ts

import {defineType, defineField} from 'sanity'

export const preflightType = defineType({
  name: 'preflight',
  title: 'Preflight',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      type: 'string',
    }),
    defineField({name: 'copyApproved', type: 'boolean'}),
    defineField({name: 'lighthouse', type: 'boolean'}),
    defineField({name: 'accessibility', type: 'boolean'}),
    defineField({name: 'seo', title: 'SEO', type: 'boolean'}),
    defineField({name: 'bestPractices', type: 'boolean'}),
  ],
})

Now create a new document. It’s a functional column of boolean fields.

A standard document with default boolean fields

All these fields should have detailed description values, but for brevity in this guide, they’ve been omitted.

Customize the document form

Create a custom form component to display the form’s current progress:

// ./schema/preflight/Progress.tsx

import {ObjectInputProps, ObjectMember} from 'sanity'
import {Flex, Card, Box, Stack} from '@sanity/ui'

interface ProgressProps extends ObjectInputProps {
  members: ObjectMember[]
}

type FieldProgress = {
  name: string
  value: boolean
}

export function Progress(props: ProgressProps) {
  const {members = []} = props
  const booleanFieldProgress = members.reduce<FieldProgress[]>((acc, member) => {
    const isFieldMember = member.kind === 'field' && member.field.schemaType.name === 'boolean'

    if (!isFieldMember) {
      return acc
    }

    return [...acc, {name: member.name, value: Boolean(member.field.value)}]
  }, [])
  const totalCount = booleanFieldProgress.length
  const completeCount = booleanFieldProgress.filter((field) => field.value).length
  const isComplete = totalCount > 0 && completeCount === totalCount

  return (
    <Stack gap={4}>
      <Card tone={isComplete ? `positive` : `transparent`} border padding={3} radius={2}>
        <Flex align="center" gap={3}>
          <Box>
            {completeCount} / {totalCount} Tasks Complete
          </Box>
        </Flex>
      </Card>
      {/* Render the default form */}
      {props.renderDefault(props)}
    </Stack>
  )
}

Unlike other guides in this series, which customize individual fields and array items, this component decorates the input for the whole document form.

The props it receives include the field members that make up the form. The component checks every boolean field and creates an array of their names and whether each value is currently true.

The component also loads from a different location, the studio configuration file:

// ./sanity.config.tsx

import {defineConfig, type ObjectInputProps} from 'sanity'
import {preflightType} from './schema/preflight/preflightType'
import {Progress} from './schema/preflight/Progress'

export default defineConfig({
  // ...all other settings
  schema: {
    types: [
      // ...your other schema types
      preflightType,
    ],
  },
  form: {
    components: {
      input: (props) => {
        if (
          props.id === 'root' &&
          props.schemaType.type?.name === 'document' &&
          props.schemaType.name === 'preflight'
        ) {
          return <Progress {...(props as ObjectInputProps)} />
        }

        return props.renderDefault(props)
      },
    },
  },
})

The Progress component only loads when the root of the form renders, the schema type is preflight, and its base type is document. In this case, Sanity Studio treats the whole document form as an “input component.”

Open a preflight document now and try changing a few boolean fields. A summary of your progress is now displayed at the top of the form. It goes green once all boolean fields are completed. Best of all, the counts stay correct even if you add or remove boolean fields in the document schema.

A normal document form with a component rendered at the top

You can improve this further with a circular progress indicator.

Install React Circular Progressbar and @sanity/color in your studio:

npm install react-circular-progressbar @sanity/color

Update the Progress component to use CircularProgressbarWithChildren:

// ./schema/preflight/Progress.tsx

import {ObjectInputProps, ObjectMember, TextWithTone} from 'sanity'
import {Flex, Card, Box, Stack} from '@sanity/ui'
import {hues} from '@sanity/color'
import {CircularProgressbarWithChildren} from 'react-circular-progressbar'
import 'react-circular-progressbar/dist/styles.css'

interface ProgressProps extends ObjectInputProps {
  members: ObjectMember[]
}

type FieldProgress = {
  name: string
  value: boolean
}

export function Progress(props: ProgressProps) {
  const {members} = props
  const booleanFieldProgress = members.reduce<FieldProgress[]>((acc, member) => {
    const isFieldMember = member.kind === 'field' && member.field.schemaType.name === 'boolean'

    if (!isFieldMember) {
      return acc
    }

    return [...acc, {name: member.name, value: Boolean(member.field.value)}]
  }, [])
  const totalCount = booleanFieldProgress.length
  const completeCount = booleanFieldProgress.filter((field) => field.value).length
  const isComplete = totalCount > 0 && completeCount === totalCount
  const percentage = totalCount ? Math.round((completeCount / totalCount) * 100) : 0

  return (
    <Stack gap={4}>
      <Card tone={isComplete ? `positive` : `transparent`} border padding={3} radius={2}>
        <Flex align="center" gap={3}>
          <Box style={{maxWidth: 70}}>
            <CircularProgressbarWithChildren
              value={percentage}
              styles={{
                path: {stroke: hues.green[500].hex},
                trail: {stroke: hues.gray[100].hex},
                text: {fill: hues.green[500].hex},
              }}
            >
              <TextWithTone tone={isComplete ? `positive` : `default`} size={2} weight="semibold">
                {percentage}%
              </TextWithTone>
            </CircularProgressbarWithChildren>
          </Box>
          <Box>
            {completeCount} / {totalCount} Tasks Complete
          </Box>
        </Flex>
      </Card>
      {/* Render the default form */}
      {props.renderDefault(props)}
    </Stack>
  )
}

Notice the imports include hues from @sanity/color so that this third-party component can still be styled to look like a consistently designed part of the Studio UI.

The document form now shows a Sanity color-compliant progress indicator!

You now have a Progress component that decorates the root input of every preflight document and shows how many of its boolean fields are complete.

Next steps

  • Add a React confetti package to shower your author with celebratory praise when a document reaches completion.
  • Call a third-party API to retrieve and display additional information based on values in the form.
  • Use an image generation package like Satori to generate an image based on values in the document.
  • Explore other ways to customize the form in the Form components API reference.

Was this page helpful?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme