Inline audio player in rich text

How to use the Portable Text Editor's flexibility to insert dynamic content in the middle of paragraphs


As a new idea in my mind, this is by no means a solid source. I'm happy to hear your thoughts and learn better.

We can host audio files in Sanity and plug them into paragraphs by adding custom types to the block.of array. You can see this in action in my Learn GROQ guide. Without further ado, here's the process:

Starting with the schema, we need to add the inlineAudio schema type to our block content's of property (documentation on this property):

export default {
  name: "contentBody",
  title: 'Body of content',
  type: 'array',
  of: [
      type: 'block',
      of: [
          name: 'inlineAudio',
          type: 'file',
          title: 'Inline audio player',
          options: {
            accept: 'audio/*',
    {type: "image"}

The `of` array defines which custom types
should be rendered inline

Accept only audio files

Notice the difference with custom types at the same level as the `type: block`:
these, like the image here, will be standalone blocks which are placed between paragraphs, not inside them.

This will allow us to add the "Inline audio player" element to our paragraph from the insert menu:

Screenshot of the rich text editor of this guide with the insert menu open

Now editors have the ability to add inline audio players to their content! 🎉🎉 Let's cover how to render this in the front-end. Here's a portion of the component that renders the block content of my articles (I'm using React and the @sanity/block-content-to-react package):

import * as React from 'react'
import BlockContent from '@sanity/block-content-to-react'

import AudioPlayer from '../AudioPlayer'

const serializers = {
  types: {
    // Handler for the "inlineAudio" _type
    inlineAudio: ({ node }) => {
      // The component we use to render the actual player
      return <AudioPlayer {...node} />

const ArticleBlockContent = (props) => {
  return (

export default ArticleBlockContent

And here's the AudioPlayer component that actually renders the data into an actionable button for users:

import * as React from 'react'

const AudioPlayer = (props) => {
  // Used to store the audio element once instanciated
  const [audioEl, setAudioEl] = React.useState()

  if (!props.asset?._ref) {
    return null
  const { _ref: ref } = props.asset
  // Example:
  // From: file-ff7d1c2d7bd5ac367359d57f0319f5f458bc3c3d-m4a
  // To:

  const assetRefParts = ref.split('-') // ["file", "ff7...", "m4a"]
  const id = assetRefParts[1] // "ff7..."
  const format = assetRefParts[2] // "m4a"
  const assetUrl = `${process.env.NEXT_PUBLIC_SANITY_PROJECT_ID}/${process.env.NEXT_PUBLIC_SANITY_DATASET}/${id}.${format}`

  function playAudio() {
    try {
      if (!audioEl) {
        const audio = new Audio(assetUrl)
      } else {
    } catch (error) {}
  return (
    <button onClick={playAudio} aria-label="Play audio">

export default AudioPlayer

And that's it! There are many more interesting use cases of inline blocks, some of which I hope to cover in the future.

Reach me at or hdorodev if you have any questions ;)

PS: This is also available in video on YouTube: