loading

Register app-wide shortcuts

Add

Add attaches a global keydown shortcut and returns a Guid you can later pass to Remove. By default the browser action is suppressed (preventDefault and stopPropagation are true) and held keys do not auto-repeat. Two shortcuts are live on this page right now.

Razor
@inject Bit.Butil.Keyboard keyboard

@code {
    private Guid ctrlKId;

    protected override async Task OnInitializedAsync()
    {
        ctrlKId = await keyboard.Add(ButilKeyCodes.KeyK, () =>
        {
            // open your command palette...
        }, ButilModifiers.Ctrl);

        await keyboard.Add(ButilKeyCodes.F9, () => { /* ... */ });
    }
}
Live sample
Ctrl + K and F9 are registered - press them and watch the output.
shortcuts output
Results will appear here when you interact with the samples.

Modifier combinations

ButilModifiers

ButilModifiers is a flags enum - combine Ctrl, Alt, Shift and Meta with the bitwise or operator. Build a shortcut below and register it live; press it to see it fire, and register a new one to replace it.

C#
await keyboard.Add(ButilKeyCodes.KeyU,
    () => { /* ... */ },
    ButilModifiers.Ctrl | ButilModifiers.Shift);
Live sample
Key code (KeyboardEvent.code)
Modifiers
modifiers output
Results will appear here when you interact with the samples.

Disposable subscriptions

Subscribe

Subscribe is Add with lifecycle sugar: it returns a ButilSubscription whose DisposeAsync detaches the shortcut, so a component can await-using a shortcut or dispose it in DisposeAsync without tracking Guids.

C#
private ButilSubscription? shortcut;

shortcut = await keyboard.Subscribe(ButilKeyCodes.KeyB, () =>
{
    // toggle the sidebar...
}, ButilModifiers.Ctrl);

// later:
await shortcut.DisposeAsync();
Live sample
subscription output
Results will appear here when you interact with the samples.

Element-scoped shortcuts

SubscribeOn

SubscribeOn binds the shortcut to a specific element: the handler only fires while that element (or one of its descendants) receives the keyboard event. Perfect for editor areas that need their own key handling without hijacking the whole page.

Razor
<textarea @ref="editorRef"></textarea>

@code {
    private ElementReference editorRef;
    private ButilSubscription? editorShortcut;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender is false) return;

        editorShortcut = await keyboard.SubscribeOn(editorRef,
            ButilKeyCodes.Enter, () => { /* submit */ }, ButilModifiers.Ctrl);
    }
}
Live sample
Ctrl+Enter fires only while this textarea has focus
element-scoped output
Results will appear here when you interact with the samples.

Removing shortcuts

Remove / RemoveAll

Remove(Guid) detaches one shortcut by the id Add returned. Remove(Action) matches by delegate identity and returns the removed ids. RemoveAll wipes every shortcut registered through this Keyboard instance.

C#
var id = await keyboard.Add(ButilKeyCodes.F9, handler);

await keyboard.Remove(id);        // by id (recommended)
await keyboard.Remove(handler);   // by the exact same delegate instance
await keyboard.RemoveAll();       // everything on this Keyboard instance
Live sample
removing output
Results will appear here when you interact with the samples.

Key codes reference

ButilKeyCodes

ButilKeyCodes provides string constants for every physical key: letters (KeyA-KeyZ), digits (Digit0-Digit9), the numpad, function keys (F1-F12), navigation, editing and symbol keys. They are plain KeyboardEvent.code values, so any valid code string works too.

C#
ButilKeyCodes.KeyK        // "KeyK"
ButilKeyCodes.Digit1      // "Digit1"
ButilKeyCodes.F9          // "F9"
ButilKeyCodes.Enter       // "Enter"
ButilKeyCodes.Escape      // "Escape"
ButilKeyCodes.ArrowDown   // "ArrowDown"
ButilKeyCodes.NumpadAdd   // "NumpadAdd"
ButilKeyCodes.Backquote   // "Backquote"
Warning:
RemoveAll clears more than your component The Keyboard service is scoped to the whole app instance, so RemoveAll detaches every shortcut any component registered on it. In a component, prefer removing your own ids - or use Subscribe and dispose the returned handles - as this page does in DisposeAsync.
Note:
Physical keys, not characters Matching uses KeyboardEvent.code - the physical key - so ButilKeyCodes.KeyK fires for that key position regardless of the active keyboard layout, and shortcuts keep working while Caps Lock or Shift change the produced character.

API reference

Member
Signature
Description
Add
Task<Guid> Add(string code, Action handler, ButilModifiers modifiers = ButilModifiers.None, bool preventDefault = true, bool stopPropagation = true, bool repeat = false)
Registers a global keydown shortcut and returns its id. Set repeat to true to also fire on key auto-repeat.
Subscribe
Task<ButilSubscription> Subscribe(string code, Action handler, ButilModifiers modifiers = ButilModifiers.None, bool preventDefault = true, bool stopPropagation = true, bool repeat = false)
Same as Add but returns a disposable handle that detaches the shortcut.
SubscribeOn
Task<ButilSubscription> SubscribeOn(ElementReference element, string code, Action handler, ButilModifiers modifiers = ButilModifiers.None, bool preventDefault = true, bool stopPropagation = true, bool repeat = false)
Element-scoped variant: fires only while the element or a descendant receives the keyboard event.
Remove
ValueTask<Guid[]> Remove(Action handler)
Removes shortcuts matching the exact same delegate instance; returns the removed ids.
Remove
ValueTask Remove(Guid id)
Removes one shortcut by the id returned from Add.
RemoveAll
ValueTask RemoveAll()
Removes every shortcut registered on this Keyboard instance.
DisposeAsync
ValueTask DisposeAsync()
Removes all shortcuts and releases the interop reference. Called by DI when the app scope tears down.
InvokeKeyboard
void InvokeKeyboard(Guid id)
JSInvokable interop entry point invoked from the browser when a shortcut fires - not intended for direct application use.
ButilModifiers
enum ButilModifiers { None, Alt, Ctrl, Meta, Shift }
Flags enum of modifier keys; combine values with the bitwise or operator.
An unhandled error has occurred. Reload 🗙