diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5a4bec04..7103c29f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -31,6 +31,7 @@ target_sources(app PRIVATE src/keymap.c)
 target_sources(app PRIVATE src/hid.c)
 target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/ble.c)
 target_sources_ifdef(CONFIG_ZMK_KSCAN_MOCK_DRIVER app PRIVATE src/kscan_mock.c)
+target_sources_ifdef(CONFIG_ZMK_KSCAN_COMPOSITE_DRIVER app PRIVATE src/kscan_composite.c)
 target_sources(app PRIVATE src/usb_hid.c)
 target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/hog.c)
 target_sources(app PRIVATE src/endpoints.c)
diff --git a/Kconfig b/Kconfig
index 264c4510..43c07d24 100644
--- a/Kconfig
+++ b/Kconfig
@@ -25,6 +25,11 @@ config ZMK_KSCAN_MOCK_DRIVER
 	bool "Enable mock kscan driver to simulate key presses"
 	default n
 
+
+config ZMK_KSCAN_COMPOSITE_DRIVER
+	bool "Enable composite kscan driver to combine kscan devices"
+	default n
+
 menu "ZMK Actions"
 
 config ZMK_ACTION_MOD_TAP
diff --git a/boards/native_posix.conf b/boards/native_posix.conf
index 4efd71d9..5bfb366c 100644
--- a/boards/native_posix.conf
+++ b/boards/native_posix.conf
@@ -1,5 +1,6 @@
 CONFIG_KSCAN=n
 CONFIG_ZMK_KSCAN_MOCK_DRIVER=y
+CONFIG_ZMK_KSCAN_COMPOSITE_DRIVER=y
 CONFIG_ZMK_KSCAN_GPIO_DRIVER=n
 CONFIG_GPIO=n
 CONFIG_ZMK_BLE=n
diff --git a/boards/native_posix.overlay b/boards/native_posix.overlay
index 1d692d4e..9d92beeb 100644
--- a/boards/native_posix.overlay
+++ b/boards/native_posix.overlay
@@ -7,9 +7,25 @@
 		zmk,keymap = &keymap0;
 	};
 
-	kscan0: kscan {
+	kscan0: kscan_0 {
+		compatible = "zmk,kscan-composite";
+		label = "KSCAN_COMP";
+		rows = <2>;
+		columns = <4>;
+
+		left: left {
+			kscan = <&left_hand>;
+		};
+
+		right: right {
+			kscan = <&right_hand>;
+			column-offset = <2>;
+		};
+	};
+
+	left_hand: kscan_1 {
 		compatible = "zmk,kscan-mock";
-		label = "KSCAN_MOCK0";
+		label = "KSCAN_LEFT";
 
 		rows = <2>;
 		columns = <2>;
@@ -17,6 +33,15 @@
 		// events = <ZMK_MOCK_PRESS(0,0,800) ZMK_MOCK_RELEASE(0,0,800) ZMK_MOCK_PRESS(0,1,800) ZMK_MOCK_RELEASE(0,1,800)>;
 	};
 
+	right_hand: kscan_2 {
+		compatible = "zmk,kscan-mock";
+		label = "KSCAN_RIGHT";
+
+		rows = <2>;
+		columns = <2>;
+		events = <ZMK_MOCK_PRESS(1,1,800) ZMK_MOCK_RELEASE(1,1,100) ZMK_MOCK_PRESS(0,1,800) ZMK_MOCK_RELEASE(0,1,100)>;
+	};
+
 	keymap0: keymap {
 		compatible = "zmk,keymap";
 		label ="Default keymap";
@@ -29,20 +54,26 @@
 		default: layer_0 {
 			label = "DEFAULT";
 			keys =
-	<
-	KC_A MT(MOD_LSFT, KC_B)
-	KC_C KC_D
-	>;
+			<
+				KC_A MT(MOD_LSFT, KC_B) KC_C KC_D
+				KC_E KC_F KC_G KC_H
+			>;
 		};
 
 		lower: layer_1 {
 			label = "LOWER";
-			keys = <KC_D KC_C ZC_TRNS ZC_TRNS>;
+			keys = <
+				KC_A KC_B KC_C KC_D
+				KC_E KC_F KC_G KC_H
+			>;
 		};
 
 		raise: layer_2 {
 			label = "RAISE";
-			keys = <KC_C KC_D ZC_TRNS ZC_TRNS>;
+			keys = <
+				KC_E KC_F KC_G KC_H 
+				KC_A KC_B KC_C KC_D
+			>;
 		};
 	};
 };
diff --git a/boards/shields/petejohanson_handwire/Kconfig.defconfig b/boards/shields/petejohanson_handwire/Kconfig.defconfig
index e69de29b..c7408ce9 100644
--- a/boards/shields/petejohanson_handwire/Kconfig.defconfig
+++ b/boards/shields/petejohanson_handwire/Kconfig.defconfig
@@ -0,0 +1,2 @@
+
+CONFIG_ZMK_KSCAN_COMPOSITE_DRIVER=y
\ No newline at end of file
diff --git a/boards/shields/petejohanson_handwire/petejohanson_handwire.overlay b/boards/shields/petejohanson_handwire/petejohanson_handwire.overlay
index 72e24a0b..d65b3b1d 100644
--- a/boards/shields/petejohanson_handwire/petejohanson_handwire.overlay
+++ b/boards/shields/petejohanson_handwire/petejohanson_handwire.overlay
@@ -4,14 +4,41 @@
 		zmk,kscan = &kscan0;
 	};
 
-	kscan0: kscan {
+	kscan0: kscan_0 {
+		compatible = "zmk,kscan-composite";
+		label = "KSCAN_COMP";
+		rows = <2>;
+		columns = <4>;
+
+		left {
+			kscan = <&left_hand>;
+		};
+
+		right {
+			kscan = <&right_hand>;
+			column-offset = <2>;
+		};
+	};
+
+	left_hand: kscan_1 {
 		compatible = "gpio-kscan";
-		label = "KSCAN";
+		label = "KSCAN_LEFT";
 
 		diode-direction = "row2col";
 		row-gpios = <&arduino_header 8 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>,
-		            <&arduino_header 10 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
-		col-gpios = <&arduino_header 13 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>,
-		            <&arduino_header 14 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
+		            <&arduino_header 9 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
+		col-gpios = <&arduino_header 10 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>,
+		            <&arduino_header 11 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
+	};
+
+	right_hand: kscan_2 {
+		compatible = "gpio-kscan";
+		label = "KSCAN_RIGHT";
+
+		diode-direction = "row2col";
+		row-gpios = <&arduino_header 12 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>,
+		            <&arduino_header 13 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
+		col-gpios = <&arduino_header 14 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>,
+		            <&arduino_header 15 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
 	};
 };
diff --git a/dts/bindings/zmk,kscan-composite.yaml b/dts/bindings/zmk,kscan-composite.yaml
new file mode 100644
index 00000000..6126c303
--- /dev/null
+++ b/dts/bindings/zmk,kscan-composite.yaml
@@ -0,0 +1,27 @@
+description: |
+  Allows composing multiple KSCAN devices into one virtual device
+
+compatible: "zmk,kscan-composite"
+
+properties:
+  label:
+    type: string
+  rows:
+    type: int
+  columns:
+    type: int
+
+child-binding:
+  description: "Details of an included KSCAN devices"
+
+  properties:
+    label:
+      type: string
+    kscan:
+      type: phandle
+    row-offset:
+      type: int
+      default: 0
+    column-offset:
+      type: int
+      default: 0
diff --git a/src/kscan_composite.c b/src/kscan_composite.c
new file mode 100644
index 00000000..d46484b7
--- /dev/null
+++ b/src/kscan_composite.c
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2020 Peter Johanson <peter@peterjohanson.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#define DT_DRV_COMPAT zmk_kscan_composite
+
+#include <device.h>
+#include <drivers/kscan.h>
+#include <logging/log.h>
+LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
+
+#define MATRIX_NODE_ID DT_DRV_INST(0)
+#define MATRIX_ROWS DT_PROP(MATRIX_NODE_ID, rows)
+#define MATRIX_COLS DT_PROP(MATRIX_NODE_ID, columns)
+
+struct kscan_composite_child_config
+{
+    char *label;
+    u8_t row_offset;
+    u8_t column_offset;
+};
+
+#define CHILD_CONFIG(inst)                          \
+    {                                               \
+        .label = DT_LABEL(DT_PHANDLE(inst, kscan)), \
+        .row_offset = DT_PROP(inst, row_offset),    \
+        .column_offset = DT_PROP(inst, column_offset)},
+
+const struct kscan_composite_child_config kscan_composite_children[] = {
+    DT_FOREACH_CHILD(MATRIX_NODE_ID, CHILD_CONFIG)};
+
+struct kscan_composite_config
+{
+};
+
+struct kscan_composite_data
+{
+    kscan_callback_t callback;
+
+    struct device *dev;
+};
+
+static int kscan_composite_enable_callback(struct device *dev)
+{
+    for (int i = 0; i < sizeof(kscan_composite_children) / sizeof(kscan_composite_children[0]); i++)
+    {
+        const struct kscan_composite_child_config *cfg = &kscan_composite_children[i];
+
+        kscan_enable_callback(device_get_binding(cfg->label));
+    }
+    return 0;
+}
+
+static int kscan_composite_disable_callback(struct device *dev)
+{
+    for (int i = 0; i < sizeof(kscan_composite_children) / sizeof(kscan_composite_children[0]); i++)
+    {
+        const struct kscan_composite_child_config *cfg = &kscan_composite_children[i];
+
+        kscan_disable_callback(device_get_binding(cfg->label));
+    }
+    return 0;
+}
+
+static void kscan_composite_child_callback(struct device *child_dev, u32_t row, u32_t column, bool pressed)
+{
+    // TODO: Ideally we can get this passed into our callback!
+    struct device *dev = device_get_binding(DT_INST_LABEL(0));
+    struct kscan_composite_data *data = dev->driver_data;
+
+    for (int i = 0; i < sizeof(kscan_composite_children) / sizeof(kscan_composite_children[0]); i++)
+    {
+        const struct kscan_composite_child_config *cfg = &kscan_composite_children[i];
+
+        if (device_get_binding(cfg->label) != child_dev)
+        {
+            continue;
+        }
+
+        data->callback(dev, row + cfg->row_offset, column + cfg->column_offset, pressed);
+    }
+}
+
+static int kscan_composite_configure(struct device *dev, kscan_callback_t callback)
+{
+    struct kscan_composite_data *data = dev->driver_data;
+
+    if (!callback)
+    {
+        return -EINVAL;
+    }
+
+    for (int i = 0; i < sizeof(kscan_composite_children) / sizeof(kscan_composite_children[0]); i++)
+    {
+        const struct kscan_composite_child_config *cfg = &kscan_composite_children[i];
+
+        kscan_config(device_get_binding(cfg->label), &kscan_composite_child_callback);
+    }
+
+    data->callback = callback;
+
+    return 0;
+}
+
+static int kscan_composite_init(struct device *dev)
+{
+    struct kscan_composite_data *data = dev->driver_data;
+
+    data->dev = dev;
+
+    return 0;
+}
+
+static const struct kscan_driver_api mock_driver_api = {
+    .config = kscan_composite_configure,
+    .enable_callback = kscan_composite_enable_callback,
+    .disable_callback = kscan_composite_disable_callback,
+};
+
+static const struct kscan_composite_config kscan_composite_config = {};
+
+static struct kscan_composite_data kscan_composite_data;
+
+DEVICE_AND_API_INIT(kscan_composite, DT_INST_LABEL(0), kscan_composite_init,
+                    &kscan_composite_data,
+                    &kscan_composite_config,
+                    APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT,
+                    &mock_driver_api);
diff --git a/src/kscan_mock.c b/src/kscan_mock.c
index a2143352..7d2d24d2 100644
--- a/src/kscan_mock.c
+++ b/src/kscan_mock.c
@@ -13,16 +13,6 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
 
 #include <zmk/kscan-mock.h>
 
-#define MATRIX_NODE_ID DT_DRV_INST(0)
-#define MATRIX_ROWS DT_PROP(MATRIX_NODE_ID, rows)
-#define MATRIX_COLS DT_PROP(MATRIX_NODE_ID, columns)
-#define MATRIX_MOCK_EVENT_COUNT DT_PROP_LEN(MATRIX_NODE_ID, events)
-
-struct kscan_mock_config
-{
-    u32_t events[MATRIX_MOCK_EVENT_COUNT];
-};
-
 struct kscan_mock_data
 {
     kscan_callback_t callback;
@@ -32,47 +22,14 @@ struct kscan_mock_data
     struct device *dev;
 };
 
-static void kscan_mock_schedule_next_event(struct device *dev)
-{
-    struct kscan_mock_data *data = dev->driver_data;
-    const struct kscan_mock_config *cfg = dev->config_info;
-
-    if (data->event_index < MATRIX_MOCK_EVENT_COUNT)
-    {
-        u32_t ev = cfg->events[data->event_index];
-        LOG_DBG("delaying next keypress: %d", ZMK_MOCK_MSEC(ev));
-        k_delayed_work_submit(&data->work, K_MSEC(ZMK_MOCK_MSEC(ev)));
-    }
-}
-
-static int kscan_mock_enable_callback(struct device *dev)
-{
-    struct kscan_mock_data *data = dev->driver_data;
-    kscan_mock_schedule_next_event(dev);
-    return 0;
-}
-
 static int kscan_mock_disable_callback(struct device *dev)
 {
     struct kscan_mock_data *data = dev->driver_data;
-    const struct kscan_mock_config *cfg = dev->config_info;
 
     k_delayed_work_cancel(&data->work);
     return 0;
 }
 
-static void kscan_mock_work_handler(struct k_work *work)
-{
-    struct kscan_mock_data *data =
-        CONTAINER_OF(work, struct kscan_mock_data, work);
-    struct kscan_mock_config *cfg = data->dev->config_info;
-
-    u32_t ev = cfg->events[data->event_index++];
-    LOG_DBG("ev %u row %d column %d state %d\n", ev, ZMK_MOCK_ROW(ev), ZMK_MOCK_COL(ev), ZMK_MOCK_IS_PRESS(ev));
-    data->callback(data->dev, ZMK_MOCK_ROW(ev), ZMK_MOCK_COL(ev), ZMK_MOCK_IS_PRESS(ev));
-    kscan_mock_schedule_next_event(data->dev);
-}
-
 static int kscan_mock_configure(struct device *dev, kscan_callback_t callback)
 {
     struct kscan_mock_data *data = dev->driver_data;
@@ -88,30 +45,58 @@ static int kscan_mock_configure(struct device *dev, kscan_callback_t callback)
     return 0;
 }
 
-static int kscan_mock_init(struct device *dev)
-{
-    struct kscan_mock_data *data = dev->driver_data;
-    const struct kscan_mock_config *cfg = dev->config_info;
+#define MOCK_INST_INIT(n)                                                          \
+    struct kscan_mock_config_##n                                                   \
+    {                                                                              \
+        u32_t events[DT_INST_PROP_LEN(n, events)];                                 \
+    };                                                                             \
+    static void kscan_mock_schedule_next_event_##n(struct device *dev)             \
+    {                                                                              \
+        struct kscan_mock_data *data = dev->driver_data;                           \
+        const struct kscan_mock_config_##n *cfg = dev->config_info;                \
+        if (data->event_index < DT_INST_PROP_LEN(n, events))                       \
+        {                                                                          \
+            u32_t ev = cfg->events[data->event_index];                             \
+            LOG_DBG("delaying next keypress: %d", ZMK_MOCK_MSEC(ev));              \
+            k_delayed_work_submit(&data->work, K_MSEC(ZMK_MOCK_MSEC(ev)));         \
+        }                                                                          \
+    }                                                                              \
+    static void kscan_mock_work_handler_##n(struct k_work *work)                   \
+    {                                                                              \
+        struct kscan_mock_data *data =                                             \
+            CONTAINER_OF(work, struct kscan_mock_data, work);                      \
+        const struct kscan_mock_config_##n *cfg = data->dev->config_info;          \
+        u32_t ev = cfg->events[data->event_index++];                               \
+        LOG_DBG("ev %u row %d column %d state %d\n", ev,                           \
+                ZMK_MOCK_ROW(ev), ZMK_MOCK_COL(ev), ZMK_MOCK_IS_PRESS(ev));        \
+        data->callback(data->dev,                                                  \
+                       ZMK_MOCK_ROW(ev), ZMK_MOCK_COL(ev), ZMK_MOCK_IS_PRESS(ev)); \
+        kscan_mock_schedule_next_event_##n(data->dev);                             \
+    }                                                                              \
+    static int kscan_mock_init_##n(struct device *dev)                             \
+    {                                                                              \
+        struct kscan_mock_data *data = dev->driver_data;                           \
+        data->dev = dev;                                                           \
+        k_delayed_work_init(&data->work, kscan_mock_work_handler_##n);             \
+        return 0;                                                                  \
+    }                                                                              \
+    static int kscan_mock_enable_callback_##n(struct device *dev)                  \
+    {                                                                              \
+        kscan_mock_schedule_next_event_##n(dev);                                   \
+        return 0;                                                                  \
+    }                                                                              \
+    static const struct kscan_driver_api mock_driver_api_##n = {                   \
+        .config = kscan_mock_configure,                                            \
+        .enable_callback = kscan_mock_enable_callback_##n,                         \
+        .disable_callback = kscan_mock_disable_callback,                           \
+    };                                                                             \
+    static struct kscan_mock_data kscan_mock_data_##n;                             \
+    static const struct kscan_mock_config_##n kscan_mock_config_##n = {            \
+        .events = DT_INST_PROP(n, events)};                                        \
+    DEVICE_AND_API_INIT(kscan_mock_##n, DT_INST_LABEL(n), kscan_mock_init_##n,     \
+                        &kscan_mock_data_##n,                                      \
+                        &kscan_mock_config_##n,                                    \
+                        APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT,          \
+                        &mock_driver_api_##n);
 
-    data->dev = dev;
-    k_delayed_work_init(&data->work, kscan_mock_work_handler);
-
-    return 0;
-}
-
-static const struct kscan_driver_api mock_driver_api = {
-    .config = kscan_mock_configure,
-    .enable_callback = kscan_mock_enable_callback,
-    .disable_callback = kscan_mock_disable_callback,
-};
-
-static const struct kscan_mock_config kscan_mock_config = {
-    .events = DT_PROP(MATRIX_NODE_ID, events)};
-
-static struct kscan_mock_data kscan_mock_data;
-
-DEVICE_AND_API_INIT(kscan_mock, DT_INST_LABEL(0), kscan_mock_init,
-                    &kscan_mock_data,
-                    &kscan_mock_config,
-                    APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT,
-                    &mock_driver_api);
+DT_INST_FOREACH_STATUS_OKAY(MOCK_INST_INIT)
\ No newline at end of file