@yaebal/keyboard

fluent inline and reply keyboard builders.

install

terminal
pnpm add @yaebal/keyboard

inline keyboard

InlineKeyboard builds an inline_keyboard markup. buttons accumulate into the current row; call .row() to start a new one. call .build() to get the final InlineKeyboardMarkup object to pass as reply_markup.

menu.ts
import { Bot } from "@yaebal/core";
import { InlineKeyboard } from "@yaebal/keyboard";

const bot = new Bot(process.env.BOT_TOKEN!);

bot.command("menu", (ctx) => {
  const kb = new InlineKeyboard()
    .text("ban", "action:ban")
    .text("warn", "action:warn")
    .row()
    .url("view profile", "https://t.me/username")
    .build();

  return ctx.reply("choose an action:", { reply_markup: kb });
});

reply keyboard

Keyboard builds a keyboard markup. same row model as InlineKeyboard. flags resized() and oneTime() are only included in the output when set to true.

start.ts
import { Keyboard } from "@yaebal/keyboard";

bot.command("start", (ctx) => {
  const kb = new Keyboard()
    .text("yes")
    .text("no")
    .row()
    .requestContact("share phone")
    .resized()
    .oneTime()
    .build();

  return ctx.reply("ready?", { reply_markup: kb });
});

web app and switch inline

inline.ts
const kb = new InlineKeyboard()
  .webApp("open app", "https://example.com")
  .row()
  .switchInline("share", "my query")
  .build();

api

InlineKeyboard

methodsignaturedescription
text(label: string, data: string) => thisbutton with callback_data
url(label: string, url: string) => thisbutton that opens a URL
webApp(label: string, url: string) => thisbutton that opens a Telegram Web App
switchInline(label: string, query?: string) => thisswitch to inline mode; query defaults to ""
row() => thisend the current row; no-op if the row is empty
build() => InlineKeyboardMarkupreturns the finished markup; does not mutate the builder

Keyboard

methodsignaturedescription
text(label: string) => thisplain text button
requestContact(label: string) => thisbutton that requests the user's phone number
requestLocation(label: string) => thisbutton that requests the user's location
row() => thisend the current row; no-op if the row is empty
resized(value?: boolean) => thisset resize_keyboard; defaults to true
oneTime(value?: boolean) => thisset one_time_keyboard; defaults to true
build() => ReplyKeyboardMarkupreturns the finished markup; does not mutate the builder

types

exportdescription
InlineKeyboardMarkupshape returned by InlineKeyboard.build()
InlineKeyboardButtonsingle button in an inline keyboard
ReplyKeyboardMarkupshape returned by Keyboard.build()
KeyboardButtonsingle button in a reply keyboard
.build() always returns a snapshot. mutating the builder after calling .build() does not affect the already-returned markup — the rows are cloned at build time.

a trailing .row() before .build() is safe — an empty in-progress row is not emitted, so you can end every row with .row() without producing a blank final row.