> For the complete documentation index, see [llms.txt](/llms.txt).
> The full corpus is at [llms-full.txt](/llms-full.txt).

# Getting started

> Install the Portable Text Editor and build your first block content editing experience.

import {
  CardGrid,
  LinkCard,
  Steps,
  TabItem,
  Tabs,
} from '@astrojs/starlight/components'
import {PackageManagers} from 'starlight-package-managers'

This guide walks you through installing and configuring the Portable Text Editor. By the end, you'll have a working block content editor with custom styles, decorators, and a toolbar.

:::tip[Just need to render Portable Text?]
If you already have Portable Text content and want to display it, see [Render Portable Text](/rendering/) instead.
:::

You'll need to:

- Create a schema that defines your content elements.
- Create a toolbar to toggle and insert these elements.
- Set up rendering for your text blocks and inline formatting, like bold and italic.
- Add bulleted and numbered lists.
- Render the editor.

## Parts of the editor

Before starting, it helps to understand the components that make up the editor.

- **Schema:** Describes the type of content the editor accepts. Think of this as the foundation for configuring the editor.
- **`EditorProvider`:** Supplies the schema and initial state to the editor.
- **`EventListenerPlugin`:** Listens to events emitted by the editor. Commonly used to update application state.
- **Node registrations:** Own how each content element renders, mounted through `NodePlugin`. [Rendering](/editor/concepts/rendering/) covers the model.
- **Toolbars:** UI elements that interact with the editor.
- **`PortableTextEditable`:** The core editor component. Hosts the editable surface and manages behavior.

## Add the library to your project

Start by installing the editor (it requires React 19.2.8 or later):

<PackageManagers pkg="@portabletext/editor" />

Next, import the components and types you'll need:

```tsx
// App.tsx
import {
  defineDecorator,
  defineSchema,
  defineTextBlock,
  EditorProvider,
  PortableTextEditable,
} from '@portabletext/editor'
import type {PortableTextBlock} from '@portabletext/editor'
import {EventListenerPlugin, NodePlugin} from '@portabletext/editor/plugins'
```

You won't need all of these right away, but you can add them now.

## Define your schema

Before you can render the editor, you need a schema. The editor schema configures the types of content rendered by the editor.

Start with a schema that includes some common formatting elements.

:::note
This guide includes a limited set of schema types to get you started. See the [rendering guide](/editor/guides/custom-rendering/) for mark examples, and the [custom blocks guide](/editor/guides/custom-blocks/) for block and inline objects.
:::

```tsx
// App.tsx
// ...
const schemaDefinition = defineSchema({
  // Decorators are simple marks that don't hold any data
  decorators: [{name: 'strong'}, {name: 'em'}, {name: 'underline'}],
  // Styles apply to entire text blocks
  // There's always a 'normal' style that can be considered the paragraph style
  styles: [
    {name: 'normal'},
    {name: 'h1'},
    {name: 'h2'},
    {name: 'h3'},
    {name: 'blockquote'},
  ],

  // The types below are left empty for this example.
  // See the rendering guide to learn more about each type.

  // Annotations are more complex marks that can hold data (for example, hyperlinks).
  annotations: [],
  // Lists apply to entire text blocks as well (for example, bullet, numbered).
  // You'll add them in the "Add lists" step.
  lists: [],
  // Inline objects hold arbitrary data that can be inserted into the text (for example, custom emoji).
  inlineObjects: [],
  // Block objects hold arbitrary data that live side-by-side with text blocks (for example, images, code blocks, and tables).
  blockObjects: [],
})
```

<LinkCard
  title="Schema and concepts"
  description="Learn more about schemas and the editor."
  href="/editor/concepts/portabletext/"
/>

## Render the editor

With a schema defined, you have enough to render the editor. It won't do much yet, but you can confirm your progress.

Add `react` and `useState`, then scaffold out a basic application component:

```tsx
// app.tsx
import {
  defineDecorator,
  defineSchema,
  defineTextBlock,
  EditorProvider,
  PortableTextEditable,
} from '@portabletext/editor'
import type {PortableTextBlock} from '@portabletext/editor'
import {EventListenerPlugin, NodePlugin} from '@portabletext/editor/plugins'
import {useState} from 'react'

const schemaDefinition = defineSchema({
  /* your schema from the previous step */
})

function App() {
  // Set up the initial state getter and setter. Leave the starting value as undefined for now.
  const [value, setValue] = useState<Array<PortableTextBlock> | undefined>(
    undefined,
  )

  return (
    <>
      <EditorProvider
        initialConfig={{
          schemaDefinition,
          initialValue: value,
        }}
      >
        <EventListenerPlugin
          on={(event) => {
            if (event.type === 'mutation') {
              setValue(event.value)
            }
          }}
        />
        <PortableTextEditable
          // Add an optional style to see it more easily on the page
          style={{border: '1px solid black', padding: '0.5em'}}
        />
      </EditorProvider>
    </>
  )
}

export default App
```

Include the `App` component in your application and run it. You should see an outlined editor that accepts text, but doesn't do much else.

## Set up rendering for schema elements

At this point the editor renders every text block as plain text, whatever its style. Fix that by registering a `defineTextBlock` node for the text blocks and `defineDecorator` nodes for the marks. The [Rendering](/editor/concepts/rendering/) page covers the model these registrations share.

If you're maintaining an editor that rendered through the `renderStyle`, `renderBlock`, `renderListItem`, `renderDecorator`, and `renderAnnotation` props, removed in this major, see the [migration guide](/editor/guides/migrate-render-props/) to move to node registrations instead of following this section from scratch.

Start by registering the text block render with `defineTextBlock`. The editor dispatches every text block to this callback. Your callback owns the block's wrapper element, so spread `props.attributes` on the outermost element you return, and use the block's `style` to pick the element.

```tsx
const textBlock = defineTextBlock({
  type: 'block',
  render: (props) => {
    if (props.node.style === 'h1') {
      return <h1 {...props.attributes}>{props.children}</h1>
    }
    if (props.node.style === 'h2') {
      return <h2 {...props.attributes}>{props.children}</h2>
    }
    if (props.node.style === 'h3') {
      return <h3 {...props.attributes}>{props.children}</h3>
    }
    if (props.node.style === 'blockquote') {
      return <blockquote {...props.attributes}>{props.children}</blockquote>
    }
    return <div {...props.attributes}>{props.children}</div>
  },
})
```

Marks (decorators and annotations) join the same `nodes` array. Registrations all follow the same shape:

- They take in props and return JSX elements.
- They decide what to render from the registration's `type` and the node itself.
- They return JSX that renders `children` somewhere inside it, the editable content the registration wraps.

With this in mind, continue for the remaining schema types.

Register a decorator with `defineDecorator`, one per decorator name:

```tsx
const strong = defineDecorator({
  type: 'strong',
  render: ({children}) => <strong>{children}</strong>,
})
const em = defineDecorator({
  type: 'em',
  render: ({children}) => <em>{children}</em>,
})
const underline = defineDecorator({
  type: 'underline',
  render: ({children}) => <u>{children}</u>,
})

const nodes = [textBlock, strong, em, underline]
```

:::note
By default, text is rendered as an inline `span` element in the editor. A decorator's render can pass `children` through unwrapped, but the registered text block render must return a block-level element, like a `<div>`.
:::

Mount every registration through one `NodePlugin`, inside the `EditorProvider`. Keep the `nodes` array itself at module scope, as above: a fresh array on every render would make `NodePlugin` unregister and re-register on every keystroke.

```tsx
<>
  <NodePlugin nodes={nodes} />
  <PortableTextEditable style={{border: '1px solid black', padding: '0.5em'}} />
</>
```

Before you can see if anything changed, you need a way to interact with the editor.

## Create a toolbar

A toolbar is a collection of UI elements for interacting with the editor. The `@portabletext/toolbar` library exposes hooks and types that allow you to create a toolbar however you like. The `@portabletext/keyboard-shortcuts` library provides drop-in shortcut access to link toolbar buttons to key commands.

Building a custom toolbar differs with each project, but in this example:

1. Add the `@portabletext/toolbar` and `@portabletext/keyboard-shortcuts` libraries to your project.
2. Create a `Toolbar` component, along with any sub-components in the same file.
3. Configure `useToolbarSchema` to access the editor schema, then loop over the schema types to create buttons for each style and decorator.
4. Enhance the schema with any icons, labels, or descriptions you want to display in the toolbar.
5. Create buttons for each schema group (styles, decorators, annotations, etc.).
6. Add the `Toolbar` to your render function inside the `EditorProvider`.

<PackageManagers pkg="@portabletext/toolbar @portabletext/keyboard-shortcuts" />

This example shows a minimal toolbar:

```tsx
// App.tsx
// ...
import {bold} from '@portabletext/keyboard-shortcuts'
import {
  useDecoratorButton,
  useStyleSelector,
  useToolbarSchema,
  type ExtendDecoratorSchemaType,
  type ExtendStyleSchemaType,
  type ToolbarDecoratorSchemaType,
  type ToolbarStyleSchemaType,
} from '@portabletext/toolbar'

function Toolbar() {
  // useToolbarSchema provides access to the PTE schema
  // optionally, pass in updated schemas to override the default
  const toolbarSchema = useToolbarSchema({
    extendDecorator, // see declarations below
    extendStyle, // see declarations below
  })

  return (
    <div>
      {toolbarSchema.decorators?.map((decorator) => (
        <DecoratorButton key={decorator.name} schemaType={decorator} />
      ))}
      {toolbarSchema.styles ? (
        <StyleButtons schemaTypes={toolbarSchema.styles} />
      ) : null}
    </div>
  )
}
// Extend the schema with icons, titles, and keyboard shortcuts

const extendStyle: ExtendStyleSchemaType = (style) => {
  // Apply updates to the schema, if needed
  if (style.name === 'h1') {
    return {
      ...style,
      title: 'Title',
    }
  }
  // ...repeat for each style type, or return the original style
  return style
}
const extendDecorator: ExtendDecoratorSchemaType = (decorator) => {
  if (decorator.name === 'strong') {
    return {
      ...decorator,
      // Optional: add a react component as an icon and unset the title
      icon: () => <strong>B</strong>,
      // Optional: connect to a keyboard shortcut from the keyboard-shortcuts library
      shortcut: bold,
      title: '',
    }
  }
  // ...repeat for each decorator type, or return the original decorator
  return decorator
}

// Create a button for each decorator type
const DecoratorButton = (props: {schemaType: ToolbarDecoratorSchemaType}) => {
  const decoratorButton = useDecoratorButton(props)
  return (
    <button
      type="button"
      onClick={() => decoratorButton.send({type: 'toggle'})}
      className={
        decoratorButton.snapshot.matches({enabled: 'active'}) ? 'active' : ''
      }
    >
      {props.schemaType.icon ? <props.schemaType.icon /> : null}
      {props.schemaType.title}
    </button>
  )
}
// One `useStyleSelector` drives all style buttons: a block has one style at
// a time, so the hook is a selector, not a per-button toggle
function StyleButtons(props: {
  schemaTypes: ReadonlyArray<ToolbarStyleSchemaType>
}) {
  const styleSelector = useStyleSelector(props)
  const activeStyle = styleSelector.snapshot.context.activeStyle ?? 'normal'
  return props.schemaTypes.map((schemaType) => (
    <button
      key={schemaType.name}
      type="button"
      onClick={() =>
        styleSelector.send({type: 'toggle', style: schemaType.name})
      }
      className={activeStyle === schemaType.name ? 'active' : ''}
    >
      {schemaType.icon ? <schemaType.icon /> : null}
      {schemaType.title}
    </button>
  ))
}
// ... and so on for each schema type, or create a generic button
```

The `useStyleSelector` and `useDecoratorButton` hooks give you access to the active editor. `send` lets you send events to the editor, and `snapshot` lets you read the current state of the editor.

<LinkCard
  title="Customize the toolbar"
  description="Learn more about building custom toolbars."
  href="/editor/guides/customize-toolbar/"
/>

## Add lists

A list in Portable Text is a run of text blocks that each carry a list type (`listItem`) and an indentation `level`. The editor doesn't draw bullets or numbers. Your text block render does, with help from `@portabletext/plugin-list-index`, which tells each list item its position in its list.

<PackageManagers pkg="@portabletext/plugin-list-index" />

Add the list types to your schema:

```tsx
const schemaDefinition = defineSchema({
  // ...
  lists: [
    {name: 'bullet', title: 'Bulleted list'},
    {name: 'number', title: 'Numbered list'},
  ],
})
```

Then update the text block render. It reads the list position with `useListIndex`, which is a React hook, so the render returns a `TextBlock` component that calls it. The component puts the list type, level, and position on the wrapper as `data-*` attributes, and the style element moves inside the wrapper, since a list item can have a style too:

```tsx
import type {TextBlockRenderProps} from '@portabletext/editor'
import {ListIndexProvider, useListIndex} from '@portabletext/plugin-list-index'

const textBlock = defineTextBlock({
  type: 'block',
  render: (props) => <TextBlock {...props} />,
})

function TextBlock(props: TextBlockRenderProps) {
  // The 1-based position within the list, or `undefined` for a block that
  // is not a list item
  const listIndex = useListIndex(props.path)

  let children = props.children
  if (props.node.style === 'h1') {
    children = <h1>{children}</h1>
  } else if (props.node.style === 'h2') {
    children = <h2>{children}</h2>
  } else if (props.node.style === 'h3') {
    children = <h3>{children}</h3>
  } else if (props.node.style === 'blockquote') {
    children = <blockquote>{children}</blockquote>
  }

  return (
    <div
      {...props.attributes}
      data-list-item={props.node.listItem}
      data-level={props.node.level}
      data-list-index={listIndex}
    >
      {children}
    </div>
  )
}
```

React leaves out attributes whose value is `undefined`, so blocks that are not list items render without them.

With the attributes in place, a few lines of CSS draw the markers and indent nested levels:

```css
[data-list-item] {
  display: flex;
  gap: 0.5rem;
}

[data-list-item='bullet']::before {
  content: '•';
}

[data-list-item='number']::before {
  content: attr(data-list-index) '.';
}

[data-level='2'] {
  padding-left: 1.5em;
}

[data-level='3'] {
  padding-left: 3em;
}
```

Finally, add a button per list type to the toolbar with `useListButton`:

```tsx
import {useListButton, type ToolbarListSchemaType} from '@portabletext/toolbar'

const ListButton = (props: {schemaType: ToolbarListSchemaType}) => {
  const listButton = useListButton(props)
  return (
    <button
      type="button"
      onClick={() => listButton.send({type: 'toggle'})}
      className={
        listButton.snapshot.matches({enabled: 'active'}) ? 'active' : ''
      }
    >
      {props.schemaType.title}
    </button>
  )
}
```

Render one per list type inside `Toolbar`, next to the other buttons:

```tsx
function Toolbar() {
  // ...
  return (
    <div>
      {/* ...decorator and style buttons */}
      {toolbarSchema.lists.map((list) => (
        <ListButton key={list.name} schemaType={list} />
      ))}
    </div>
  )
}
```

In a list item, Tab indents the item one level and Shift+Tab outdents it.

<LinkCard
  title="Render lists"
  description="Number each level in its own style, render markers in React, and handle lists inside containers."
  href="/editor/guides/render-lists/"
/>

In the next step, you'll add the toolbar and the list position provider to the editor.

## Bring it all together

With the registrations created and a toolbar in place, you can fully render the editor. Add the `Toolbar` inside the `EditorProvider`, and wrap `PortableTextEditable` in `ListIndexProvider` so the text block render can read list positions.

```tsx
// App.tsx
// ...
function App() {
  const [value, setValue] = useState<Array<PortableTextBlock> | undefined>(
    undefined,
  )

  return (
    <>
      <EditorProvider
        initialConfig={{
          schemaDefinition,
          initialValue: value,
        }}
      >
        <EventListenerPlugin
          on={(event) => {
            if (event.type === 'mutation') {
              setValue(event.value)
            }
          }}
        />
        <NodePlugin nodes={nodes} />
        <Toolbar />
        <ListIndexProvider>
          <PortableTextEditable
            style={{border: '1px solid black', padding: '0.5em'}}
          />
        </ListIndexProvider>
      </EditorProvider>
    </>
  )
}
// ...
```

You can now enter text and interact with the toolbar buttons to toggle the styles, decorators, and lists. These are only a small portion of the types of things you can do. Check out the [custom rendering guide](/editor/guides/custom-rendering/) and the [toolbar customization guide](/editor/guides/customize-toolbar/) for options.

## View the Portable Text data

You can preview the Portable Text from the editor by reading the state. Add the following after the `EditorProvider`:

```tsx
<pre style={{border: '1px dashed black', padding: '0.5em'}}>
  {JSON.stringify(value, null, 2)}
</pre>
```

This displays the raw Portable Text. To customize how Portable Text renders in your apps, explore the serializers.

<LinkCard
  title="Render Portable Text"
  description="Use a serializer to render Portable Text in your app"
  href="/rendering/"
/>

## Behavior API

The Behavior API lets you customize how users interact with the editor by hooking into events:

- Declaratively hook into editor **events** and define new behaviors.
- Imperatively trigger **events**.
- Derive editor **state** using **pure functions**.
- Subscribe to **emitted** editor **events**.

Learn more about [behaviors](/editor/concepts/behavior/) and how to [create your own](/editor/guides/create-behavior/).

## Next steps

<CardGrid>
  <LinkCard
    title="Customize rendering"
    description="Span-level render props and additional node registrations."
    href="/editor/guides/custom-rendering/"
  />
  <LinkCard
    title="Nest block content with containers"
    description="Callouts, code blocks, and tables that carry editable block content."
    href="/editor/concepts/containers/"
  />
  <LinkCard
    title="Further customize the toolbar"
    description="Dive deeper into toolbar customization."
    href="/editor/guides/customize-toolbar/"
  />
  <LinkCard
    title="Create your own behaviors"
    description="Intercept editor events and add functionality with the Behavior API."
    href="/editor/guides/create-behavior/"
  />
</CardGrid>