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:
Builder Pattern
Array Construction
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 ();
Define keyboard structure as nested arrays: import { Keyboard } from 'vk-io' ;
const keyboard = Keyboard . keyboard ([
// Row 1: Single button
[
Keyboard . textButton ({
label: 'Start' ,
payload: { command: 'start' },
color: Keyboard . PRIMARY_COLOR
})
],
// Row 2: Two buttons
[
Keyboard . textButton ({
label: 'Help' ,
payload: { command: 'help' }
}),
Keyboard . textButton ({
label: 'Settings' ,
payload: { command: 'settings' }
})
],
// Row 3: Single button (alternative syntax)
Keyboard . textButton ({
label: 'Cancel' ,
color: Keyboard . NEGATIVE_COLOR
})
]);
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
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
});
Button text (max 40 characters)
Custom data (max 255 chars as JSON). Available in context.messagePayload
Button color: PRIMARY, SECONDARY, NEGATIVE, or POSITIVE
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!' );
}
});
Button text (max 40 characters)
Custom data. Available in context.eventPayload
Callback buttons don’t flood the chat with user messages. Perfect for inline actions like ratings, pagination, or toggles.
Opens a link when pressed (takes 2 button slots):
keyboard . urlButton ({
label: 'Visit Website' ,
url: 'https://example.com' ,
payload: { source: 'keyboard' }
});
Button text (max 40 characters)
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 } ` );
}
});
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 :
Pay to Group
Pay to User
Transfer to Group
Transfer to User
{
action : 'pay-to-group' ,
group_id : 123456 ,
amount : 100 , // Rubles
description : 'Order' ,
data : 'custom_data' ,
aid : 10 // App ID
}
Opens a VK Mini App (takes 2 button slots):
keyboard . applicationButton ({
label: 'Open App' ,
appId: 6232540 ,
ownerId: - 157525928 ,
hash: 'profile' // Navigation hash
});
Button text (max 40 characters)
Community ID where app is installed (negative for groups)
Navigation parameter (passed after # in app URL)
Four color options for text and callback buttons:
Constant Color Use Case Hex Keyboard.SECONDARY_COLORWhite Neutral actions #FFFFFF Keyboard.PRIMARY_COLORBlue Primary actions #5181B8 Keyboard.POSITIVE_COLORGreen Confirm, agree, accept #4BB34B Keyboard.NEGATIVE_COLORRed Dangerous, 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' )
});
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.