Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

On-Screen Keyboard

A focused on-screen keyboard for touch tablets and kiosks. It auto-shows when a text input takes focus, docks along the bottom edge, and types into whatever is focused — without ever taking the caret off it.

Scope is deliberately narrow: English US-QWERTY, typing into the host app’s own text fields, no IME and no dead-key composition. The 80% of a kiosk, not a replacement for the OS on-screen keyboard.

  • NuGet downloads for Shiny.Blazor.Controls
Frameworks
Blazor

Blazor

Auto-shown on focus Typing into a password field
The QWERTY keyboard docked along the bottom edge with Shiny typed into the focused input The same keyboard following focus into a password field
Shiny.Maui.Controls.DesktopNuGet package Shiny.Maui.Controls.Desktop

No add-on package — it is in the main Blazor controls package alongside Docking.

Program.cs — the umbrella call covers the keyboard along with every other service-backed control (Toast, Dialogs, the splash screen, the walkthrough store and docking):

using Shiny.Blazor.Controls;
builder.Services.AddShinyControls(cfg => cfg
.ConfigureKeyboard(opts =>
{
opts.AutoShowOnFocus = true;
opts.AutoHideOnBlur = true;
opts.HeightPx = 280;
opts.PushContent = true; // pad the body out from under the keys
opts.Theme = OnScreenKeyboardTheme.Auto;
})
);

Or register just the keyboard:

using Shiny.Blazor.Controls.OnScreenKeyboard;
builder.Services.AddShinyOnScreenKeyboard(opts => opts.HeightPx = 280);

Both are TryAdd, so calling both is safe — the first registration wins.

_Imports.razor:

@using Shiny.Blazor.Controls.OnScreenKeyboard

Then place exactly one host, near the root of the layout (typically MainLayout.razor):

<OnScreenKeyboardHost />

That is the whole setup. The host is position: fixed, so it does not matter where in the layout it sits — but it does watch focus for the entire document, so put one in the app rather than one per page.

Inject IOnScreenKeyboardService anywhere:

@inject IOnScreenKeyboardService Keyboard
<button @onclick="() => Keyboard.Show()">Kiosk mode</button>
public interface IOnScreenKeyboardService
{
bool IsVisible { get; }
event EventHandler<bool>? VisibilityChanged;
void Show();
void Hide();
void Toggle();
}

A kiosk screen usually turns auto-show off and pins the keyboard up for the life of the screen — otherwise AutoHideOnBlur will drop it the moment the user taps one of your own buttons, because that genuinely moves focus off the field.

OnScreenKeyboardOptions is live. Configure it once at startup, or inject it and change it at runtime — the host picks the new values up on its next render.

public sealed class OnScreenKeyboardOptions
{
public bool AutoShowOnFocus { get; set; } = true;
public bool AutoHideOnBlur { get; set; } = true;
public double HeightPx { get; set; } = 280;
public bool PushContent { get; set; } = true; // false = overlay the content
public OnScreenKeyboardTheme Theme { get; set; } = OnScreenKeyboardTheme.Auto;
public bool EnterInsertsNewLine { get; set; } // textarea only; see below
public TimeSpan AutoRepeatDelay { get; set; } = TimeSpan.FromMilliseconds(400);
public TimeSpan AutoRepeatInterval { get; set; } = TimeSpan.FromMilliseconds(50);
}

Both the options and the service are registered scoped, not singleton — they are per-user state. Under WebAssembly the two lifetimes are identical; on Blazor Server a singleton would apply one user’s settings, and one user’s visible keyboard, to everyone. The configure delegate therefore runs once per scope against that scope’s own options instance.

PushContent pads the document body out from under the keyboard. That is only half the problem, so the host also measures the focused field against the keyboard’s top edge and scrolls it clear — in whichever container actually scrolls, not just the body. Apps whose content scrolls inside a shell (an AppLayout content region, a dialog) get the second behaviour whether or not they take the first.

US-QWERTY, plus a symbols layer behind the 123 key.

Layer Keys
Letters ` 1 2 3 4 5 6 7 8 9 0 - = ⌫ / ⇥ q w e r t y u i o p [ ] \ / ⇪ a s d f g h j k l ; ' ⏎ / ⇧ z x c v b n m , . /
Symbols (123) Digits and - = + / ~ ! @ # $ % ^ & * ( ) _ [ ] \ / € £ ¥ ¢ ° ± × ÷ { } | : ; ⏎ / « » " ' < > ? / , . • – — … ¡
Bottom row (both) 123/ABC · , · space · . · ◀ ▼ ▲ ▶ · ⌄ (hide)

Modifiers behave like a real keyboard. is momentary — it drops after one character. is sticky, and it only raises the letters, so the number row keeps its digits. The 123 toggle is sticky too. All three light up while engaged.

Holding a character, , space or an arrow auto-repeats after AutoRepeatDelay, and a held key keeps repeating the character it typed first rather than dropping out of shift on the second repeat.

1. No focus stealing. Every key cancels pointerdown, on the key itself and on the container, so the browser never runs its focus default and the caret stays in the field the user is typing into. Get this wrong and the target input loses its caret the moment the first key is tapped — the single biggest cause of “the OSK does nothing” reports.

2. Caret and selection. Typing goes in through execCommand('insertText'), which replaces the selection and leaves the undo stack intact, with a deterministic value splice behind it for browsers that decline. Because a programmatic value assignment does not set the element’s dirty flag, the fallback path raises both input and change itself — so a plain @bind and a @bind:event="oninput" both see the keystroke.

3. Arrow keys know about lines. ▲ / ▼ in a <textarea> walk to the same column on the adjacent line rather than jumping one character, and contenteditable is left to the selection API.

On a single-line <input>, Enter dispatches real keydown / keyup events and then submits the containing form — unless a handler cancelled the keydown, in which case it does not. Typing a literal newline into a single-line field is meaningless, so it never does that.

In a <textarea>, set EnterInsertsNewLine = true to type a newline instead.

CSS custom properties — set them on any ancestor, or on <OnScreenKeyboardHost> directly:

--shiny-osk-bg, --shiny-osk-border, --shiny-osk-key-bg, --shiny-osk-key-fg, --shiny-osk-modifier-bg, --shiny-osk-key-pressed-bg, --shiny-osk-key-pressed-fg, --shiny-osk-height, --shiny-osk-gap, --shiny-osk-radius.

OnScreenKeyboardTheme.Auto — the default — resolves those from the app’s Shiny theme tokens, so it tracks a runtime theme switch. Light and Dark pin a fixed palette regardless of what the rest of the app is doing, which is what a kiosk usually wants.

Reduced-motion preferences are honoured: the slide-in transition is dropped entirely.

  • DOM inputs only. The keyboard types into <input> (text-ish types), <textarea> and contenteditable inside this app. It cannot inject into another window, another process, or a cross-origin <iframe>. For a kiosk that is the desired behaviour.
  • Shadow DOMfocusin does not pierce shadow roots, so Web Components with an internal <input> are not detected.
  • Rich editors (Quill, ProseMirror, Monaco) — insertText works against plain inputs and simple contenteditable, but selection behaviour inside a full editor framework is best-effort.
  • English US-QWERTY only. No IME, no dead keys, no language switching.
  • No Ctrl / Alt. Modifier chords are not dispatched; there are no inert keys on the board pretending otherwise.

The keyboard is a role="application" region with an accessible name, and each key is a button with an aria-label matching what it will actually type in the current shift state. The three latching keys (, , 123) report aria-pressed.

Keys are tabindex="-1" on purpose: they must not enter the tab order, because taking focus is exactly what the keyboard exists to avoid. Screen-reader users on a device with this keyboard are using the real one; the ARIA tree is there so the board is describable, not so it is tab-navigable.

Nothing below exists. It is recorded so the shape can be reviewed.

using Shiny;
using Shiny.Maui.Controls.Desktop.OnScreenKeyboard;
builder
.UseMauiApp<App>()
.UseOnScreenKeyboard(opts =>
{
opts.AutoShowOnFocus = true;
opts.Height = 280;
opts.PushContent = true;
});
<!-- Inline use, e.g. a kiosk page that always shows the keyboard -->
<Grid RowDefinitions="*,Auto">
<osk:OnScreenKeyboardView Grid.Row="1" Height="280" />
</Grid>

It would ship in Shiny.Maui.Controls.Desktop alongside Tray Icon and Docking, exposing IOnScreenKeyboard with the same Show / Hide / Toggle / IsVisible / VisibilityChanged surface, themed through ResourceDictionary keys (OnScreenKeyboardKeyBrush and friends) rather than CSS properties.

Note that this is not the keyboard accessory bar — that decorates the OS keyboard on iOS and Android. This one draws its own keys, for platforms where there is no OS keyboard to decorate.

  • Docking — ships in the same Blazor package
  • Tray Icon — the MAUI desktop add-on the planned MAUI half would join