Skip to content

Keyboards

VibeGram provides a declarative Markup builder for Telegram inline keyboards, reply keyboards, pagination, and safe text escaping.

Inline Keyboards

Inline keyboards appear below a message and usually trigger callback queries or open Telegram-supported surfaces.

ts
import { Markup } from 'vibegram';

const keyboard = Markup.inlineKeyboard([
    [Markup.button.callback('Buy', 'buy_1'), Markup.button.callback('Cancel', 'cancel')],
    [Markup.button.url('Website', 'https://example.com')],
]);

await ctx.reply('Choose an option:', { reply_markup: keyboard });

Markup.grid()

Use Markup.grid() when you already have a flat array of inline buttons and want VibeGram to split it into rows.

ts
const days = ['Mon', 'Tue', 'Wed', 'Thu'].map(day => {
    return Markup.button.callback(day, `day:${day}`);
});

await ctx.reply('Pick a day:', {
    reply_markup: Markup.grid(days, 2),
});

Inline Button Types

MethodDescription
Markup.button.callback(text, data)Sends callback data to bot.action().
Markup.button.url(text, url)Opens an external URL.
Markup.button.webApp(text, url)Opens a Telegram Mini App.
Markup.button.pay(text)Payment button for invoice messages.
Markup.button.switchInlineQuery(text, query)Opens inline mode in another chat.
Markup.button.switchInlineQueryCurrentChat(text, query)Opens inline mode in the current chat.
Markup.button.login(text, loginUrl)Telegram Login button.
Markup.button.copy(text, textToCopy)Copy-to-clipboard button.

Login Button Example

ts
await ctx.reply('Sign in:', {
    reply_markup: Markup.inlineKeyboard([
        [
            Markup.button.login('Login with Telegram', {
                url: 'https://example.com/auth/telegram',
                request_write_access: true,
            }),
        ],
    ]),
});

Copy Button Example

ts
await ctx.reply('Copy your invite code:', {
    reply_markup: Markup.inlineKeyboard([
        [Markup.button.copy('Copy code', 'INVITE-2026')],
    ]),
});

Pagination

ts
const items = products.map(product => ({
    text: product.name,
    callback_data: `product:${product.id}`,
}));

await ctx.reply('Catalog:', {
    reply_markup: Markup.pagination(items, {
        currentPage: 1,
        itemsPerPage: 6,
        columns: 2,
        actionPrev: 'catalog:prev',
        actionNext: 'catalog:next',
        pageIndicatorPattern: '{current}/{total}',
    }),
});

Handle actionPrev, actionNext, and item callback data with bot.action().

Reply Keyboards

Reply keyboards replace the device keyboard with Telegram-native buttons.

ts
const keyboard = Markup.keyboard(
    [
        [Markup.replyButton.text('News'), Markup.replyButton.text('Settings')],
        [Markup.replyButton.requestContact('Share phone')],
        [Markup.replyButton.requestLocation('Share location')],
    ],
    {
        resize_keyboard: true,
        one_time_keyboard: false,
    }
);

await ctx.reply('Menu:', { reply_markup: keyboard });

Reply Button Types

MethodDescription
Markup.replyButton.text(text)Plain text button.
Markup.replyButton.requestContact(text)Requests the user's phone number.
Markup.replyButton.requestLocation(text)Requests the user's location.
Markup.replyButton.requestPoll(text, type?)Opens poll creation.
Markup.replyButton.requestUser(text, requestId, options?)Opens Telegram user selection.
Markup.replyButton.requestChat(text, requestId, options?)Opens chat selection.
Markup.replyButton.requestManagedBot(text, requestId, options?)Requests managed bot authorization.

Remove Keyboard

ts
await ctx.reply('Keyboard removed.', {
    reply_markup: Markup.removeKeyboard(),
});

Force Reply

ts
await ctx.reply('What is your name?', {
    reply_markup: Markup.forceReply({
        input_field_placeholder: 'Type your name...',
    }),
});

Escaping Untrusted Text

When interpolating user-supplied text into a message with parse_mode, escape only the dynamic values.

ts
const safeHtml = Markup.escapeHTML(userName);
await ctx.reply(`Hello <b>${safeHtml}</b>`, { parse_mode: 'HTML' });

const safeMarkdown = Markup.escapeMarkdownV2(userName);
await ctx.reply(`Hello *${safeMarkdown}*`, { parse_mode: 'MarkdownV2' });

const safeLegacyMarkdown = Markup.escapeMarkdown(userName);
await ctx.reply(`Hello _${safeLegacyMarkdown}_`, { parse_mode: 'Markdown' });
HelperUse with
Markup.escapeHTML(text)parse_mode: 'HTML'
Markup.escapeMarkdownV2(text)parse_mode: 'MarkdownV2'
Markup.escapeMarkdown(text)parse_mode: 'Markdown' (legacy)

If the whole reply is dynamic user content, send plain text without parse_mode.

Released under the ISC License.