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

Add things to Portable Text

Next

Create your own Sanity template

Was this page helpful?

On this page

  • The schema
  • The field
  • The custom input component
Developer guidesLast updated September 24, 2025

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

Ever used a PTE and thought you would like it to take up less space and be focusable without activating it? Now you can!

This developer guide was contributed by Saskia Bobinska (Senior Support Engineer).

Portable Text Editors are a fantastic tool!

Learn how to reduce the height and add a character counter based on the max length set in the field validation.

Loading...
A Portable text editor with a reduced height

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

In my example, we have a PTE that can only use decorators and annotations, and we need to keep track of the character count. Custom input components make this possible.

The schema

// schemas/portableText/overview.tsx

import { CharacterCountInputPTE } from '@/sanity/components/inputs/CharacterCount'
import { defineArrayMember, defineType, PortableTextBlock } from 'sanity'

/** ## `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
 * @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',
  // You can override the max values from the schema by setting a validation on the field
  validation: (Rule) => Rule.required().max(280),
  components: {
    input: CharacterCountInputPTE,
  },
  of: [
    // Paragraphs
    defineArrayMember({
      lists: [],
      marks: {
        annotations: [],
        decorators: [
          {
            title: 'Italic',
            value: 'em',
          },
          {
            title: 'Strong',
            value: 'strong',
          },
        ],
      },
      styles: [],
      type: 'block',
    }),
  ],
})

The field

// * * * Title * * *
    defineField({
      name: 'title',
      type: 'overview',
      description: 'This will be used as the H2 of the Sections. Short and on point – max. 200',
      // setting a validation on the field will override the validation on the overview schema
      validation: (Rule) => Rule.required().max(200),
    }),

The custom input component

We use the max values set on the field schema definition (validation) and then use them in the component to show users how many characters they already used.

Then we also wrap renderDefault in a container that we use to change the height of this specific PTE, making sure it's resizable, using styled-components.

initialActive set to true allows editors to just focus on the PTE and start writing without the need to activate it.

// CharacterCountInputPTE.tsx

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

export function CharacterCountInputPTE(props: PortableTextInputProps) {
  // check if validations exist
  // @ts-ignore
  const validationRules = props.schemaType.validation[0]._rules || []
  const characters = props.value ? toPlainText(props.value).length : 0

  //check if max Character validation exists and get the value
  const max = validationRules
    .filter((rule) => rule.flag === 'max')
    .map((rule) => rule.constraint)[0]
  
  return (
    <Stack space={3}>
      <Container id={'PTE-height-container'}>
        {props.renderDefault({
          ...props,
          // 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
const Container = styled.div`
  [data-testid='pt-editor'][data-fullscreen='false'] {
    height: 100px;
  }
`

And that's it! 🥳

A Portable text editor with a reduced height
// schemas/portableText/overview.tsx

import { CharacterCountInputPTE } from '@/sanity/components/inputs/CharacterCount'
import { defineArrayMember, defineType, PortableTextBlock } from 'sanity'

/** ## `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
 * @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',
  // You can override the max values from the schema by setting a validation on the field
  validation: (Rule) => Rule.required().max(280),
  components: {
    input: CharacterCountInputPTE,
  },
  of: [
    // Paragraphs
    defineArrayMember({
      lists: [],
      marks: {
        annotations: [],
        decorators: [
          {
            title: 'Italic',
            value: 'em',
          },
          {
            title: 'Strong',
            value: 'strong',
          },
        ],
      },
      styles: [],
      type: 'block',
    }),
  ],
})
// * * * Title * * *
    defineField({
      name: 'title',
      type: 'overview',
      description: 'This will be used as the H2 of the Sections. Short and on point – max. 200',
      // setting a validation on the field will override the validation on the overview schema
      validation: (Rule) => Rule.required().max(200),
    }),
// CharacterCountInputPTE.tsx

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

export function CharacterCountInputPTE(props: PortableTextInputProps) {
  // check if validations exist
  // @ts-ignore
  const validationRules = props.schemaType.validation[0]._rules || []
  const characters = props.value ? toPlainText(props.value).length : 0

  //check if max Character validation exists and get the value
  const max = validationRules
    .filter((rule) => rule.flag === 'max')
    .map((rule) => rule.constraint)[0]
  
  return (
    <Stack space={3}>
      <Container id={'PTE-height-container'}>
        {props.renderDefault({
          ...props,
          // 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
const Container = styled.div`
  [data-testid='pt-editor'][data-fullscreen='false'] {
    height: 100px;
  }
`