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

How to add custom YouTube blocks to Portable Text

Next

Add things to Portable Text

Was this page helpful?

On this page

  • Overview
  • Prerequisites
  • The Problem
  • A Solution
  • Key Features
  • How It Works
  • Testing the Solution
  • Conclusion
Developer guidesLast updated September 24, 2025

Converting Inline Styles to Sanity Block Decorators

Learn how to seamlessly migrate rich text with inline styles (like bold, italic, and underline) into Sanity block content. This guide provides a step-by-step solution to handle custom deserialization, including nested spans and advanced inline styles.

This developer guide was contributed by KJ O'Brien (Senior Support Engineer).

When migrating rich text content from other CMS platforms into Sanity, you may encounter inline styles like <span style="font-weight: bold;">. By default, Sanity's html-to-blocks method doesn't handle these styles, leading to loss of formatting in the converted content. This guide walks you through creating custom deserialization rules to properly handle these cases. We'll also cover how to process nested spans, preserve spaces between words, and merge decorators effectively. By the end, you'll have a robust solution for rich text migration without losing valuable formatting.

Overview

When migrating rich text content from another CMS to Sanity, inline styles (e.g., <span style="font-weight: bold;">) often need to be translated into decorators like strong, em, or underline. This guide walks you through customizing the html-to-blocks serialization to handle such cases, including nested spans with multiple styles.

Prerequisites

  • Familiarity with Sanity's block content structure.
  • Installed @sanity/block-tools package.

The Problem

By default, the html-to-blocks method handles common tags like <strong> and <em>, but inline styles like <span style="font-weight: bold;"> are ignored. To convert these spans into appropriate decorators, we need to extend the deserialization rules.

A Solution

Custom Deserialization Rules

The following code demonstrates how to handle spans with inline styles, including nested spans:

const customRules = [
  {
    deserialize(el, next) {
      if (el.tagName === 'SPAN') {
        const style = el.style
        const marks = []

        // Collect marks from inline styles
        if (style?.fontWeight === 'bold' || style?.fontWeight >= 600) {
          marks.push('strong')
        }
        if (style?.fontStyle === 'italic') {
          marks.push('em')
        }
        if (style?.textDecoration.includes('underline')) {
          marks.push('underline')
        }

        // Initialize an array to hold the final processed spans
        const processedSpans = []

        // Process child nodes recursively
        Array.from(el.childNodes).forEach((node) => {
          if (node.nodeType === 3) {
            // Handle text nodes
            const text = node.nodeValue
            if (text) {
              processedSpans.push({
                _type: 'span',
                text,
                marks,
              })
            }
          } else {
            // Process child elements recursively
            const childNodes = next([node]).map((child) => {
              if (child._type === 'span') {
                return {
                  ...child,
                  marks: [...new Set([...(child.marks || []), ...marks])],
                }
              }
              return child
            })
            processedSpans.push(...childNodes)
          }
        })

        return processedSpans
      }

      return undefined // Pass to the next rule if not a span
    },
  },
]

Key Features

  • Handles Inline Styles: Detects and converts font-weight, font-style, and text-decoration styles into Sanity decorators.
  • Supports Nested Spans: Processes nested spans by merging inherited and child decorators.
  • Prevents Redundant Marks: Uses Set to ensure each mark is applied only once.

How It Works

  • Marks Collection: The style property of the <span> tag is inspected to determine which marks to apply.
  • Child Node Processing: Text nodes are wrapped into spans, and child elements are recursively processed with the accumulated marks.
  • Nested Styles: For child elements, existing marks are merged with those inherited from the parent.

Testing the Solution

To ensure your custom rules work as expected, test the following HTML input:

<span style="font-weight: bold;">Want to <span style="font-style: italic;">learn <span style="text-decoration: underline;">a lot</span> more</span></span>

This should output:

[
  {
    "_type": "span",
    "text": "Want to ",
    "marks": ["strong"]
  },
  {
    "_type": "span",
    "text": "learn ",
    "marks": ["strong", "em"]
  },
  {
    "_type": "span",
    "text": "a lot",
    "marks": ["strong", "em", "underline"]
  },
  {
    "_type": "span",
    "text": " more",
    "marks": ["strong", "em"]
  }
]

Conclusion

This approach allows for seamless migration of rich text content with inline styles into Sanity's block content, enabling you to preserve the original formatting. For additional details, refer to the @sanity/block-tools documentation.

const customRules = [
  {
    deserialize(el, next) {
      if (el.tagName === 'SPAN') {
        const style = el.style
        const marks = []

        // Collect marks from inline styles
        if (style?.fontWeight === 'bold' || style?.fontWeight >= 600) {
          marks.push('strong')
        }
        if (style?.fontStyle === 'italic') {
          marks.push('em')
        }
        if (style?.textDecoration.includes('underline')) {
          marks.push('underline')
        }

        // Initialize an array to hold the final processed spans
        const processedSpans = []

        // Process child nodes recursively
        Array.from(el.childNodes).forEach((node) => {
          if (node.nodeType === 3) {
            // Handle text nodes
            const text = node.nodeValue
            if (text) {
              processedSpans.push({
                _type: 'span',
                text,
                marks,
              })
            }
          } else {
            // Process child elements recursively
            const childNodes = next([node]).map((child) => {
              if (child._type === 'span') {
                return {
                  ...child,
                  marks: [...new Set([...(child.marks || []), ...marks])],
                }
              }
              return child
            })
            processedSpans.push(...childNodes)
          }
        })

        return processedSpans
      }

      return undefined // Pass to the next rule if not a span
    },
  },
]
<span style="font-weight: bold;">Want to <span style="font-style: italic;">learn <span style="text-decoration: underline;">a lot</span> more</span></span>
[
  {
    "_type": "span",
    "text": "Want to ",
    "marks": ["strong"]
  },
  {
    "_type": "span",
    "text": "learn ",
    "marks": ["strong", "em"]
  },
  {
    "_type": "span",
    "text": "a lot",
    "marks": ["strong", "em", "underline"]
  },
  {
    "_type": "span",
    "text": " more",
    "marks": ["strong", "em"]
  }
]