Sanity logosanity.ioAll Systems Operational© Sanity 2026
Change Site Theme
Sanity logo

Documentation

    • Overview
    • Platform introduction
    • Next.js quickstart
    • Nuxt.js quickstart
    • Astro quickstart
    • React Router quickstart
    • Studio quickstart
    • Build with AI
    • Content Lake
    • Functions
    • APIs and SDKs
    • Agent Actions
    • Visual Editing
    • Blueprints
    • Platform management
    • Dashboard
    • Studio
    • Canvas
    • Media Library
    • App SDK
    • Content Agent
    • HTTP API
    • CLI
    • Libraries
    • Specifications
    • Changelog
    • User guides
    • Developer guides
    • Courses and certifications
    • Join the community
    • Templates
Developer guides
Overview

  • Develop with AI

    Best practices

  • Query optimization

    Paginating with GROQ
    High performance GROQ

  • Roles and authentication

    Setting up Single Sign-On with SAML
    Third-Party Login (Deprecated)
    OAuth2
    Set up SSO authentication with SAML and Azure/Entra ID
    Set up SSO authentication with SAML and PingIdentity
    Set up SSO authentication with SAML and JumpCloud
    Reconcile users against internal systems
    Restrict Access to Specific Documents
    Setting up a Default Relay State for IdP Initiated - SAML Logins

  • Structured content

    Scalable navigation patterns
    An opinionated guide to Sanity Studio
    Browsing Content How You Want with Structure Builder
    Deciding on fields and relationships
    Create richer array item previews
    Dynamic folder structure using the currentUser and workflow states
    Create a time duration object field
    Level up Your Edit Modal with Next/Previous Navigation Buttons for Array Items
    Create a “coupon generator” string field input
    Managing redirects with Sanity
    Create a document form progress component
    Create an array input field with selectable templates
    Creating a Parent/Child Taxonomy
    Create interactive array items for featured elements
    Create a visual string selector field input
    Create a survey rating number field input
    How to use structured content for page building
    Create a recycling bin for deleted documents via Sanity Functions

  • Frontend integration

    Add live content to your application
    Forms with Sanity
    Vercel Integration
    Build your blog with Astro and Sanity
    How to implement front-end search with Sanity

  • Ecommerce

    Displaying Sanity content in Shopify
    Sanity Connect for Shopify
    Custom sync handlers for Sanity Connect

  • Integrating with other services

    A/B testing with Sanity and Growthbook
    Cookie consent integrations with Sanity
    Integrating external data sources with Sanity
    Klaviyo (email campaigns)
    Developing with Next.js on GitHub Codespaces

  • Adopting Sanity

    How to pitch Sanity.io to your team
    Convincing your clients to go with Sanity.io, rather than a traditional CMS
    Not-profit plan
    Agencies: Navigating the Spring 2025 Organization Changes
    How to generate massive amounts of demo content for Sanity
    How to implement Multi-tenancy with Sanity

  • GROQ

    GROQ-Powered Webhooks – Intro to Filters
    GROQ-Powered Webhooks – Intro to Projections

  • Portable Text

    Presenting Portable Text
    Add Inline blocks for the Portable Text Editor
    Beginners guide to Portable Text
    How to add custom YouTube blocks to Portable Text
    Converting Inline Styles to Sanity Block Decorators
    Add things to Portable Text
    Change the height of the PTE

  • Community and ecosystem

    Create your own Sanity template
    Community guides
    Community Code of Conduct
    Contribute to the ecosystem

  • Plugin development

    Migrating plugins to support Content Releases

On this page

Previous

Managing redirects with Sanity

Next

Create an array input field with selectable templates

Was this page helpful?

On this page

  • What you need to know:
  • Custom form components by example
  • What you’ll learn
  • Schema preparation
  • Customizing the document form
  • Next steps
Developer guidesLast updated September 24, 2025

Create a document form progress component

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

This developer guide was contributed by Simeon Griggs (Principal Educator).

Summarise 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.

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.

  • Create a “coupon generator” string field input
  • Create a visual string selector field input
  • Create a survey rating number field input
  • Create a time duration object field
  • Create an array input field with selectable templates
  • Create interactive array items for featured elements
  • Create richer array item previews
  • Create a document form progress component

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

Schema preparation

The imaginary scenario is that your Studio contains preflight documents which 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.ts:

// ./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.

Loading...
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. Now you can make this a truly excellent editing experience.

Customizing 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 = completeCount === totalCount

  return (
		<Stack space={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 where the component is decorating or replacing a built-in part of the Studio – this component will receive props and be rendered on its own.

The props it receives will be the field members that make up the form. In the component you’ll check for every boolean type field, and create array of just their names and whether they’re currently to true or falsy.

The component will also be loaded from a different location, as demonstrated below:

// ./sanity.config.tsx

import {defineConfig, isObjectInputProps} from 'sanity'
import {Stack} from '@sanity/ui'
import {Progress} from './schema/preflight/Progress'

export default defineConfig({
  // ...all other settings
  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)
      },
    },
  },
})

Notice how you’ll only load the Progress component if the root of the form is being rendered, and only on the preflight schema type and it’s the document component. Yes, in this case the 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 fields are completed. Best of all, the counts will be correct even if boolean fields are added or removed from the document schema.

Loading...
A normal document form with a component rendered at the top

This is good, but we can do even better.

Install React Circular Progressbar to your Studio:

npm install react-circular-progressbar
pnpm add react-circular-progressbar
yarn add react-circular-progressbar
bun add react-circular-progressbar

Now update your component to use the component.

// ./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 = completeCount === totalCount
  const percentage = Math.round((completeCount / totalCount) * 100)

  return (
    <Stack space={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 3rd party component can still be styled to look like a consistently designed part of the Studio UI.

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

Job done!

Next steps

  • Add a React confetti package to shower your author with celebratory praise when a document reaches completion.
  • Imagine how this might be used to call a 3rd party API to retrieve and display additional information based on values in the form.
  • Other ideas include using an image generation package like Satori to generate an image based on values in the document.
// ./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'}),
  ],
})
A standard document with default boolean fields
// ./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 = completeCount === totalCount

  return (
		<Stack space={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>
  )
}
// ./sanity.config.tsx

import {defineConfig, isObjectInputProps} from 'sanity'
import {Stack} from '@sanity/ui'
import {Progress} from './schema/preflight/Progress'

export default defineConfig({
  // ...all other settings
  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)
      },
    },
  },
})
A normal document form with a component rendered at the top
npm install react-circular-progressbar
pnpm add react-circular-progressbar
yarn add react-circular-progressbar
bun add react-circular-progressbar
// ./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 = completeCount === totalCount
  const percentage = Math.round((completeCount / totalCount) * 100)

  return (
    <Stack space={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>
  )
}
The document form now shows a Sanity color-compliant progress indicator!