Code·September 8, 2025

Using Nuxt UI Typography with Sanity

Nuxt UI's Typography components only work with Nuxt Content out of the box. However, with just a few lines of code, we can use them with Sanity.

The Typography docs for Nuxt UI

Attribution: nuxt.com

Since NuxtLabs joined Vercel, Nuxt UI v4 will now include all the Pro components for free 🎉🥳. Party time!

This means that the Typography components like ProsePre or ProseH2 will now be available for free.

Nuxt UI v4 is in alpha at the time of writing. I like living on the edge.

Like all real programmers, I'm lazy and would like to use the prebuilt typography components rather than writing my own. The issue, however, is that they are designed to work with Nuxt Content, not Sanity. However, with just a few lines of code, we can use them with Sanity.

Setup Guide

I'm assuming at this point that you have Sanity installed and set up and have a way to query and retrieve the content in Nuxt

1. Install @portabletext/vue and @nuxt/ui

npm install --save @portabletext/vue @nuxt/ui slugify

2. Enable the Typography components in nuxt.config.ts and ensure @nuxt/ui is enabled

nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    "@nuxt/ui",
  ],

  ui: {
    mdc: true,
  }
})

Enabling the Typography components is done by setting mdc: true in the @nuxt/ui module config. This adds them to the auto-imports.

3. Use <PortableText>

<script setup lang="ts">
import {
  PortableText,
  type PortableTextVueComponents,
  toPlainText,
} from "@portabletext/vue";
import {
  ProseH1,
  ProseH2,
  ProseH3,
  ProseH4,
  ProseP,
  ProseBlockquote,
  ProseEm,
  ProseStrong,
  ProseCode,
  ProseA,
  ProseUl,
  ProseOl
} from "#components";
import slugify from "slugify";

// TODO: Fetch from Sanity
const yourSanityBlocks = []

const components: Partial<PortableTextVueComponents> = {
  block: {
    // We use slugify and sanity's toPlainText to provide IDs for each heading for anchor links
    h1: ({ value }, { slots }) =>
      h(ProseH1, { id: slugify(toPlainText(value)) }, () => slots.default?.()),
    h2: ({ value }, { slots }) =>
      h(ProseH2, { id: slugify(toPlainText(value)) }, () => slots.default?.()),
    h3: ({ value }, { slots }) =>
      h(ProseH3, { id: slugify(toPlainText(value)) }, () => slots.default?.()),
    h4: ({ value }, { slots }) =>
      h(ProseH4, { id: slugify(toPlainText(value)) }, () => slots.default?.()),
    blockquote: ProseBlockquote,
    normal: ProseP,
  },
  marks: {
    em: (_, { slots }) => h(ProseEm, () => slots.default?.()),
    strong: (_, { slots }) => h(ProseStrong, () => slots.default?.()),
    code: (_, { slots }) => h(ProseCode, () => slots.default?.()),
    // Passes the href value and sets target="_blank" (new tab) for external links
    link: ({ value }, { slots }) =>
      h(
        ProseA,
        {
          href: value?.href,
          target: (value?.href || "").startsWith("http") ? "_blank" : undefined,
        },
        () => slots.default?.()
      ),
  },
  list: {
    bullet: ProseUl,
    number: ProseOl,
  },
};
</script>

<template>
  <PortableText
    :value="yourSanityBlocks"
    :components="components"
  />
</template>

Conclusion

And voila! You can now use Nuxt UI's Typography components with your Sanity blocks. In a future blog post, I will show how to get the ProsePre component working with syntax highlighting by Shiki (like I have on this page). Happy coding!

Hey! 👋

I hope you enjoyed this post!

I post whatever is on my mind, whether it's life, code, theology, or something random. Follow me on X if you'd like a heads up on new posts.