Skip to main content

Building a Keyboard with tscircuit

Overview

This tutorial guides you through creating a custom mechanical keyboard PCB using tscircuit. We'll cover setting up your environment, understanding the core components, building a key matrix, creating a simple 4-key keyboard, and finally scaling up to a standard 60% layout using data from Keyboard Layout Editor.

We'll be using a Raspberry Pi Pico as the microcontroller, Kailh Choc-style key switches, and standard diodes for the matrix.

1. Set Up Your Environment

Before we start building, let's get your development environment ready.

Prerequisites

You need Node.js or Bun installed on your system.

Install tscircuit CLI

Install the tscircuit command-line interface (CLI) globally using npm or bun:

npm install -g @tscircuit/cli
# or
bun install -g @tscircuit/cli

This installs the tsci command, which you can use to create, develop, and export tscircuit projects.

Create a New Project

Navigate to where you want to create your project and run tsci init:

mkdir my-keyboard
cd my-keyboard
tsci init

This command bootstraps a new tscircuit project with a basic structure, including an index.tsx file (our main circuit definition), package.json, tsconfig.json, and other necessary configuration files.

Run the Development Server

Start the tscircuit development server by running:

tsci dev

This command compiles your index.tsx file and serves it on http://localhost:3020 (or the next available port). Open this URL in your browser. You should see a live preview of your circuit, including PCB, Schematic, and 3D views. The server watches for file changes and automatically updates the preview.

tsci dev output example

Browser preview example

2. Import the Main Components

A keyboard PCB primarily consists of three types of components:

  1. Microcontroller: The "brain" that scans the keys and communicates with the computer. We'll use a Raspberry Pi Pico.
  2. Key Switches: The physical buttons you press. We'll use a generic Kailh Choc footprint component.
  3. Diodes: Prevent "ghosting" (incorrect key press readings) in the matrix.

Let's import these into our project.

First, you'll need to install the packages containing the Pico and Key components:

tsci add @tsci/seveibar.PICO @tsci/seveibar.Key

Now, let's import and preview each component to see what we're working with:

Microcontroller (Raspberry Pi Pico)

We import the PICO component. It provides the necessary footprint and pin definitions for the Raspberry Pi Pico.

import { PICO } from "@tsci/seveibar.PICO";

export default () => (
<board width="50mm" height="60mm">
<PICO name="U1" />
</board>
);
PCB Circuit Preview

Hot-Swappable Key Switch (Kailh Choc Style)

We import a Key component, representing a single switch with its footprint. This component is comprised of a couple different footprints and 3d models, but when placed on the board is just a small plastic piece that key switches can be pressed into.

import { Key } from "@tsci/seveibar.Key";

export default () => (
<board width="30mm" height="30mm">
<Key name="K1" />
</board>
);
PCB Circuit Preview

Diode (1N4148WS SMD)

Let's import a diode component. The 1N4148WS is one of the most in-stock and common diodes according to jlcsearch. We can use the File > Import menu to quickly add it to our project. You should get something like this:

imports/A1N4148WS.tsx
import type { ChipProps } from "@tscircuit/props"

const pinLabels = {
pin1: ["C"], // Cathode
pin2: ["A"], // Anode
} as const

export const A_1N4148WS = (props: ChipProps<typeof pinLabels>) => {
return (
<chip
pinLabels={pinLabels}
symbolName="diode"
supplierPartNumbers={{
jlcpcb: ["C57759"],
}}
manufacturerPartNumber="A_1N4148WS"
footprint={
<footprint>
<smtpad
portHints={["pin1"]} // Cathode
pcbX="-1.1725910000000113mm"
pcbY="0mm"
width="0.9999979999999999mm"
height="0.7500112mm"
shape="rect"
/>
<smtpad
portHints={["pin2"]} // Anode
pcbX="1.1725910000000113mm"
pcbY="0mm"
width="0.9999979999999999mm"
height="0.7500112mm"
shape="rect"
/>
{/* Silkscreen omitted for brevity */}
</footprint>
}
cadModel={{
objUrl:
"https://modelcdn.tscircuit.com/easyeda_models/download?uuid=973acf8a660c48b1975f1ba1c890421a&pn=C57759",
rotationOffset: { x: 0, y: 0, z: 0 },
positionOffset: { x: 0, y: 0, z: 0 },
}}
{...props}
/>
)
}
import { A_1N4148WS } from "./imports/A1N4148WS";

export default () => (
<board width="20mm" height="20mm">
<A_1N4148WS name="D1" />
</board>
);
PCB Circuit Preview

3. Create the KeyMatrix Component

Connecting each key switch directly to the microcontroller would require a large number of pins, especially for full-size keyboards. Instead, keyboards use a matrix scanning technique.

How Matrix Scanning Works

  1. Grid Layout: Keys are arranged logically in a grid of rows and columns.
  2. Connections: Each key switch connects a specific row wire to a specific column wire when pressed.
  3. Scanning: The microcontroller activates one row (or column) at a time and checks which columns (or rows) become active. This identifies the pressed key(s) at the intersection.
  4. Diodes: A diode is placed in series with each switch. This prevents "ghosting," where pressing multiple keys simultaneously might falsely register additional key presses. The diode ensures current flows only in one direction (typically from column to row, or vice-versa depending on the scanning direction).

The KeyMatrix Component

Let's create a reusable React component, KeyMatrix, that takes a keyboard layout definition and pin mappings, then generates the grid of keys and diodes.

We'll need helper functions to parse the layout data (from Keyboard Layout Editor format) and generate reference designators for each key.

First, the helper to generate reference designators based on key labels:

lib/getRefDesForKey.tsx
const refDesMap = {
"~": "K_TILDE",
"~`": "K_TILDE",
"!": "K_EXCLAMATION",
"@": "K_AT",
"#": "K_HASH",
$: "K_DOLLAR",
"%": "K_PERCENT",
"^": "K_CARET",
"&": "K_AMPERSAND",
"*": "K_ASTERISK",
"(": "K_LPAREN",
")": "K_RPAREN",
"{": "K_LBRACE",
"}": "K_RBRACE",
"|": "K_PIPE",
"[": "K_LSQBRAK",
"]": "K_RSQBRAK",
'"': "K_QUOTE",
"'": "K_SINGLEQUOTE",
"<": "K_LT",
">": "K_GT",
"?": "K_QUESTION",
"/": "K_SLASH",
windows: "K_WINDOWS",
win: "K_WINDOWS",
menu: "K_MENU",
"caps lock": "K_CAPSLOCK",
":": "K_COLON",
";": "K_SEMICOLON",
"1": "K_N1",
"2": "K_N2",
"3": "K_N3",
"4": "K_N4",
"5": "K_N5",
"6": "K_N6",
"7": "K_N7",
"8": "K_N8",
"9": "K_N9",
"0": "K_N0",
"-": "K_MINUS",
"+": "K_PLUS",
backspace: "K_BACKSPACE",
tab: "K_TAB",
" ": "K_SPACE",
enter: "K_ENTER",
shift: "K_SHIFT",
ctrl: "K_CTRL",
alt: "K_ALT",
escape: "K_ESCAPE",
arrowleft: "K_ARROW_LEFT",
arrowright: "K_ARROW_RIGHT",
arrowup: "K_ARROW_UP",
arrowdown: "K_ARROW_DOWN",
backquote: "K_BACKQUOTE",
quote: "K_QUOTE",
semicolon: "K_SEMICOLON",
comma: "K_COMMA",
period: "K_PERIOD",
slash: "K_SLASH",
backslash: "K_BACKSLASH",
bracketleft: "K_BRACKET_LEFT",
bracketright: "K_BRACKET_RIGHT",
a: "K_A",
b: "K_B",
c: "K_C",
d: "K_D",
e: "K_E",
f: "K_F",
g: "K_G",
h: "K_H",
i: "K_I",
j: "K_J",
k: "K_K",
l: "K_L",
m: "K_M",
n: "K_N",
o: "K_O",
p: "K_P",
q: "K_Q",
r: "K_R",
s: "K_S",
t: "K_T",
u: "K_U",
v: "K_V",
w: "K_W",
x: "K_X",
y: "K_Y",
z: "K_Z",
}
export const getRefDesForKey = (key: string): string => {
const normKeyLabels = key.toLowerCase().split("\n")
for (const normKeyLabel of normKeyLabels) {
if (normKeyLabel in refDesMap) {
return refDesMap[normKeyLabel as keyof typeof refDesMap]
}
}
if (key === "") {
return "K_SPACE"
}
if (key.match(/^[a-zA-Z0-9]+$/)) {
return `K_${key.toUpperCase()}`
}
throw new Error(`No defined refDes for key "${key}"`)
}

Next, the layout parser:

lib/KLELayout.tsx
import { getRefDesForKey } from "./getRefDesForKey"; // Corrected import path

export type KLEKey =
| string
| {
x?: number
y?: number
w?: number
h?: number
x2?: number
y2?: number
w2?: number
h2?: number
r?: number
rx?: number
ry?: number
n?: boolean
l?: boolean
d?: boolean
g?: boolean
sm?: string
sb?: string
st?: string
a?: number
}

export type KLELayout = KLEKey[][]

export const parseKLELayout = (layout: KLELayout) => {
const KEY_SIZE = 19.05 // Standard keycap size (19.05mm or 0.75 inches)
const keys: Array<{
name: string
x: number
y: number
width: number
height: number
rotation: number
rotationX: number
rotationY: number
row: number
col: number
}> = []

let currentX = 0
let currentY = 0
let currentRotation = 0
let currentRotationX = 0
let currentRotationY = 0

// Default properties for keys
let currentProps = {
width: 1,
height: 1,
x: 0,
y: 0,
align: undefined as undefined | number,
}

const nameCounters = new Map<string, number>()
layout.forEach((row, rowIndex) => {
currentX = 0
let colIndex = 0

row.forEach((item) => {
if (typeof item === "object") {
if (item.x !== undefined) currentX += item.x
if (item.y !== undefined) currentY += item.y
if (item.w !== undefined) currentProps.width = item.w
if (item.h !== undefined) currentProps.height = item.h
if (item.r !== undefined) currentRotation = item.r
if (item.rx !== undefined) currentRotationX = item.rx
if (item.ry !== undefined) currentRotationY = item.ry
if (item.a !== undefined) currentProps.align = item.a
} else if (typeof item === "string") {
let refDes = getRefDesForKey(item);
const baseRefDes = refDes; // Store original refDes before appending counter

if (nameCounters.has(baseRefDes)) {
const count = nameCounters.get(baseRefDes)!;
// If the original key exists and doesn't have a counter suffix, update it
const ogKey = keys.find((k) => k.name === baseRefDes);
if (ogKey && !ogKey.name.match(/\d+$/)) {
ogKey.name = `${baseRefDes}1`; // Assuming first one becomes '1'
}

// Increment counter and set new refDes
nameCounters.set(baseRefDes, count + 1);
refDes = `${baseRefDes}${count + 1}`;
} else {
nameCounters.set(baseRefDes, 1);
// Don't append '1' if it's the first instance, unless needed later
}


keys.push({
name: refDes,
x: (currentX + currentProps.width / 2) * KEY_SIZE,
y: -(currentY + currentProps.height / 2) * KEY_SIZE, // Invert Y for PCB coordinates
width: currentProps.width * KEY_SIZE,
height: currentProps.height * KEY_SIZE,
rotation: currentRotation,
rotationX: currentRotationX * KEY_SIZE,
rotationY: currentRotationY * KEY_SIZE,
row: rowIndex,
col: colIndex,
})

currentX += currentProps.width
colIndex++

currentProps.width = 1
currentProps.height = 1
}
})
currentY++
})

// Final pass to ensure keys with the same base name have counters if needed
const finalNameCounts = new Map<string, number>();
keys.forEach(key => {
const baseNameMatch = key.name.match(/^([A-Z_]+)/);
if (baseNameMatch) {
const baseName = baseNameMatch[1];
finalNameCounts.set(baseName, (finalNameCounts.get(baseName) || 0) + 1);
}
});

keys.forEach(key => {
const baseNameMatch = key.name.match(/^([A-Z_]+)(\d*)$/);
if (baseNameMatch) {
const baseName = baseNameMatch[1];
const numSuffix = baseNameMatch[2];
if (finalNameCounts.get(baseName)! > 1 && numSuffix === "") {
// If there are multiple keys with this base name, and this one doesn't have a suffix,
// assume it's the first one and add '1'. This relies on the initial pass logic.
// A more robust solution might require re-numbering all duplicates.
// For simplicity, we'll stick to the logic inferred from the provided code.
// The original code seems to handle this by potentially renaming the *first*
// instance when a *second* one is encountered. Let's replicate that nuance.
// The provided code snippet's name counting logic was slightly ambiguous
// on exactly *when* the first item gets renamed. Assuming it happens
// when the second is added.
}
}
});


return keys
}

Finally, the KeyMatrix component itself:

lib/KeyMatrix.tsx
import { Key } from "@tsci/seveibar.Key";
import { parseKLELayout, type KLELayout } from "./KLELayout"; // Corrected import path
import { A_1N4148WS } from "../imports/A1N4148WS"; // Corrected import path

interface KeyMatrixProps {
layout: KLELayout
rowToMicroPin?: string[] // Array mapping row index to net name (e.g., "net.ROW0")
colToMicroPin?: string[] // Array mapping col index to net name (e.g., "net.COL0")
name?: string
pcbX?: number
pcbY?: number
schX?: number
schY?: number
}

export const KeyMatrix = ({
layout,
rowToMicroPin,
colToMicroPin,
name = "KB",
pcbX = 0,
pcbY = 0,
schX = 0,
schY = 0,
}: KeyMatrixProps) => {
const keys = parseKLELayout(layout)

// Calculate keyboard bounding box to center it
const minX = Math.min(...keys.map((k) => k.x - k.width / 2))
const maxX = Math.max(...keys.map((k) => k.x + k.width / 2))
const minY = Math.min(...keys.map((k) => k.y - k.height / 2))
const maxY = Math.max(...keys.map((k) => k.y + k.height / 2))
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;


return (
<group name={name} pcbX={pcbX} pcbY={pcbY} schX={schX} schY={schY}>
{keys.map((key) => {
// Calculate the relative position from the center of the keyboard
const relX = key.x - centerX;
const relY = key.y - centerY;

const rowNet = rowToMicroPin?.[key.row];
const colNet = colToMicroPin?.[key.col];

// Diode: Anode (A/pin2) -> Key Pin 1, Cathode (C/pin1) -> Row Net
// Key: Pin 1 -> Diode Anode, Pin 2 -> Column Net

return (
<group
key={key.name}
// Position the group containing the key and diode
pcbX={relX}
pcbY={relY}
// Adjust schematic positions for better layout
schX={relX / 10} // Scale down schematic positions
schY={relY / 10}
>
<Key name={key.name} />
{rowNet !== undefined && (
<A_1N4148WS
name={`${key.name}_DIO`}
connections={{
pin2: `.${key.name} .pin1`, // Diode Anode (A) connects to Key pin 1
pin1: rowNet, // Diode Cathode (C) connects to Row net
}}
// Position diode relative to the key
pcbX={0} // Adjust as needed for PCB layout
pcbY={-7} // Place diode slightly below the key center
schY={-1} // Place diode below key in schematic
layer="bottom" // Place diode on the bottom layer
/>
)}
{colNet !== undefined && (
<trace from={`.${key.name} .pin2`} to={colNet} /> // Key pin 2 connects to Col net
)}
</group>
)
})}
</group>
)
}

This component:

  1. Parses the layout using parseKLELayout.
  2. Calculates the center of the keyboard for relative positioning.
  3. Iterates through each key definition from the parsed layout.
  4. For each key, it creates a <group> to hold the key switch and its diode.
  5. Places the <Key> component.
  6. If rowToMicroPin is provided for the key's row, it places an A_1N4148WS diode:
    • The diode's Anode (A/pin2) connects to the Key's pin1.
    • The diode's Cathode (C/pin1) connects to the corresponding row net.
    • The diode is placed slightly below the key on the PCB's bottom layer.
  7. If colToMicroPin is provided for the key's column, it creates a <trace> connecting the Key's pin2 to the corresponding column net.
  8. Positions the key groups relative to the keyboard's center.

4. Create a Basic 4-Key Keyboard

Now, let's use our KeyMatrix component to create a simple 2x2 keyboard.

We'll define the layout directly in our index.tsx and map the rows/columns to Pico pins.

index.tsx
import { PICO } from "@tsci/seveibar.PICO";
import { KeyMatrix } from "./lib/KeyMatrix"; // Assuming KeyMatrix is in lib/
import type { KLELayout } from "./lib/KLELayout"; // Assuming KLELayout types are in lib/

// Define a simple 2x2 layout
const simpleLayout: KLELayout = [
["1", "2"],
["3", "4"],
];

// Map rows and columns to Pico pins (using net names for clarity)
const rowPins = ["net.ROW0", "net.ROW1"];
const colPins = ["net.COL0", "net.COL1"];

export default () => (
<board width="60mm" height="60mm">
{/* Place the Pico */}
<PICO
name="U1"
pcbX={-25} // Position Pico to the left
pcbRotation="90deg"
connections={{
// Connect Pico pins to our row/column nets
GP15: rowPins[0], // Row 0
GP16: rowPins[1], // Row 1
GP0: colPins[0], // Col 0
GP1: colPins[1], // Col 1
// Add Ground connections for Pico
GND1: "net.GND",
GND2: "net.GND",
GND3: "net.GND",
GND4: "net.GND",
GND5: "net.GND",
GND6: "net.GND",
GND7: "net.GND",
}}
/>

{/* Place the KeyMatrix */}
<KeyMatrix
layout={simpleLayout}
rowToMicroPin={rowPins}
colToMicroPin={colPins}
pcbX={15} // Position matrix to the right
/>
</board>
);
import { PICO } from "@tsci/seveibar.PICO";
import { KeyMatrix } from "./lib/KeyMatrix"; // Assuming KeyMatrix is in lib/
import type { KLELayout } from "./lib/KLELayout"; // Assuming KLELayout types are in lib/
import { Key } from "@tsci/seveibar.Key"; // Need Key for KeyMatrix
import { A_1N4148WS } from "./imports/A1N4148WS"; // Need Diode for KeyMatrix
import { parseKLELayout } from "./lib/KLELayout"; // Need parser for KeyMatrix
import { getRefDesForKey } from "./lib/getRefDesForKey"; // Need refdes for parser

// Define a simple 2x2 layout
const simpleLayout = [
["1", "2"],
["3", "4"],
];

// Map rows and columns to Pico pins (using net names for clarity)
const rowPins = ["net.ROW0", "net.ROW1"];
const colPins = ["net.COL0", "net.COL1"];

export default () => (
<board width="60mm" height="60mm">
{/* Place the Pico */}
<PICO
name="U1"
pcbX={-25} // Position Pico to the left
pcbRotation="90deg"
connections={{
// Connect Pico pins to our row/column nets
GP15: rowPins[0], // Row 0
GP16: rowPins[1], // Row 1
GP0: colPins[0], // Col 0
GP1: colPins[1], // Col 1
// Add Ground connections for Pico
GND1: "net.GND", GND2: "net.GND", GND3: "net.GND", GND4: "net.GND",
GND5: "net.GND", GND6: "net.GND", GND7: "net.GND",
}}
/>

{/* Place the KeyMatrix */}
<KeyMatrix
layout={simpleLayout}
rowToMicroPin={rowPins}
colToMicroPin={colPins}
pcbX={15} // Position matrix to the right
/>
</board>
);
PCB Circuit Preview

You now have a functional 4-key macropad PCB design!

5. Import a Standard Layout (60% Keyboard)

Manually defining layouts for larger keyboards is tedious. We can use data directly from Keyboard Layout Editor. This website allows you to design layouts graphically and export them as JSON data, which matches our KLELayout type.

Let's use the default60 layout provided in the repomix-output.txt. Create this file:

keyboard-layouts/default60.tsx
export const default60 = [
[
"~\n`", "!\n1", "@\n2", "#\n3", "$\n4", "%\n5", "^\n6", "&\n7", "*\n8", "(\n9", ")\n0", "_\n-", "+\n=", { w: 2 }, "Backspace",
],
[
{ w: 1.5 }, "Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "{\n[", "}\n]", { w: 1.5 }, "|\n\\",
],
[
{ w: 1.75 }, "Caps Lock", "A", "S", "D", "F", "G", "H", "J", "K", "L", ":\n;", "\"\n'", { w: 2.25 }, "Enter",
],
[
{ w: 2.25 }, "Shift", "Z", "X", "C", "V", "B", "N", "M", "<\n,", ">\n.", "?\n/", { w: 2.75 }, "Shift",
],
[
{ w: 1.25 }, "Ctrl", { w: 1.25 }, "Win", { w: 1.25 }, "Alt", { a: 7, w: 6.25 }, "", { a: 4, w: 1.25 }, "Alt", { w: 1.25 }, "Win", { w: 1.25 }, "Menu", { w: 1.25 }, "Ctrl",
],
];

Now, update index.tsx to use this layout. We also need to expand our rowPins and colPins to match the requirements of a 60% keyboard (typically 5 rows and up to 15 columns). We'll use the pin assignments from the repomix-output.txt index.tsx example.

index.tsx (Updated for 60%)
import { PICO } from "@tsci/seveibar.PICO";
import { KeyMatrix } from "./lib/KeyMatrix";
import type { KLELayout } from "./lib/KLELayout";
import { default60 } from "./keyboard-layouts/default60"; // Import the 60% layout

// Define row and column nets needed for a 60% layout
const rowPins = [
"net.ROW0", // Note: Pin names adjusted for clarity, map starts from 0
"net.ROW1",
"net.ROW2",
"net.ROW3",
"net.ROW4",
];
const colPins = [
"net.COL0", "net.COL1", "net.COL2", "net.COL3", "net.COL4",
"net.COL5", "net.COL6", "net.COL7", "net.COL8", "net.COL9",
"net.COL10", "net.COL11", "net.COL12", "net.COL13", "net.COL14",
];


export default () => (
<board width="300mm" height="120mm"> {/* Increased board size */}
{/* Place the Pico */}
<PICO
name="U1"
pcbRotation="-90deg" // Rotate Pico for better placement
pcbX={-130} // Position Pico far left
pcbY={0}
connections={{
// Map Pico pins to rows (GP15-GP19)
GP15: rowPins[0], // Row 0
GP16: rowPins[1], // Row 1
GP17: rowPins[2], // Row 2
GP18: rowPins[3], // Row 3
GP19: rowPins[4], // Row 4
// Map Pico pins to columns (GP0-GP14)
GP0: colPins[0], GP1: colPins[1], GP2: colPins[2], GP3: colPins[3], GP4: colPins[4],
GP5: colPins[5], GP6: colPins[6], GP7: colPins[7], GP8: colPins[8], GP9: colPins[9],
GP10: colPins[10], GP11: colPins[11], GP12: colPins[12], GP13: colPins[13], GP14: colPins[14],
// Add Ground connections
GND1: "net.GND", GND2: "net.GND", GND3: "net.GND", GND4: "net.GND",
GND5: "net.GND", GND6: "net.GND", GND7: "net.GND",
}}
/>

{/* Place the KeyMatrix using the default60 layout */}
<KeyMatrix
layout={default60}
rowToMicroPin={rowPins}
colToMicroPin={colPins}
pcbX={0} // Center the keyboard matrix
pcbY={0}
/>
</board>
);
import { PICO } from "@tsci/seveibar.PICO";
import { KeyMatrix } from "./lib/KeyMatrix";
import type { KLELayout } from "./lib/KLELayout";
import { default60 } from "./keyboard-layouts/default60";
import { Key } from "@tsci/seveibar.Key"; // Need Key for KeyMatrix
import { A_1N4148WS } from "./imports/A1N4148WS"; // Need Diode for KeyMatrix
import { parseKLELayout } from "./lib/KLELayout"; // Need parser for KeyMatrix
import { getRefDesForKey } from "./lib/getRefDesForKey"; // Need refdes for parser

// Define row and column nets needed for a 60% layout
const rowPins = [ "net.ROW0", "net.ROW1", "net.ROW2", "net.ROW3", "net.ROW4", ];
const colPins = [ "net.COL0", "net.COL1", "net.COL2", "net.COL3", "net.COL4", "net.COL5", "net.COL6", "net.COL7", "net.COL8", "net.COL9", "net.COL10", "net.COL11", "net.COL12", "net.COL13", "net.COL14", ];

export default () => (
<board width="300mm" height="120mm">
<PICO
name="U1"
pcbRotation="-90deg"
pcbX={-130}
pcbY={0}
connections={{
GP15: rowPins[0], GP16: rowPins[1], GP17: rowPins[2], GP18: rowPins[3], GP19: rowPins[4],
GP0: colPins[0], GP1: colPins[1], GP2: colPins[2], GP3: colPins[3], GP4: colPins[4],
GP5: colPins[5], GP6: colPins[6], GP7: colPins[7], GP8: colPins[8], GP9: colPins[9],
GP10: colPins[10], GP11: colPins[11], GP12: colPins[12], GP13: colPins[13], GP14: colPins[14],
GND1: "net.GND", GND2: "net.GND", GND3: "net.GND", GND4: "net.GND",
GND5: "net.GND", GND6: "net.GND", GND7: "net.GND",
}}
/>
<KeyMatrix
layout={default60}
rowToMicroPin={rowPins}
colToMicroPin={colPins}
pcbX={0}
pcbY={0}
/>
</board>
);
PCB Circuit Preview

With this setup, you can easily swap default60 with any other KLE layout JSON data to generate different keyboard PCBs!

Next Steps

  • Firmware: You'll need firmware (like KMK, QMK, or ZMK) for the Raspberry Pi Pico to scan the matrix and act as a USB keyboard.
  • Export: Use tsci export or the download button in the web UI to get Gerber files for manufacturing.
  • Manufacturing: Order your PCB from a manufacturer like JLCPCB or PCBWay. See our guide on Ordering Prototypes.
  • Assembly: Solder the components (Pico, diodes, key switches) onto your manufactured PCB.

Happy building!