# Portable Text, and rich text as JSON with annotations attached to spans

> Portable Text is a specification, not a library: a JSON shape for rich text that any renderer can turn into HTML, Markdown, SSML or XML. The design decision that matters is the separation of decorators from annotations, and the specification is still marked as a working draft.

**portabletext/portabletext** — Portable Text is a JSON based rich text specification for modern content editing platforms.

- Repository: https://github.com/portabletext/portabletext
- Website: https://www.portabletext.org
- Stars: 1,454 · Forks: 22
- Language: Unknown
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/portabletext-portabletext

## A specification, currently labelled a working draft

Portable Text is a JSON based rich text specification for modern content editing platforms, and the version line at the top of the specification says v0.0.1, WORKING DRAFT.

That label is the first thing to weigh, and it is the sort of honesty that helps. The specification is stable in practice because implementations exist and interoperate, and it is unstable in the sense that the document itself says the text may change.

The idea is an agnostic abstraction of rich text that can be serialized into pretty much any markup language, be it HTML, Markdown, SSML, XML and so on. It is designed to be efficient for real-time collaborative interfaces, and it makes it possible to annotate rich text with additional data structures recursively.

Those four design goals pull in different directions, and the specification resolves them in a particular way. The agnosticism means the format stores no markup, only structure, so a renderer decides the tags. The efficiency for collaborative editing means the format is small, flat and diffable, since every character of a collaborative document is transmitted on every change. The recursion means the data model is a tree of arrays and objects rather than a string with embedded tags.

The audience is the people building the editing interface and the people building the renderer, because the specification only becomes useful once both ends agree on the shape. The homepage is portabletext.org, and the licence is MIT, so the specification text itself can be reused in your own documentation without restriction.

The repository is correspondingly small. It holds a README with the specification, a CODE_OF_CONDUCT.md, the licence, two logo files and an examples directory with a single file, withLink.json. The last push was on 2026-07-15 and there are no GitHub releases.

## The shape: an array of blocks, each an array of spans

Portable Text is built on the idea of rich text as an array of blocks, themselves arrays of children spans. Each block can have a style and a set of mark definitions, which describe data structures distributed on the children spans.

The simplest form is an array of objects of a type with an array of children, and the example in the specification shows a block with some text annotated with some data.

```json
{
  "style": "normal",
  "_type": "block",
  "children": [
    {
      "_type": "span",
      "marks": ["a-key", "emphasis"],
      "text": "some text"
    }
  ],
  "markDefs": [
    {
      "_key": "a-key",
      "_type": "markType",
      "extraData": "some data"
    }
  ]
}
```

Three properties carry most of the meaning. The _type on every block and every child is mandatory, and it is what makes the format extensible, because a serializer reads the type to decide what the object means. The style typically describes a visual property for the whole block, and a heading is the same shape as a paragraph with a different style value.

```json
{
  "style": "h1",
  "_type": "block",
  "children": [
    {
      "_type": "span",
      "text": "This is a heading"
    }
  ]
}
```

And the markDefs array is where the interesting part lives, because it is where the document carries data that is not text.

## Decorators and annotations, the distinction that carries the data

Marks are how inline text is marked up with additional data or features, and they come in two forms.

Decorators are marks as simple string values, and annotations are keys to a data structure. The marks array is therefore either an array of string values, or keys corresponding to markDefs, with the same _key.

The decorator example is the familiar case.

```json
[
  {
    "_type": "span",
    "marks": [],
    "text": "This is"
  },
  {
    "_type": "span",
    "marks": ["emphasis"],
    "text": "important"
  }
]
```

The annotation case is where the data rides along with the text.

```json
[
  {
    "_type": "block",
    "children": [
      {
        "_type": "span",
        "text": "Portable Text",
        "marks": ["<markDefId>"]
      }
    ],
    "markDefs": [
      {
        "_type": "link",
        "_key": "<markDefId>",
        "href": "https://www.portabletext.org"
      }
    ]
  }
]
```

A decorator is a flag. An annotation is a reference to a structure, and the structure is stored once at the block level and referenced from the spans that carry it. The specification's own example of a link shows the shape, with the markDefId appearing in the span's marks array and the _key of the link definition, and it notes the correspondence in comments on both sides.

The reason this matters is the reference. A link with an href is not a string inside the text, it is an object with a key, and that is what lets a renderer style a link, a comment and a mention differently while a single text run carries all three. It is also what makes the data available to code: the renderer looks up the annotation by key and gets the data, rather than parsing markup out of a string.

## Lists and levels are derived from position, not stored

Two properties of a block handle structure that a word processor would store explicitly, and both are derived rather than stored.

A block can be given the property listItem with a value that describes which kind of list it is, typically bullet, number, square and so on. The list position is derived from the position the block has in the array and from the surrounding list items on the same level.

That is a design decision with a clear motivation. A nested list in a format that stores explicit indentation has to represent the relationship between items, and a document where the author drags an item across a level boundary has to be updated in several places. Deriving the position means the array is the list, and the structure of the list is a property of where the items sit.

The second property is level, which is used to express visual nesting and hierarchical structures between blocks in the array.

The distinction between the two is worth holding onto. listItem describes what kind of list the block belongs to, and it is what makes the block a list entry at all. level describes how deep the nesting goes, and it is about visual structure rather than about list membership. A renderer uses the first to decide whether to emit a list element and the second to decide how deeply to nest it.

For a collaborative editing interface, both are cheap to merge, because they are single values on a block rather than a tree that has to be restructured. That is the collaborative-friendly part of the format, and it is the part a naive HTML conversion tends to get wrong.

## Custom blocks and custom objects, and the extensibility that follows

Custom blocks are typically images, code blocks, tables, video embeds, or any data structure, and they should be given a _type.

```json
{
  "_type": "image",
  "asset": {
    "_ref": "some-asset-reference"
  }
}
```

```json
{
  "_type": "code",
  "language": "javascript",
  "code": "console.log(\"hello world\");"
}
```

The image example shows the other half of the pattern, which is the reference to an asset rather than the asset itself, so the document stays small and the binary lives in an asset pipeline.

The code example is more interesting, because a code block is content that has no inline equivalent. There is no way to express a block of code as a span with a mark, so the block itself is the custom type, and the type carries the language and the code.

The same idea extends further. Portable Text allows inserting arbitrary data objects in the array, only requiring the _type key, and it allows custom content objects in the root array, which enables editing and rendering environments to mix rich text with custom content types.

That is the extensibility mechanism in one sentence: anything with a _type can appear in the array, and a renderer that does not understand a type can skip it. That is what lets a single document hold a paragraph next to an interactive chart next to a product card, and it is why the specification does not need a plugin system to accommodate every kind of content an editor might produce.

The specification also links a live CodeSandbox example with an inline image and a link, which is the fastest way to see a mark and an annotation working together in a running renderer.

## The implementation ecosystem, and what a specification repository is not

The serialization section lists the tools that turn Portable Text into something renderable, and the list is long enough to tell you the format has been adopted rather than proposed.

For JavaScript there are packages for React, React Native, React PDF, Svelte and Vue, plus a to-html converter and a block-tools package, along with a Sanity converter to markdown and one to hyperscript, and an Astro port. For PHP there is the Sanity\BlockContent class in the sanity-php client. For Python there is portabletext-html. For C# there is the SanityHtmlBuilder class in the Sanity LINQ client and a Portable Text .NET converter that is described as a graceful port of the block-tools htmlToBlocks function to C#. And for Dart and Flutter there is a widget that supports the entire spec.

The spread across five language ecosystems is the strongest evidence about the format, more than anything in the specification text.

What the repository itself is not, though, is code. There is no implementation here. The top level holds a README that is the specification, a CODE_OF_CONDUCT.md, a LICENSE, two logo files and an examples directory containing one file, withLink.json.

So the repository is the document, and the implementations live in their own repositories under the same organisation, with a naming convention that makes the family obvious. For an adopter, that means you read the specification once, choose a renderer for your stack, and you are relying on that renderer to implement the specification faithfully rather than on this repository to do anything for you.

The specification is MIT licensed, the repository publishes no releases, and the version string in the document itself, v0.0.1, is the honest signal about how settled the text is.

## Conclusion

Adopt Portable Text as your interchange format if you are building an editing platform or moving rich text between one, because the separation of structure from presentation means one stored document can render as HTML for a website, Markdown for a repository and something else for a native app. Do not adopt it as a replacement for HTML, since it only becomes HTML through one of the renderer implementations, and do not treat it as stable, because the specification is still labelled v0.0.1, a working draft. Before you commit, read the marks section carefully, because the difference between a decorator and an annotation is the part that determines whether your data survives a round trip through another implementation.

## FAQ

### What is Portable Text?

It is a JSON based rich text specification for modern content editing platforms, an agnostic abstraction of rich text that can be serialized into HTML, Markdown, SSML, XML and other markup, designed to be efficient for real-time collaborative interfaces.

### What is the difference between a decorator and an annotation?

Decorators are marks as simple string values, such as emphasis. Annotations are keys to a data structure: the span's marks array contains a _key that points at an entry in the block's markDefs array, which is where the data such as a link href is stored.

### How does Portable Text represent lists?

A block can be given a listItem property describing the kind of list, typically bullet, number or square. The list position is derived from the block's position in the array and the surrounding list items on the same level, and a level property expresses visual nesting between blocks.

### Can I store my own data types in Portable Text?

Yes. Portable Text allows inserting arbitrary data objects in the array, requiring only the _type key, and allows custom content objects in the root array so that editing and rendering environments can mix rich text with custom content types. Custom blocks such as images, code blocks, tables and video embeds are the common case.

### Which implementations exist for Portable Text?

The specification lists JavaScript packages for React, React Native, React PDF, Svelte and Vue plus a to-html converter and block tools, a PHP class in sanity-php, a Python portabletext-html converter, C# converters including a port of the htmlToBlocks function, and a Dart and Flutter widget.

## Sources

- [Issues](https://github.com/portabletext/portabletext/issues)
- [License: MIT](https://github.com/portabletext/portabletext/blob/main/LICENSE)
- [portabletext/portabletext on GitHub](https://github.com/portabletext/portabletext)
- [Project website](https://www.portabletext.org)
- [README](https://github.com/portabletext/portabletext/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/portabletext-portabletext
