1import { type ChangeEvent, useRef, useState } from "react";2
3import { Input } from "@/components/ui/pxl/input";4import { Keyboard, type KeyboardRef } from "@/components/ui/pxl/keyboard";5
6export default function KeyboardPreview() {7 const [value, setValue] = useState("");8 const input = useRef<HTMLInputElement>(null);9 const keyboard = useRef<KeyboardRef>(null);10
11 const onChangeInput = (event: ChangeEvent<HTMLInputElement>): void => {12 setValue(event.target.value);13 keyboard.current?.setInput(event.target.value);14 };15
16 const onKbChange = (value: string) => {17 setValue(value);18
19 requestAnimationFrame(() => {20 input.current?.focus();21 });22 }23
24 return (25 <div className="flex flex-col gap-2.5 items-center justify-center">26 <div className="size-full flex flex-col gap-2">27 <Input28 autoFocus29 ref={input}30 value={value}31 placeholder={"Tap on the virtual keyboard to start"}32 onChange={onChangeInput}33 name="preview"34 />35 <Keyboard36 inputName="preview"37 enableKeyNavigation38 keyboardRef={keyboard}39 onChange={onKbChange}40 onSubmit={() => console.log("onSubmit")}41 />42 </div>43 </div>44 );45}The <Keyboard /> component uses the simple-keyboard component by hodgef.
Installation
Section titled “Installation”pnpm dlx shadcn@latest add pxl-ui/registry/keyboard
Install the following dependencies:
pnpm add react-simple-keyboard simple-keyboard-key-navigation
Copy and paste the following code into your project.
1"use client";2
3import { cva, type VariantProps } from "class-variance-authority";4import { type RefObject, useEffect, useMemo, useState } from "react";5import SimpleKeyboard, {6 type KeyboardOptions,7 type SimpleKeyboard as KeyboardRef,8} from "react-simple-keyboard";9
10import { buttonVariants } from "@/components/ui/pxl/button";11import { cn } from "@/lib/utils";12
13type Language = "en" | "es";14type KeyboardDisplay = "desktop" | "mobile" | "alphabetical";15type KeyboardLayout = "default" | "shift" | "numbers";16type KeyboardLayoutConfig = Record<KeyboardLayout, string[]>;17
18let keyNavigationModule: any;19
20if (typeof window !== "undefined") {21 // @ts-expect-error simple-keyboard-key-navigation doesn't provide types22 keyNavigationModule = (await import("simple-keyboard-key-navigation")).default23 .default;24}25
26const keyboardVariants = cva("hg-theme-pxl", {27 variants: {28 font: {29 default: "font-sans",30 heading: "font-heading",31 sans: "font-sans",32 mono: "font-mono",33 },34 size: {35 default: "[--keyboard-spacing:--spacing(1)]",36 "3xs": "[--keyboard-spacing:--spacing(0.25)]",37 "2xs": "[--keyboard-spacing:--spacing(0.25)]",38 xs: "[--keyboard-spacing:--spacing(0.5)]",39 sm: "[--keyboard-spacing:--spacing(0.5)]",40 md: "[--keyboard-spacing:--spacing(1)]",41 lg: "[--keyboard-spacing:--spacing(1)]",42 },43 variant: {44 default:45 "keyboard-highlight-bg-foreground/80 keyboard-highlight-text-background keyboard-shift-text-foreground keyboard-shift-bg-background",46 primary:47 "keyboard-highlight-bg-primary/80 keyboard-highlight-text-primary-foreground keyboard-shift-text-primary keyboard-shift-bg-primary-foreground",48 outline:49 "keyboard-highlight-bg-muted keyboard-highlight-text-foreground keyboard-shift-text-muted keyboard-shift-bg-foreground",50 secondary:51 "keyboard-highlight-bg-secondary/80 keyboard-highlight-text-secondary-foreground keyboard-shift-text-secondary keyboard-shift-bg-secondary-foreground",52 ghost:53 "keyboard-highlight-bg-muted keyboard-highlight-text-foreground keyboard-shift-text-background keyboard-shift-bg-foreground",54 destructive:55 "keyboard-highlight-bg-destructive/20 keyboard-highlight-text-destructive keyboard-shift-text-destructive/20 keyboard-shift-bg-destructive",56 info: "keyboard-highlight-bg-info/80 keyboard-highlight-text-info-foreground keyboard-shift-text-info keyboard-shift-bg-info-foreground",57 success:58 "keyboard-highlight-bg-success/80 keyboard-highlight-text-success-foreground keyboard-shift-text-success keyboard-shift-bg-success-foreground",59 warning:60 "keyboard-highlight-bg-warning/80 keyboard-highlight-text-warning-foreground keyboard-shift-text-warning keyboard-shift-bg-warning-foreground",61 danger:62 "keyboard-highlight-bg-danger/80 keyboard-highlight-text-danger-foreground keyboard-shift-text-danger keyboard-shift-bg-danger-foreground",63 },64 },65 defaultVariants: {66 font: "default",67 size: "default",68 variant: "default",69 },70});71
72const BUTTON_CHARS = {73 ABC: "{abc}",74 ALT_LEFT: "{altleft}",75 ALT_RIGHT: "{altright}",76 BACKSPACE: "{backspace}",77 BKSP: "{bksp}",78 CTRL_LEFT: "{controlleft}",79 CTRL_RIGHT: "{controlright}",80 ENT: "{ent}",81 ENTER: "{enter}",82 ESCAPE: "{escape}",83 LOCK: "{lock}",84 CAPSLOCK: "{capslock}",85 META_LEFT: "{metaleft}",86 META_RIGHT: "{metaright}",87 NUMBERS: "{numbers}",88 SHIFT: "{shift}",89 SPACE: "{space}",90 TAB: "{tab}",91};92
93const keyboardLayouts: Record<94 Language,95 Record<KeyboardDisplay, KeyboardLayoutConfig>96> = {97 en: {98 desktop: {99 default: [100 "` 1 2 3 4 5 6 7 8 9 0 - = {bksp}",101 "{tab} q w e r t y u i o p [ ] \\",102 "{lock} a s d f g h j k l ; ' {enter}",103 "{shift} z x c v b n m , . / {shift}",104 ".com @ {space}",105 ],106 shift: [107 "~ ! @ # $ % ^ & * ( ) _ + {bksp}",108 "{tab} Q W E R T Y U I O P { } |",109 '{lock} A S D F G H J K L : " {enter}',110 "{shift} Z X C V B N M < > ? {shift}",111 ".com @ {space}",112 ],113 numbers: [],114 },115 mobile: {116 default: [117 "q w e r t y u i o p",118 "a s d f g h j k l",119 "{shift} z x c v b n m {backspace}",120 "{numbers} {space} {ent}",121 ],122 shift: [123 "Q W E R T Y U I O P",124 "A S D F G H J K L",125 "{shift} Z X C V B N M {backspace}",126 "{numbers} {space} {ent}",127 ],128 numbers: ["1 2 3", "4 5 6", "7 8 9", "{abc} 0 {backspace}"],129 },130 alphabetical: {131 default: [132 "a b c d e f g",133 "h i j k l m n",134 "o p q r s t u",135 "{shift} v w x y z {backspace}",136 "{numbers} {space} {ent}",137 ],138 shift: [139 "A B C D E F G",140 "H I J K L M N",141 "O P Q R S T U",142 "{shift} V W X Y Z {backspace}",143 "{numbers} {space} {ent}",144 ],145 numbers: ["1 2 3", "4 5 6", "7 8 9", "{abc} 0 {backspace}"],146 },147 },148 es: {149 desktop: {150 default: [151 "\u007c 1 2 3 4 5 6 7 8 9 0 ' \u00bf {bksp}",152 "{tab} q w e r t y u i o p \u0301 +",153 "{lock} a s d f g h j k l \u00f1 \u007b \u007d {enter}",154 "{shift} < z x c v b n m , . - {shift}",155 ".com @ {space}",156 ],157 shift: [158 '\u00b0 ! " # $ % & / ( ) = ? \u00a1 {bksp}',159 "{tab} Q W E R T Y U I O P \u0308 *",160 "{lock} A S D F G H J K L \u00d1 \u005b \u005d {enter}",161 "{shift} > Z X C V B N M ; : _ {shift}",162 ".com @ {space}",163 ],164 numbers: [],165 },166 mobile: {167 default: [168 "q w e r t y u i o p",169 "a s d f g h j k l ñ",170 "{shift} z x c v b n m {backspace}",171 "{numbers} {space} {ent}",172 ],173 shift: [174 "Q W E R T Y U I O P",175 "A S D F G H J K L Ñ",176 "{shift} Z X C V B N M {backspace}",177 "{numbers} {space} {ent}",178 ],179 numbers: ["1 2 3", "4 5 6", "7 8 9", "{abc} 0 {backspace}"],180 },181 alphabetical: {182 default: [183 "a b c d e f g",184 "h i j k l m n",185 "ñ o p q r s t",186 "{shift} u v w x y z {backspace}",187 "{numbers} {space} {ent}",188 ],189 shift: [190 "A B C D E F G",191 "H I J K L M N",192 "Ñ O P Q R S T",193 "{shift} U V W X Y Z {backspace}",194 "{numbers} {space} {ent}",195 ],196 numbers: ["1 2 3", "4 5 6", "7 8 9", "{abc} 0 {backspace}"],197 },198 },199};200
201const keyboardDisplays: Record<202 Language,203 Partial<Record<KeyboardDisplay, Record<string, string>>>204> = {205 en: {206 mobile: {207 [BUTTON_CHARS.ABC]: "ABC",208 [BUTTON_CHARS.ALT_LEFT]: "alt ⌥",209 [BUTTON_CHARS.ALT_RIGHT]: "alt ⌥",210 [BUTTON_CHARS.BACKSPACE]: "⌫",211 [BUTTON_CHARS.CTRL_LEFT]: "ctrl ⌃",212 [BUTTON_CHARS.CTRL_RIGHT]: "ctrl ⌃",213 [BUTTON_CHARS.ENT]: "Confirm",214 [BUTTON_CHARS.ESCAPE]: "esc ⎋",215 [BUTTON_CHARS.CAPSLOCK]: "caps lock ⇪",216 [BUTTON_CHARS.META_LEFT]: "cmd ⌘",217 [BUTTON_CHARS.META_RIGHT]: "cmd ⌘",218 [BUTTON_CHARS.NUMBERS]: "123",219 [BUTTON_CHARS.SHIFT]: "⇧",220 [BUTTON_CHARS.TAB]: "tab ⇥",221 },222 alphabetical: {223 [BUTTON_CHARS.ABC]: "ABC",224 [BUTTON_CHARS.ALT_LEFT]: "alt ⌥",225 [BUTTON_CHARS.ALT_RIGHT]: "alt ⌥",226 [BUTTON_CHARS.BACKSPACE]: "⌫",227 [BUTTON_CHARS.CTRL_LEFT]: "ctrl ⌃",228 [BUTTON_CHARS.CTRL_RIGHT]: "ctrl ⌃",229 [BUTTON_CHARS.ENT]: "Confirm",230 [BUTTON_CHARS.ESCAPE]: "esc ⎋",231 [BUTTON_CHARS.CAPSLOCK]: "caps lock ⇪",232 [BUTTON_CHARS.META_LEFT]: "cmd ⌘",233 [BUTTON_CHARS.META_RIGHT]: "cmd ⌘",234 [BUTTON_CHARS.NUMBERS]: "123",235 [BUTTON_CHARS.SHIFT]: "⇧",236 [BUTTON_CHARS.TAB]: "tab ⇥",237 },238 },239 es: {240 mobile: {241 [BUTTON_CHARS.ABC]: "ABC",242 [BUTTON_CHARS.ALT_LEFT]: "alt ⌥",243 [BUTTON_CHARS.ALT_RIGHT]: "alt ⌥",244 [BUTTON_CHARS.BACKSPACE]: "⌫",245 [BUTTON_CHARS.CTRL_LEFT]: "ctrl ⌃",246 [BUTTON_CHARS.CTRL_RIGHT]: "ctrl ⌃",247 [BUTTON_CHARS.ENT]: "Confirmar",248 [BUTTON_CHARS.ESCAPE]: "esc ⎋",249 [BUTTON_CHARS.CAPSLOCK]: "caps lock ⇪",250 [BUTTON_CHARS.META_LEFT]: "cmd ⌘",251 [BUTTON_CHARS.META_RIGHT]: "cmd ⌘",252 [BUTTON_CHARS.NUMBERS]: "123",253 [BUTTON_CHARS.SHIFT]: "⇧",254 [BUTTON_CHARS.TAB]: "tab ⇥",255 },256 alphabetical: {257 [BUTTON_CHARS.ABC]: "ABC",258 [BUTTON_CHARS.ALT_LEFT]: "alt ⌥",259 [BUTTON_CHARS.ALT_RIGHT]: "alt ⌥",260 [BUTTON_CHARS.BACKSPACE]: "⌫",261 [BUTTON_CHARS.CTRL_LEFT]: "ctrl ⌃",262 [BUTTON_CHARS.CTRL_RIGHT]: "ctrl ⌃",263 [BUTTON_CHARS.ENT]: "Confirmar",264 [BUTTON_CHARS.ESCAPE]: "esc ⎋",265 [BUTTON_CHARS.CAPSLOCK]: "caps lock ⇪",266 [BUTTON_CHARS.META_LEFT]: "cmd ⌘",267 [BUTTON_CHARS.META_RIGHT]: "cmd ⌘",268 [BUTTON_CHARS.NUMBERS]: "123",269 [BUTTON_CHARS.SHIFT]: "⇧",270 [BUTTON_CHARS.TAB]: "tab ⇥",271 },272 },273};274
275const sizedDisplays: Record<276 NonNullable<VariantProps<typeof keyboardVariants>["size"]>,277 KeyboardDisplay278> = {279 default: "mobile",280 "3xs": "mobile",281 "2xs": "mobile",282 xs: "mobile",283 sm: "mobile",284 md: "mobile",285 lg: "desktop",286};287
288function Keyboard({289 border = "default",290 className,291 display = "auto",292 enableKeyNavigation,293 font,294 keyboardRef,295 language = "en",296 onChange,297 onLayoutChange,298 onSubmit,299 size = "default",300 variant = "default",301 ...props302}: Omit<KeyboardOptions, "display"> &303 VariantProps<typeof keyboardVariants> &304 Pick<VariantProps<typeof buttonVariants>, "border" | "variant"> & {305 className?: string;306 /** Enables the key navigation module */307 keyNavigation?: boolean;308 keyboardRef?: RefObject<KeyboardRef | null>;309 /** The keyboard display. "auto" will resolve a display based on the size */310 display?: KeyboardDisplay | "auto";311 language?: Language;312 onChange?: (input: string) => void;313 onLayoutChange?: (layout: KeyboardLayout) => void;314 onSubmit?: () => void;315 }) {316 const [layoutName, setLayoutName] = useState<KeyboardLayout>("default");317
318 const config = useMemo(() => {319 const resolvedDisplay =320 display === "auto" ? sizedDisplays[size ?? "default"] : display;321
322 const layout = keyboardLayouts[language][resolvedDisplay];323
324 return {325 display: keyboardDisplays[language][resolvedDisplay],326 layout,327 allButtons: [328 ...layout.default,329 ...layout.numbers,330 ...layout.shift,331 ].join(" "),332 };333 }, [language, display, size]);334
335 const modules = useMemo(() => {336 if (enableKeyNavigation) {337 return [keyNavigationModule];338 }339 }, [enableKeyNavigation]);340
341 const buttonSize = useMemo<342 VariantProps<typeof buttonVariants>["size"]343 >(() => {344 if (!size) {345 return null;346 }347
348 return {349 default: "sm",350 "3xs": "4xs",351 "2xs": "3xs",352 xs: "2xs",353 sm: "xs",354 md: "sm",355 lg: "md",356 }[size] as VariantProps<typeof buttonVariants>["size"];357 }, [size]);358
359 function onKeyPress(button: string) {360 if (button === "{shift}" || button === "{lock}") {361 const nextLayout = layoutName === "shift" ? "default" : "shift";362 setLayoutName(nextLayout);363 onLayoutChange?.(nextLayout);364 }365
366 if (button === "{numbers}" || button === "{abc}") {367 const nextLayout = layoutName === "numbers" ? "default" : "numbers";368 setLayoutName(nextLayout);369 onLayoutChange?.(nextLayout);370 }371
372 if (button === "{ent}") {373 onSubmit?.();374 }375 }376
377 useEffect(378 function handleKeyNavigation() {379 if (!enableKeyNavigation) {380 return;381 }382
383 function onKeyDown(e: KeyboardEvent) {384 if (e.key === "ArrowUp")385 keyboardRef?.current?.modules.keyNavigation.up();386 else if (e.key === "ArrowDown")387 keyboardRef?.current?.modules.keyNavigation.down();388 else if (e.key === "ArrowLeft")389 keyboardRef?.current?.modules.keyNavigation.left();390 else if (e.key === "ArrowRight")391 keyboardRef?.current?.modules.keyNavigation.right();392 else if (e.key === "Enter")393 keyboardRef?.current?.modules.keyNavigation.press();394 }395
396 document.addEventListener("keydown", onKeyDown, false);397
398 return () => document.removeEventListener("keydown", onKeyDown);399 },400 [enableKeyNavigation, keyboardRef],401 );402
403 return (404 <SimpleKeyboard405 buttonTheme={[406 {407 class: buttonVariants({408 variant,409 size: buttonSize,410 border,411 }),412 buttons: config.allButtons,413 },414 ]}415 display={config.display}416 keyboardRef={(r) => {417 if (keyboardRef) {418 keyboardRef.current = r;419 }420 }}421 layoutName={layoutName}422 layout={config.layout}423 mergeDisplay424 theme={cn(425 keyboardVariants({426 font,427 size,428 variant,429 }),430 className,431 )}432 enableKeyNavigation={enableKeyNavigation}433 modules={modules}434 onChange={onChange}435 onKeyPress={onKeyPress}436 physicalKeyboardHighlight437 physicalKeyboardHighlightBgColor="var(--keyboard-highlight)"438 physicalKeyboardHighlightTextColor="var(--keyboard-highlight-foreground)"439 useMouseEvents440 onModulesLoaded={(keyboard: KeyboardRef) => {441 /**442 * Optional: If keyboard.modules is not available below.443 * You can call module methods here444 * e.g: keyboard.modules.keyNavigation.up();445 * etc.446 */447 keyboard.modules.keyNavigation.up();448 }}449 {...props}450 />451 );452}453
454export { Keyboard, type KeyboardRef, keyboardLayouts };Update the import paths to match your project setup.
import { Keyboard } from "@/components/ui/pxl/keyboard"<Keyboard />Variants
Section titled “Variants”Use the variant prop to change the variant of the keyboard.
Primary
Success
Danger
Warning
Info
Secondary
Outline
Ghost
1import { Keyboard } from "@/components/ui/pxl/keyboard";2
3export default function KeyboardVariants() {4 return (5 <div className="w-full flex flex-col gap-2.5 items-start justify-center">6 <div>7 <h2>Primary</h2>8 <Keyboard9 inputName="primary"10 variant="primary"11 physicalKeyboardHighlight={false}12 className="bg-popover p-1.25 rounded-md" />13 </div>14 <div>15 <h2>Success</h2>16 <Keyboard17 inputName="success"18 variant="success"19 physicalKeyboardHighlight={false}20 className="bg-popover p-1.25 rounded-md" />21 </div>22 <div>23 <h2>Danger</h2>24 <Keyboard25 inputName="danger"26 variant="danger"27 physicalKeyboardHighlight={false}28 className="bg-popover p-1.25 rounded-md" />29 </div>30 <div>31 <h2>Warning</h2>32 <Keyboard33 inputName="warning"34 variant="warning"35 physicalKeyboardHighlight={false}36 className="bg-popover p-1.25 rounded-md" />37 </div>38 <div>39 <h2>Info</h2>40 <Keyboard41 inputName="info"42 variant="info"43 physicalKeyboardHighlight={false}44 className="bg-popover p-1.25 rounded-md" />45 </div>46 <div>47 <h2>Secondary</h2>48 <Keyboard49 inputName="secondary"50 variant="secondary"51 physicalKeyboardHighlight={false}52 className="bg-popover p-1.25 rounded-md" />53 </div>54 <div>55 <h2>Outline</h2>56 <Keyboard57 inputName="outline"58 variant="outline"59 physicalKeyboardHighlight={false}60 className="bg-popover p-1.25 rounded-md" />61 </div>62 <div>63 <h2>Ghost</h2>64 <Keyboard65 inputName="ghost"66 variant="ghost"67 physicalKeyboardHighlight={false}68 className="bg-popover p-1.25 rounded-md" />69 </div>70 </div>71 );72}Use the size prop to change the size of the keyboard.
2XS
XS
SM
MD
LG
1import { Keyboard } from "@/components/ui/pxl/keyboard";2
3export default function KeyboardSizes() {4 return (5 <div className="w-full min-h-92 flex flex-col gap-2.5 items-start justify-center">6 <div>7 <h2>2XS</h2>8 <Keyboard9 inputName="2xs"10 size="2xs"11 physicalKeyboardHighlight={false}12 className="bg-popover" />13 </div>14 <div>15 <h2>XS</h2>16 <Keyboard17 inputName="xs"18 size="xs"19 physicalKeyboardHighlight={false}20 className="bg-popover" />21 </div>22 <div>23 <h2>SM</h2>24 <Keyboard25 inputName="sm"26 size="sm"27 physicalKeyboardHighlight={false}28 className="bg-popover" />29 </div>30 <div>31 <h2>MD</h2>32 <Keyboard33 inputName="md"34 size="md"35 physicalKeyboardHighlight={false}36 className="bg-popover" />37 </div>38 <div>39 <h2>LG</h2>40 <Keyboard41 inputName="lg"42 size="lg"43 physicalKeyboardHighlight={false}44 className="bg-popover" />45 </div>46 </div>47 );48}