How to model adjacent `<kbd>` tags in Sanity without auto-merging?

16 replies
Last updated: Jul 7, 2020
Relatively new to Sanity land, and moving our changelog over but came across potentially exotic issue; We write quite a lot of combination keyboard shortcuts (eg
<kdb>Cmd</kdb><kdb>i</kdb
). I managed to write scripts to import data from markdown content, but as I modeled
kbd
tags as mark decorations, any two tags that are next to each others will automatically get combined as they are marks unless I add a whitespace in between... So question is, what would be the correct way to model them? Annotations?
AI Update

You're experiencing a classic Portable Text behavior! The issue is that decorators (marks) automatically merge when adjacent, which is exactly what you're seeing with your <kbd> tags. For keyboard shortcuts where you need <kbd>Cmd</kbd><kbd>i</kbd> to remain separate, you should indeed use annotations or inline objects instead.

Here are your two best options:

Option 1: Use Annotations (simpler)

Annotations are marks that can carry additional data and don't auto-merge like decorators. Define a kbd annotation in your schema:

defineType({
  name: 'blockContent',
  type: 'array',
  of: [
    {
      type: 'block',
      marks: {
        annotations: [
          {
            name: 'kbd',
            type: 'object',
            title: 'Keyboard Key',
            fields: [
              {
                name: 'key',
                type: 'string',
                title: 'Key'
              }
            ]
          }
        ]
      }
    }
  ]
})

Annotations won't merge when adjacent because each one is a distinct object with its own data. However, they still wrap text spans, so you'd select text and apply the annotation.

For your use case, inline objects are probably the better choice since keyboard keys are really discrete objects, not text formatting. According to the Portable Text documentation, inline blocks let you embed custom content within text paragraphs by adding them to the of array within the block type:

defineType({
  name: 'kbd',
  type: 'object',
  title: 'Keyboard Key',
  fields: [
    {
      name: 'key',
      type: 'string',
      title: 'Key'
    }
  ]
})

// Then in your block content:
defineType({
  name: 'blockContent',
  type: 'array',
  of: [
    {
      type: 'block',
      of: [
        { type: 'kbd' }  // Add as inline object
      ]
    }
  ]
})

With inline objects, each <kbd> becomes a distinct object in the content array, so they'll never merge. This also makes them much easier to insert programmatically during your markdown import.

For Your Import Script

When importing, inline objects would be easier to work with since you can insert them as distinct objects in the children array:

{
  _type: 'block',
  children: [
    { _type: 'span', text: 'Press ' },
    { _type: 'kbd', _key: 'unique-key-1', key: 'Cmd' },
    { _type: 'kbd', _key: 'unique-key-2', key: 'i' },
    { _type: 'span', text: ' to italicize' }
  ]
}

The inline objects approach is probably your best bet here since keyboard shortcuts are semantic units rather than text styling. This aligns with how Portable Text separates content structure from presentation, and it will make both authoring and rendering much cleaner. Each keyboard key will be a first-class object that you can style and render independently on your frontend.

Show original thread
16 replies
Oh, this is an interesting and clever use of marks 😄
Not really ideal as you can see because marks do get connected.
user M
anyone at Sanity who might be more in tuned with schema and editor (I noticed you're on Slate)
what about creating an object that you can add to the document, which is never rendered visually? I something like that an option? 🤔
not sure I follow? I'm trying to do something that gets rendered to the user 🙂
oh - I get it now 🤦 I wasn't understanding what you're doing
hmm, I'd be interested in how you solve this
I think annotations would work but I just need to have a field which is not great. Best would be if there would be inline nodes that aren't marks
Hi Jori, great question - and thanks Bryan for helping out 🙌 Would be nice if
kbd
itself would recognise a space and turn it into a new key 😉 But this is a clear use case that we should probably support somehow. For now you’ll probably have to fall back to using annotations but let me check with the person on the team who’s particularly well-versed in the PT editor and get back to you.
user M
thanks. I'm now exploring decorators more, so that maybe one writes it as one go and the renderer splits it up as you mentioned. Annotations are annoying because you need to annotate something, and it needs to have content so you have to write stuff twice for it. Inline nodes feels quite a big missing thing from the editor as one might want to use them for example emojis
Hi
user B
- I think inline objects are the correct way to model this. It's more an object on it's own than something that has to do with text formatting (decorators) or to annotate a certain text (annotations).
I'm thinking an object with a type like:


            {
              type: 'object',
              name: 'keyboardShortcut',
              preview: {
                select: {
                  keys: 'keys'
                },
                prepare(selection) {
                  return {
                    title: `${selection.keys.join(' + ')}`
                  }
                }
              },
              fields: [
                {
                  type: 'string',
                  name: 'shortcutName'
                },
                {
                  type: 'array',
                  name: 'keys',
                  of: [
                    {
                      type: 'string',
                      name: 'key'
                    }
                  ]
                }
              ]
            }
          ]
        }

Example of how it would look in the editor:
You could even make them on beforehand and then use references to them as inline objects in Portable Text, so you don't have to define them all the time.
Bonus is also that you'd have an index of all your shortcuts 🙂
oh, this is what I was trying to say - but then got myself confused 😂
thanks
user A
ah interesting, thanks
user A

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?