Skip to main content

V2 message layout

await client.messages.send(channelId, {
  flags: MESSAGE_FLAGS.IS_COMPONENTS_V2,
  components: [
    Container.stack(
      Section.text('Open the survey modal')
        .accessory(Button.primary('open_v2_modal', 'Open modal')),
      Section.text('Open the upload modal')
        .accessory(Button.secondary('open_v2_upload_modal', 'Upload file'))
    )
  ]
})

Typed modal definition

const surveyModal = modal('survey_modal', 'Survey')
  .add(
    field.short('name', 'Your name'),
    field.radioGroup('color', 'Favorite color', {
      options: [
        { label: 'Red', value: 'red' },
        { label: 'Blue', value: 'blue' }
      ]
    }),
    field.fileUpload('attachment', 'Upload a file', { required: false })
  )
  .handle(async (ctx) => {
    const name = ctx.fields.name
    const color = ctx.fields.color
    const files = ctx.attachments.attachment ?? []

    await ctx.reply({
      content: `name=${name}, color=${color}, files=${files.length}`,
      ephemeral: true
    })
  })

Open the modal

client.commands.registerModal(surveyModal)

client.components.register(
  defineButton({
    customId: 'open_v2_modal',
    style: 'primary',
    label: 'Open modal',
    execute: async (ctx) => {
      await ctx.showModal(surveyModal)
    }
  })
)

What improved recently

  • One Button helper works in both V1 rows and V2 accessories.
  • Section.text(...).accessory(...) avoids positional accessory arguments.
  • modal(...).add(...).handle(...) keeps ctx.fields inferred.
  • Upload fields surface resolved files through ctx.attachments[fieldId].

Current limitation

Components V2 are much better than before, but the layout DSL is still more structural than ideal. The framework is moving toward fewer raw payload shapes, not zero internal structure everywhere yet.
Last modified on June 13, 2026