External data
Fill an external_select's options from your own data with onOptions and block_suggestion.
A static select ships its options inline. An external_select (and multi_external_select) asks
your app for them instead, as the user types. Slack calls this a block_suggestion request.
@nkootstra/block-kit emulates the same request/response cycle in the browser through onOptions.
onOptions
Pass onOptions to <BlockKitProvider>. It receives the block_suggestion payload and returns
options the same way an app’s options-load URL would reply:
import { BlockKitProvider, Message, type OptionsResponse } from "@nkootstra/block-kit";
const FRUIT = ["Apple", "Banana", "Cherry", "Grape", "Mango", "Orange", "Peach", "Pear"];
function onOptions({ value }: { value: string }): OptionsResponse {
const query = value.toLowerCase();
return {
options: FRUIT.filter((name) => name.toLowerCase().includes(query)).map((name) => ({
text: { type: "plain_text", text: name },
value: name.toLowerCase(),
})),
};
}
export function App() {
return (
<BlockKitProvider onOptions={onOptions}>
<Message
blocks={[
{
type: "section",
block_id: "fruit",
text: { type: "mrkdwn", text: "Pick a fruit" },
accessory: {
type: "external_select",
action_id: "value",
placeholder: { type: "plain_text", text: "Search fruit" },
min_query_length: 1,
},
},
]}
/>
</BlockKitProvider>
);
}
onOptions can also return a promise, so it’s fine to call a real API from it.
Without an onOptions handler, an external_select still lets the user type a value and press Enter,
but it just never offers suggestions, since there’s nothing to ask.
The block_suggestion payload
onOptions receives the exact shape Slack sends to an options-load URL:
type?"block_suggestion"
"block_suggestion"action_id?string
stringblock_id?string
stringvalue?string
What the user has typed into the search box so far.
stringcontainer?Container
The message or view the select is in: { type: "message", ... } or { type: "view", ... }.
Containerview?ViewLike
Present when the select is inside a modal or Home tab.
ViewLiketeam?PayloadTeam | null
PayloadTeam | nulluser?PayloadUser
PayloadUserUse value to filter your results, and block_id / action_id if the same handler serves more than
one external select.
Answering with options or option_groups
options?SuggestionOption[]
Flat list. Each is { text: { type: "plain_text", text }, value, description?, url? }.
SuggestionOption[]option_groups?Array<{ label, options }>
Labelled groups, each with its own options array, same option shape as above.
Array<{ label, options }>function onOptions({ value }: { value: string }): OptionsResponse {
return {
option_groups: [
{
label: { type: "plain_text", text: "Citrus" },
options: [{ text: { type: "plain_text", text: "Orange" }, value: "orange" }],
},
{
label: { type: "plain_text", text: "Stone fruit" },
options: [{ text: { type: "plain_text", text: "Peach" }, value: "peach" }],
},
],
};
}
Slack allows up to 100 options total across options/option_groups in a single response.
min_query_length and debouncing
An external_select waits for min_query_length characters (Slack’s default is 3) before it asks
onOptions at all, and debounces keystrokes after that so it doesn’t fire on every character. Set
min_query_length on the element to change the threshold. The preview above uses 1 so it responds
immediately:
{ type: "external_select", action_id: "value", min_query_length: 1 }
Inside a modal
onOptions works the same way whether the select is in a message, a modal, or the Home tab: the
payload’s container/view tells you which. This is what lets an external_select inside a
modal load real data: