Change the height of a Portable Text Editor (PTE) using a custom input component

Reduce the height of a Portable Text Editor and add a character counter with a custom input component.

Learn how to reduce the height and add a character counter based on a maximum character length set in the schema.

A Portable Text Editor with a reduced height

Sometimes, you need a Portable Text Editor (PTE) but want it to take up less space in your document form.

In this example, the PTE can only use decorators, and you need to keep track of the character count. Custom input components make this possible.

Create the schema type

The overview type uses the CharacterCountInputPTE input component, which you create in "Create the custom input component."

import { toPlainText } from 'next-sanity'
import { defineArrayMember, defineType, type PortableTextBlock } from 'sanity'
import { CharacterCountInputPTE } from '../components/CharacterCountInputPTE'

/** ## `overview` Type - reduced Portable Text
 *
 * The height of the input is reduced to 2 lines.
 *
 * @name overview
 * @type {PortableTextBlock[]}
 * @validation {Rule} - Required, max 280 characters (set with `options.maxLength`)
 * @description Used both for the <meta> description tag for SEO, and the personal website subheader.
 *
 * ### Blocks
 * - **Decorators**: `em`, `strong`
 * - **Annotations**: none
 * - **Styles**: none
 * - **Lists**: none
 *
 *
 */
export default defineType({
  name: 'overview',
  description: 'Short and on point – max. 280',
  title: 'Meta & SEO Description',
  type: 'array',
  // The character limit, read by the validation rule and the input component.
  // Override it on a field by setting `options: {maxLength: 200}`.
  options: { maxLength: 280 },
  // Rule.max() on a Portable Text array counts blocks, not characters,
  // so a custom rule measures the plain-text length instead.
  validation: (Rule) =>
    Rule.required().custom((value, context) => {
      const { maxLength } = (context.type?.options ?? {}) as { maxLength?: number }
      if (!value || !maxLength) return true
      const length = toPlainText(value as PortableTextBlock[]).length
      return length <= maxLength || `Must be at most ${maxLength} characters (currently ${length})`
    }),
  components: {
    input: CharacterCountInputPTE,
  },
  of: [
    // Paragraphs
    defineArrayMember({
      lists: [],
      marks: {
        annotations: [],
        decorators: [
          {
            title: 'Italic',
            value: 'em',
          },
          {
            title: 'Strong',
            value: 'strong',
          },
        ],
      },
      styles: [],
      type: 'block',
    }),
  ],
}, { strict: false }) // allows the custom `maxLength` option

Use the type in a field

// * * * Title * * *
    defineField({
      name: 'title',
      type: 'overview',
      description: 'This will be used as the H2 of the Sections. Short and on point – max. 200',
      // overrides the 280-character limit set on the overview type.
      // Don't set `validation` here: it replaces the type's character-limit rule.
      options: { maxLength: 200 },
    }, { strict: false }),

Create the custom input component

The input component reads the maxLength value from the field's options and shows editors how many characters they've used.

The component also wraps the output of renderDefault in a container that sets the height of this specific PTE while keeping it resizable, using styled-components. The selector targets the editor's data-testid attribute, which isn't a public API and can change between Studio releases.

initialActive set to true lets editors focus the PTE and start writing without activating it first. In Sanity Studio v3.83.0 and later, this is the default behavior, so the setting only matters on earlier versions.

import { Stack, Text } from '@sanity/ui'
import { toPlainText } from 'next-sanity'
import type { ArrayOfObjectsInputProps, PortableTextInputProps } from 'sanity'
import styled from 'styled-components'

// `components.input` on an array type expects ArrayOfObjectsInputProps.
// A Portable Text field receives PortableTextInputProps at runtime, so cast inside.
export function CharacterCountInputPTE(props: ArrayOfObjectsInputProps) {
  const ptProps = props as unknown as PortableTextInputProps
  const characters = ptProps.value ? toPlainText(ptProps.value).length : 0

  // the character limit set with `options.maxLength` on the type or the field
  const { maxLength: max } = (props.schemaType.options ?? {}) as { maxLength?: number }

  return (
    <Stack gap={3}>
      <Container id={'PTE-height-container'}>
        {ptProps.renderDefault({
          ...ptProps,
          // remove the need to activate the PTE 
          initialActive: true,
        })}
      </Container>
      <Text muted align={'right'} size={1}>
        Characters: {characters}
        {max ? ` / ${max}` : ''}
      </Text>
    </Stack>
  )
}
// add a specific height to the PTE without losing the ability to resize it
// `data-testid` is not a public API and can change between Studio releases
const Container = styled.div`
  [data-testid='pt-editor'][data-fullscreen='false'] {
    height: 100px;
  }
`

You now have a compact PTE that shows a character count and enforces a character limit. To customize the editor further, see Customize the Portable Text Editor and Form components.

Was this page helpful?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme