Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/alineacms/alinea/llms.txt

Use this file to discover all available pages before exploring further.

The Field.richText field renders a full-featured block editor powered by Tiptap. Unlike classic WYSIWYG editors, Alinea’s rich text field does not produce HTML strings. It stores content as a structured document — an array of typed nodes that can be traversed, transformed, and rendered with full type safety. This makes it straightforward to render rich text in any environment, from server-side React to a native mobile app.
Importing Field
import {Field} from 'alinea'

Basic usage

Defining a rich text field
import {Config, Field} from 'alinea'

export const Article = Config.document('Article', {
  fields: {
    body: Field.richText('Body text', {
      searchable: true,
      required: true
    })
  }
})
Querying the field returns a TextDoc value, which is an array of Node objects. Alinea ships with a createRichTextView rendering helper for React, but the structure is fully inspectable without it.

The TextDoc type

The TextDoc<T> type is the data structure stored for every rich text field. It is an array of Node values. Three node kinds exist:
Node kind_type prefixDescription
TextNode"text"A leaf text run with optional inline marks
ElementNodelowercase stringA structural element such as "paragraph" or "heading"
BlockNodeuppercase stringA custom embedded block; keys match your schema definition
TextDoc type shape
// Exported from 'alinea'
type TextDoc<T = {}> = Array<Node>

// TextNode — a run of formatted text
interface TextNode {
  _type: 'text'
  text?: string
  marks?: Array<{_type: string; [attr: string]: string | undefined}>
}

// ElementNode — a structural element containing child nodes
interface ElementNode {
  _type: string       // e.g. "paragraph", "heading", "bulletList"
  content?: TextDoc
  [key: string]: any
}

// BlockNode — a custom embedded block
interface BlockNode {
  _id: string
  _type: string       // matches a key in the richText schema, e.g. "CalloutBlock"
}

Embedding custom block types

Pass a schema option to allow editors to insert custom block types between paragraphs. Each key in schema becomes a selectable block type in the editor toolbar.
Rich text with embedded blocks
import {Config, Field} from 'alinea'

export const Page = Config.document('Page', {
  fields: {
    body: Field.richText('Body', {
      schema: {
        // A callout card that can be inserted between paragraphs
        Callout: Config.type('Callout', {
          fields: {
            icon: Field.select('Icon', {
              options: {info: 'Info', warning: 'Warning', tip: 'Tip'},
              initialValue: 'info'
            }),
            text: Field.text('Text', {multiline: true, required: true})
          }
        }),

        // An image block with a caption
        Figure: Config.type('Figure', {
          fields: {
            image: Field.image('Image'),
            caption: Field.text('Caption', {width: 0.75})
          }
        })
      }
    })
  }
})
Block types embedded inside a rich text field are fully typed. TypeScript infers the BlockNode discriminated union from the schema object.

Enabling tables

Set enableTables: true to add table support to the editor toolbar.
Rich text with table support
import {Config, Field} from 'alinea'

export const DataPage = Config.document('Data page', {
  fields: {
    body: Field.richText('Body', {
      enableTables: true
    })
  }
})

Options reference

schema
Schema
A map of Config.type(...) definitions. Each key is an uppercase block type that editors can insert into the document. Block data is fully typed and validated.
searchable
boolean
When true, all text content inside this field is indexed for full-text search in the dashboard and query layer.
required
boolean
Prevents saving an entry if the field is empty.
enableTables
boolean
Adds a table insertion and editing toolbar to the editor. Defaults to false.
toolbar
ToolbarConfig
Fine-grained control over which toolbar sections and items appear in the editor. Import prebuilt sections from alinea/field/richtext/Toolbar.
extensions
Record<string, AnyExtension>
Override or extend the underlying Tiptap extensions. Import the defaults from alinea/field/richtext/Extensions and spread them alongside your overrides.
help
ReactNode
Instructional text displayed below the field label.
initialValue
TextDoc
The document value pre-populated when a new entry is created.
readOnly
boolean
Renders the editor in a non-editable state.
inline
boolean
Compact rendering without the full toolbar chrome.
width
number
Fractional form width (0–1).

Setting an initial value

The initialValue option accepts a TextDoc array. You can build it manually or use Edit.richText() to compose it programmatically.
Using Edit.richText() for initial values
import {Config, Edit, Field} from 'alinea'

export const LandingPage = Config.document('Landing page', {
  fields: {
    intro: Field.richText('Introduction', {
      initialValue: Edit.richText()
        .addHtml('<p>Welcome to our site.</p>')
        .value()
    })
  }
})
Edit.richText() accepts an optional existing TextDoc to start from and exposes an .addHtml(html) method that parses HTML into TextDoc nodes, plus a .value() method that returns the completed document.

Rendering rich text

Use the @alinea/ui rendering utilities or write your own renderer that traverses the TextDoc array. Here is a minimal custom renderer in React:
Rendering a TextDoc in React
import type {TextDoc, Node} from 'alinea'

function RichText({doc}: {doc: TextDoc}) {
  return (
    <>
      {doc.map((node, i) => (
        <RichTextNode key={i} node={node} />
      ))}
    </>
  )
}

function RichTextNode({node}: {node: Node}) {
  if (node._type === 'text') {
    return <span>{node.text}</span>
  }
  if (node._type === 'paragraph') {
    return (
      <p>
        {node.content?.map((child, i) => (
          <RichTextNode key={i} node={child} />
        ))}
      </p>
    )
  }
  if (node._type === 'heading') {
    const Tag = `h${node.attrs?.level ?? 2}` as 'h1' | 'h2' | 'h3'
    return (
      <Tag>
        {node.content?.map((child, i) => (
          <RichTextNode key={i} node={child} />
        ))}
      </Tag>
    )
  }
  // Custom block nodes have an uppercase _type
  if (node._type === 'Callout') {
    return <div className="callout">{(node as any).text}</div>
  }
  return null
}

Rich text inside list rows

Field.richText can be nested inside Field.list or Field.object block types to create rich structured content:
Rich text nested in a list schema
import {Config, Field} from 'alinea'

export const BlogPost = Config.document('Blog post', {
  fields: {
    sections: Field.list('Sections', {
      schema: {
        TextSection: Config.type('Text section', {
          fields: {
            heading: Field.text('Heading', {required: true}),
            body: Field.richText('Body', {searchable: true})
          }
        })
      }
    })
  }
})

Build docs developers (and LLMs) love