Kurdish Keyboard

Open source · Kurdish text input · v0.4.0

Kurdish Keyboard

Type Kurdish anywhere on the web, period.

An embeddable virtual keyboard for Sorani (Arabic script) and Kurmanji (Latin script). One widget, with unified Unicode that is correct by construction.

npm install kurdish-keyboard
scripts
2
layouts
3
runtime dependencies
0
verified codepoints
55

Kurmanji, QWERTY + row

QWERTY with an extra row for ç ê î ş û, left to right.

Sorani

Layout from Unicode CLDR data, right to left.

Why

One letter, many codes

Kurdish text on the web has decades of broken encodings. Letters that look the same have different codes: ك and ک, ي and ی, ه and ە. This silently breaks search, sorting and spellcheck. This keyboard types only the unified codepoints from the KRG Department of IT standard, checked against the Unicode Character Database. So all text typed with it is correct by construction.

Live demo

No screenshots. This is the widget.

The keyboards on this page are not images. They are the real package, themed with CSS custom properties. Pick a layout and type. ⇧ shows the second layer; the Latin layouts add ⇪ Caps Lock. Arrow keys move through the keys, and screen readers can use it.

Features

Everything a Kurdish input needs

  1. Unified Unicode

    Correct by construction.

    The widget can only type the codepoints verified against the KRG standard and the UCD: ک never ك, ی never ي, and ە as the vowel it is. Broken text stays in the past.

  2. Two scripts, three layouts

    Right to left and left to right, one engine.

    A Sorani layout made from Unicode CLDR data, plus two community Kurmanji layouts. Switch live with setLayout(); direction, layers and Caps Lock follow.

  3. Made for mobile

    The widget is the keyboard.

    Per field, inputmode="none" keeps the phone keyboard hidden while a docked or floating panel does the typing. Touch targets grow to 44px from a capability query, never from user-agent sniffing.

  4. Rich text too

    Contenteditable and editors.

    Plain contenteditable works out of the box. Framework editors like Lexical, ProseMirror and Slate connect with a small insertion adapter.

  5. Accessible by default

    Roving tabindex, spoken names.

    WAI-ARIA keyboard navigation, visible focus, and screen-reader labels in English or Kurdish, switchable at runtime.

  6. Legacy text cleaner

    Clean decades of ك and ي.

    normalizeLegacy() changes old, broken Sorani text to the unified codepoints, from the same data file the keyboard uses. It needs no DOM, so it also runs in Node and in CLIs.

Principles

How it is built

  1. Standards, not invention

    The codepoints come from the KRG unified keyboard standard, the Sorani layout from Unicode CLDR. Where a standard exists, nothing is made by hand.

  2. Data, not code

    Layouts are JSON files, checked against a schema. The engine never looks at a layout's name. A new layout is new data, not new code.

  3. The platform is enough

    setRangeText to type, inputmode for phones, custom properties for theming. Zero runtime dependencies, ESM and CJS, tree-shakable layouts.

  4. Verified, not assumed

    Every codepoint is checked letter by letter against primary sources. Before each release, an executable spec proves the published package.

Guide

Integration guide

Everything you need to add the keyboard to your site. Each example runs live next to its code.

Quick start

Attach, type, detach

One import per layout, one call. The panel appears after the field. A key types at the caret, replaces any selection, and keeps the focus in the field. kb.detach() removes the panel and undoes every side effect.

import { KurdishKeyboard } from "kurdish-keyboard";
import "kurdish-keyboard/styles.css"; // optional, recommended
import { soraniLayout } from "kurdish-keyboard/layouts/sorani";

const kb = KurdishKeyboard.attachTo(document.querySelector("input"), {
  layout: soraniLayout,
});
// later: kb.detach()

Live

OptionTypeDefaultWhat it does
layoutLayoutminimal panelThe layout to show. Import one per script, or give your own.
adapterInsertionAdapterbuilt-inYour own insertion for framework editors. See “Rich text” below.
suppressNativebooleanfalseSets inputmode="none", so phones keep their own keyboard hidden.
position'inline' | 'docked' | 'floating''inline'Where the panel goes. See “Mobile” below.
labelLanguage'en' | 'ku''en'Language of the screen-reader labels (Shift, Space, …).

Layouts

Three layouts, live switching

One entry point per layout, so a Sorani-only site never bundles the Kurmanji data. setLayout() switches live: the panel redraws, the direction flips between right to left and left to right, and Shift and Caps Lock reset. Latin layouts get ⇪ Caps Lock; Shift works for one key everywhere.

Your own layout: a layout is plain data. The Layout type is exported, and the JSON is checked against the published schema. Keys type strings, not characters: CLDR's Shift+و is وو. The verified codepoint table ships as kurdish-keyboard/codepoints.json. Right-to-left fields: the panel sets its own direction, but the field's dir is yours. Use dir="rtl" or dir="auto" for Sorani fields; the widget logs a hint if you forget.

import { soraniLayout } from "kurdish-keyboard/layouts/sorani";
import { kurmanjiPhonetic } from "kurdish-keyboard/layouts/kurmanji-phonetic";
import { kurmanjiOfficial } from "kurdish-keyboard/layouts/kurmanji-official";

kb.setLayout(kurmanjiPhonetic); // live swap, LTR
kb.setLayout(soraniLayout); // back to RTL

Live

Rich text

Contenteditable and your own adapter

The panel attaches to contenteditable elements out of the box. Typing goes through the browser's own editing commands, so the caret, selection and input events act like real keystrokes. One rule: keys only work while the selection is inside the attached element. With the caret somewhere else, a key does nothing; it never types into the wrong field.

Framework editors such as Lexical, ProseMirror and Slate undo outside edits by design. For them, give an adapter. insertChar and deleteBack are required; insertLineBreak and insertParagraph are optional. The panel shows a key only for the operations the adapter has: no insertParagraph, no ¶ key. The built-in adapters are exported too (valueAdapter, contentEditableAdapter), if you want to wrap one.

// Plain contenteditable: nothing new to learn
KurdishKeyboard.attachTo(document.querySelector("[contenteditable]"), {
  layout: soraniLayout,
});

// Lexical (e.g. Payload CMS): supply an editor-native adapter
import {
  $getSelection,
  DELETE_CHARACTER_COMMAND,
  INSERT_LINE_BREAK_COMMAND,
  INSERT_PARAGRAPH_COMMAND,
} from "lexical";

KurdishKeyboard.attachTo(editorRootElement, {
  layout: soraniLayout,
  adapter: {
    insertChar: (text) =>
      editor.update(() => $getSelection()?.insertText(text)),
    deleteBack: () => editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true),
    insertLineBreak: () =>
      editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false),
    insertParagraph: () =>
      editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined),
  },
});

Live

Editable area (Sorani)

Mobile

Be the keyboard

suppressNative sets inputmode="none", the standard way to tell a phone “this page has its own keyboard”. The cost is the phone's autocorrect for that field, so it is opt-in per field. docked fixes the panel to the bottom of the screen and keeps the field visible; floating keeps it near the corner. Touch targets grow to 44px on touch screens.

KurdishKeyboard.attachTo(field, {
  layout: soraniLayout,
  suppressNative: true,
  position: "docked", // or "floating"
});

Live

On a computer you see the fixed panel. On a phone, inputmode="none" also keeps the phone keyboard away.

Theming

Custom properties, no CSS-in-JS

Import the stylesheet once, then set --kkb-* properties on the panel or on any parent. The stylesheet is optional; the widget also works without it. The two keyboards below differ only in custom properties.

The internal --_* properties are derived. Change only the public ones.

.theme-night {
  --kkb-key-bg: #1e293b;
  --kkb-key-color: #e2e8f0;
  --kkb-key-border: #334155;
  --kkb-pressed-bg: #334155;
  --kkb-focus-color: #38bdf8;
  --kkb-surface-bg: #1e293b;
}

Default

Night, with --kkb-* only

PropertyDefaultStyles
--kkb-gap4pxSpace between keys and rows
--kkb-key-size2.2rem · 44px on touch screensSmallest key width and height
--kkb-key-font-size1.1rem · 1.3rem on touch screensKey label size
--kkb-key-bgButtonFaceKey background
--kkb-key-colorButtonTextKey label colour
--kkb-key-borderButtonText 40%Key border colour
--kkb-key-radius4pxKey corner radius
--kkb-pressed-bg#cbdceeBackground of active Shift and Caps Lock
--kkb-focus-color#1a56a0Keyboard focus ring
--kkb-surface-bgCanvasBackground of the docked or floating panel

Accessibility

Keyboard navigation, spoken names

The panel is a labelled role="group" of native buttons with the WAI-ARIA roving tabindex pattern. It is one Tab stop. Arrow keys move between keys in the visual direction, so they are mirrored in right-to-left panels. Home and End jump in a row; Enter and Space press. Keys you cannot see get names (Space, ZWNJ, ⇧ Shift, ⇪ Caps Lock). Focus stays after a redraw, and :focus-visible shows an outline with at least 3:1 contrast.

// at attach time…
KurdishKeyboard.attachTo(field, {
  layout: soraniLayout,
  labelLanguage: "ku",
});

// …or live
kb.setLabelLanguage("en");

Live

The ⇧ key says:

Press Tab to go into the panel, then use the arrow keys.

Legacy text

Clean old text

normalizeLegacy() changes old, broken Sorani (Arabic ك and ي, ه for ە) to the unified codepoints. It uses the same data file as the keyboard. It needs no DOM, so it runs in Node, in workers and in CLIs.

Read this first: the input is taken to be Kurdish. ك→ک and ي→ی always apply, so real Arabic quotes change too; split out other languages yourself. The ه→ە rule looks at position only: a ه at the end of a word changes (also before ZWNJ); a ه before another Arabic letter, tatweel or a diacritic does not. Old vowels in the middle of a word are not found, and there is no dictionary. Latin text does not change.

import { normalizeLegacy } from "kurdish-keyboard/normalize";

normalizeLegacy("كوردي"); // → "کوردی"
normalizeLegacy("ئێمه"); // → "ئێمە"

Live

Result

CDN

No build step

The script bundle gives a global KurdishKeyboard. Its attachTo takes layout names ('sorani', 'kurmanji-phonetic', 'kurmanji-official') or Layout objects; an unknown name throws an error with the list. It also has a layouts list and normalizeLegacy. This bundle holds all layouts; with a bundler, use the module entries.

<script src="https://unpkg.com/kurdish-keyboard"></script>
<script>
  KurdishKeyboard.attachTo(document.querySelector("input"), {
    layout: "sorani",
  });
</script>

Frameworks

React, Vue, Svelte: no wrappers needed

Every insertion sends a bubbling input event (inputType: 'insertText') on the field. So framework bindings see the change as if the user typed it. Attach on mount, detach on unmount.

function KurdishInput() {
  const [value, setValue] = useState("");
  const ref = useRef(null);

  useEffect(() => {
    const kb = KurdishKeyboard.attachTo(ref.current, { layout: soraniLayout });
    return () => kb.detach();
  }, []);

  return (
    <input
      dir="rtl"
      ref={ref}
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}
<script setup>
const text = ref("");
const field = ref(null);
let kb;

onMounted(() => {
  kb = KurdishKeyboard.attachTo(field.value, { layout: soraniLayout });
});
onUnmounted(() => kb?.detach());
</script>

<template>
  <input dir="rtl" ref="field" v-model="text" />
</template>

<!-- Svelte: bind:value works the same way -->

Limits

What it cannot do, and why

These are browser rules, not missing features.

It cannot type into iframes from other sites
The same-origin policy blocks all access to pages embedded from another origin.
It cannot make real keystrokes
Keyboard events made by a script have isTrusted: false, and browsers ignore what they would do. So the widget changes the field value directly with setRangeText(). This works everywhere, also in password fields.
suppressNative turns off autocorrect
inputmode="none" removes the whole phone keyboard for that field, autocorrect too. That is how the mechanism works, so it is opt-in per field.
Framework editors need an adapter
Lexical, ProseMirror and Slate own their DOM and undo outside edits. A browser-level insertion cannot drive them safely; the adapter option is for them. Plain contenteditable works out of the box.
Browser support
Evergreen browsers from about the last three years, plus Safari 15.4+. One small loss: the default key border uses color-mix() (Safari 16.2+). Older browsers show keys without it, or you set --kkb-key-border. No polyfills ship.

Get it

Free, as a keyboard should be

Free

MIT licence · no tiers, no tracking, no lock-in

npm install kurdish-keyboard

Version 0.4.0 on npm, or install from the GitHub repository.

View on GitHub

Questions

Answered before you ask

Which browsers work?
Evergreen browsers from the last three years, plus Safari 15.4+. Every browser API the widget uses is widely available, so no polyfills ship.
Does it work with React, Vue or Svelte?
Yes. After every insertion the widget sends a bubbling input event, so framework bindings see the change without wrappers.
Can it type into any field?
Any input, textarea or contenteditable element on the same page, and rich-text editors through an adapter. Iframes from other sites are blocked by browser security.
Why does it not type the Arabic kaf?
In Kurdish text, ك only looks like ک, and it silently breaks search and sorting. So the widget always types ک. The real Arabic letters are on the Sorani Shift layer, to type Arabic names.
Can I make my own layout?
Yes. Layouts are plain JSON, checked against a published schema. The engine shows any valid layout as it is, so a community layout is an addition, not a fork.