Skip to main content
The Keyboard module provides a builder pattern for creating VK message keyboards with various button types and layouts.

Creating Keyboards

There are two ways to create keyboards, both return a KeyboardBuilder instance:
Recommended approach with chainable methods:
import { Keyboard } from 'vk-io';

const keyboard = Keyboard.builder()
  .textButton({
    label: 'Start',
    payload: { command: 'start' },
    color: Keyboard.PRIMARY_COLOR
  })
  .row()
  .textButton({
    label: 'Help',
    payload: { command: 'help' }
  })
  .textButton({
    label: 'Settings',
    payload: { command: 'settings' }
  })
  .row()
  .textButton({
    label: 'Cancel',
    color: Keyboard.NEGATIVE_COLOR
  })
  .oneTime();

Sending Keyboards

// Send with message
await api.messages.send({
  peer_id: 123456,
  message: 'Choose an option:',
  keyboard: keyboard,
  random_id: 0
});

// Or with context
await context.send('Choose an option:', {
  keyboard: keyboard
});

Keyboard Limits

Keyboard size restrictions:
  • Regular keyboard: 10 rows × 5 buttons per row
  • Inline keyboard: 6 rows × 5 buttons per row
  • Wide buttons (URL, Location, Pay, App): occupy 2 button spaces

Button Types

Text Button

Standard clickable button that sends text when pressed:
keyboard.textButton({
  label: 'Buy Coffee',
  payload: {
    command: 'buy',
    item: 'coffee',
    price: 3.50
  },
  color: Keyboard.POSITIVE_COLOR
});
label
string
required
Button text (max 40 characters)
payload
object
Custom data (max 255 chars as JSON). Available in context.messagePayload
color
ButtonColor
Button color: PRIMARY, SECONDARY, NEGATIVE, or POSITIVE

Callback Button

Silent button that triggers message_event without sending a message:
keyboard.callbackButton({
  label: 'Like',
  payload: {
    action: 'like',
    post_id: 123
  },
  color: Keyboard.POSITIVE_COLOR
});

// Handle callback
updates.on('message_event', async (context) => {
  const { action, post_id } = context.eventPayload;
  
  if (action === 'like') {
    await api.likes.add({
      type: 'post',
      item_id: post_id
    });
    
    // Show notification
    await context.showSnackbar('Liked!');
  }
});
label
string
required
Button text (max 40 characters)
payload
object
Custom data. Available in context.eventPayload
color
ButtonColor
Button color
Callback buttons don’t flood the chat with user messages. Perfect for inline actions like ratings, pagination, or toggles.

URL Button

Opens a link when pressed (takes 2 button slots):
keyboard.urlButton({
  label: 'Visit Website',
  url: 'https://example.com',
  payload: { source: 'keyboard' }
});
label
string
required
Button text (max 40 characters)
url
string
required
URL to open
payload
object
Optional custom data

Location Request Button

Requests user’s location (takes 2 button slots):
keyboard.locationRequestButton({
  payload: {
    command: 'order_delivery'
  }
});

// Handle location
updates.on('message', (context) => {
  if (context.geo) {
    const { latitude, longitude } = context.geo.coordinates;
    console.log(`Location: ${latitude}, ${longitude}`);
  }
});

VK Pay Button

Initiates VK Pay transaction (takes 2 button slots):
keyboard.payButton({
  payload: { order_id: '12345' },
  hash: {
    action: 'pay-to-group',
    group_id: 123456,
    amount: 100,
    description: 'Coffee order',
    aid: 10
  }
});

// Or with string hash
keyboard.payButton({
  payload: {},
  hash: 'action=transfer-to-group&group_id=123456&aid=10'
});
Hash object options:
{
  action: 'pay-to-group',
  group_id: 123456,
  amount: 100,          // Rubles
  description: 'Order',
  data: 'custom_data',
  aid: 10               // App ID
}

VK App Button

Opens a VK Mini App (takes 2 button slots):
keyboard.applicationButton({
  label: 'Open App',
  appId: 6232540,
  ownerId: -157525928,
  hash: 'profile'  // Navigation hash
});
label
string
required
Button text (max 40 characters)
appId
number
required
VK App ID
ownerId
number
Community ID where app is installed (negative for groups)
hash
string
Navigation parameter (passed after # in app URL)

Button Colors

Four color options for text and callback buttons:
ConstantColorUse CaseHex
Keyboard.SECONDARY_COLORWhiteNeutral actions#FFFFFF
Keyboard.PRIMARY_COLORBluePrimary actions#5181B8
Keyboard.POSITIVE_COLORGreenConfirm, agree, accept#4BB34B
Keyboard.NEGATIVE_COLORRedDangerous, delete, cancel#E64646
const keyboard = Keyboard.builder()
  .textButton({
    label: 'Continue',
    color: Keyboard.PRIMARY_COLOR
  })
  .textButton({
    label: 'Accept',
    color: Keyboard.POSITIVE_COLOR
  })
  .row()
  .textButton({
    label: 'Cancel',
    color: Keyboard.NEGATIVE_COLOR
  })
  .textButton({
    label: 'Back',
    color: Keyboard.SECONDARY_COLOR
  });
Actual colors may vary based on user’s VK theme (light/dark mode).

Layout Methods

Add Row

Move to the next row:
const keyboard = Keyboard.builder()
  .textButton({ label: 'Button 1' })
  .textButton({ label: 'Button 2' })
  .row()  // New row
  .textButton({ label: 'Button 3' })
  .textButton({ label: 'Button 4' });

One-Time Keyboard

Hide keyboard after button press:
const keyboard = Keyboard.builder()
  .textButton({ label: 'Yes' })
  .textButton({ label: 'No' })
  .oneTime();
One-time keyboards don’t work with inline keyboards.

Inline Keyboard

Attach keyboard to specific message:
const keyboard = Keyboard.builder()
  .callbackButton({
    label: 'Edit',
    payload: { action: 'edit' }
  })
  .callbackButton({
    label: 'Delete',
    payload: { action: 'delete' }
  })
  .inline();

await context.send('Post created', { keyboard });
Inline keyboards are perfect for message-specific actions like “Edit”, “Delete”, or “Show More”.

Advanced Usage

Clone Keyboard

const baseKeyboard = Keyboard.builder()
  .textButton({ label: 'Help' })
  .textButton({ label: 'Settings' });

const keyboard1 = baseKeyboard.clone()
  .row()
  .textButton({ label: 'Option A' });

const keyboard2 = baseKeyboard.clone()
  .row()
  .textButton({ label: 'Option B' });

Remove Keyboard

Send empty keyboard to remove:
await context.send('Keyboard removed', {
  keyboard: Keyboard.builder()
});

// Or use JSON string
await api.messages.send({
  peer_id: 123456,
  message: 'Keyboard removed',
  keyboard: JSON.stringify({ buttons: [] }),
  random_id: 0
});

Dynamic Keyboards

function createMenuKeyboard(userRole: string) {
  const keyboard = Keyboard.builder();
  
  // Common buttons
  keyboard
    .textButton({ label: 'Profile' })
    .textButton({ label: 'Help' })
    .row();
  
  // Role-specific buttons
  if (userRole === 'admin') {
    keyboard
      .textButton({
        label: 'Admin Panel',
        color: Keyboard.NEGATIVE_COLOR
      })
      .row();
  }
  
  keyboard.textButton({
    label: 'Logout',
    color: Keyboard.SECONDARY_COLOR
  });
  
  return keyboard;
}

await context.send('Menu:', {
  keyboard: createMenuKeyboard('admin')
});

Pagination Keyboard

function createPaginationKeyboard(page: number, totalPages: number) {
  const keyboard = Keyboard.builder().inline();
  
  if (page > 1) {
    keyboard.callbackButton({
      label: '◀️ Previous',
      payload: { action: 'page', page: page - 1 }
    });
  }
  
  keyboard.callbackButton({
    label: `${page} / ${totalPages}`,
    payload: { action: 'current' }
  });
  
  if (page < totalPages) {
    keyboard.callbackButton({
      label: 'Next ▶️',
      payload: { action: 'page', page: page + 1 }
    });
  }
  
  return keyboard;
}

updates.on('message_event', async (context) => {
  const { action, page } = context.eventPayload;
  
  if (action === 'page') {
    await context.editMessage({
      message: `Page ${page} content`,
      keyboard: createPaginationKeyboard(page, 10)
    });
  }
});

Complete Example

import { Keyboard } from 'vk-io';

// Main menu keyboard
const mainMenu = Keyboard.builder()
  .textButton({
    label: 'Shop',
    payload: { command: 'shop' },
    color: Keyboard.PRIMARY_COLOR
  })
  .textButton({
    label: 'Orders',
    payload: { command: 'orders' }
  })
  .row()
  .textButton({
    label: 'Profile',
    payload: { command: 'profile' }
  })
  .textButton({
    label: 'Support',
    payload: { command: 'support' }
  })
  .row()
  .urlButton({
    label: 'Website',
    url: 'https://example.com'
  })
  .oneTime();

// Inline action keyboard
const itemActions = Keyboard.builder()
  .callbackButton({
    label: 'Add to Cart',
    payload: { action: 'add_to_cart', item_id: 123 },
    color: Keyboard.POSITIVE_COLOR
  })
  .row()
  .callbackButton({
    label: 'Details',
    payload: { action: 'details', item_id: 123 }
  })
  .urlButton({
    label: 'Reviews',
    url: 'https://example.com/item/123/reviews'
  })
  .inline();

// Send keyboards
await context.send('Welcome!', { keyboard: mainMenu });
await context.send('Coffee - $3.50', { keyboard: itemActions });
Combine keyboard types: use regular keyboards for navigation and inline keyboards for item-specific actions.

Build docs developers (and LLMs) love