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.
This developer guide was contributed by Saskia Bobinska (Senior Support Engineer).
Learn how to reduce the height and add a character counter based on a maximum character length set in the schema.
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` optionRule.max() on a Portable Text field limits the number of blocks, not characters, and reports Must have at most 280 blocks. To limit characters, the overview type uses a custom rule that measures the plain-text length, and stores the limit in options.maxLength so the input component can display it.
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;
}
`If the field only ever holds one line of text, Sanity Studio v3.93.0 and later has a built-in alternative to the CSS height override: set options: {oneLine: true} on the block array member. The editor then blocks line breaks and sizes itself to a single line.
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?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?