---
title: "Building Custom Keyboard Backlighting"
description: "Upgrading my [Ergodox EZ](https://ergodox-ez.com/) using [QMK](https://github.com/qmk/qmk_firmware)"
date: 2026-07-09
---
# Intro

I own an Ergodox EZ, a very comfortable, split, columnar keyboard. I bought it
because I gave myself carpal tunnel typing on my last keyboard, and I wanted to
see if I could type any faster on a split board.[^1] Ergodox keyboards run
open-source QMK firmware, meaning they are extensively configurable for a
programmer like me! I love my keyboard, and I would recommend a split keyboard
to anybody looking for something more comfortable than a standard keyboard.

# Key Layout

My key layout isn't the main focus of this post, but I wanted to highlight it
because it uses some cool features QMK offers.

![Keyboard Layout](/qmk/layout.png)

The first thing to notice is that I have plenty of thumb keys for common
keycodes. I never really liked that common keys like Enter, Backspace, and
Escape[^2] were off to the side, so I moved them to be more easily accessible.

On the bottom left side, there is a key labelled Layer. QMK has a concept called
[layers](https://docs.qmk.fm/feature_layers), not unlike how a phone keyboard
works: tap or hold a layer key, and access different keys. I use this for my
function keys because I use them so infrequently, and I don't want them wasting
space normally.

Finally, I have a key labelled Macro. I commonly type `->` when programming, so
I made this key type both characters in a single stroke. I also modified the
handling of the Backspace key so it deletes both characters if the previous
keycode was Macro.

# Backlighting

All the default backlighting options provided by QMK are too boring for me, so I
rolled my own. It works like this, with each keyboard half operating
independently:

- Every keystroke changes the backlight colour
- When all keys are released, the backlight LEDs fade out nicely
- Backlight LEDs closer to the most recently pressed key are brighter than
  those further from it

Every time a keystroke is detected, the following function runs, tracking which
key was pressed and updating the colours:

```c
void post_process_record_user(uint16_t keycode, keyrecord_t *record)
{
    if (!backlight || !record->event.pressed) {
        return;
    }

    if (record->event.key.row < 7) {
        hue_left = rand();
        brightness_left = 255;

        if (record->event.key.col != 5) {
            position_left = 29 - 2 * record->event.key.row;
        } else {
            position_left = 15;
        }

        color(hue_left, brightness_left, position_left, 15);
    } else {
        hue_right = rand();
        brightness_right = 255;

        if (record->event.key.col != 5) {
            position_right = 26 - 2 * record->event.key.row;
        } else {
            position_right = 14;
        }

        color(hue_right, brightness_right, position_right, 0);
    }
}
```

This function is rather long because I have to control the keyboard halves
independently, but the main idea is simple. Every keystroke:

- Randomize the colour hue
- Maximize the brightness
- Record the column the key is in
- Turn on the LEDs using those values

In another function, I keep track of how many keys are pressed at any given
time, so I can dim the LEDs once no more are pressed:

```c
if (record->event.key.row < 7) {
	count_left += record->event.pressed ? 1 : -1;
} else {
	count_right += record->event.pressed ? 1 : -1;
}
```

## Brightness Calculation

Now that I track these values, I need to use them to calculate the brightness of
each LED:

```c
static void color(uint8_t hue, uint8_t brightness, uint8_t position,
		uint8_t index)
{
    for (int i = index; i < (index + 15); ++i) {
		const uint32_t cube = (uint32_t)brightness * brightness * brightness;
		const uint8_t distance = position > i ? position - i : i - position;
		sethsv(hue, 255, cube / 65025 * 5 / (5 + distance), i);
    }
    rgblight_set();
}
```

This function takes in a hue, brightness, position, and starting index. There
are 30 LEDs on an Ergodox EZ, meaning that I can pass either `0` or `15` as the
starting index to have the function loop over the right or left LEDs
respectively.

The `brightness` and `position` parameters contribute to the brightness of the
LED. First, the brightness is passed through a cubic ease-out function. This
produces a more pleasing dimming as opposed to linearly dimming the LED, as the
human eye senses brightness on a logarithmic scale. Second, the brightness is
scaled down by an amount proportional to its distance from the last key pressed.
The scaling function being used is `5 / (5 + n)` where `n` is the distance in
LED index from the brightest LED, which is not dimmed because its scaling
fraction evaluates to `1`.

The `sethsv` function can then be passed a hue (given as a parameter and
unchanging as the LED is dimmed), a saturation (always the maximum), the value
(or brightness, as just calculated), and the index of the LED. Finally, I flush
my LED settings out to the LED peripheral with the `rgblight_set` function.

# Result

With a little logic, the backlights on each keyboard half act just as I wanted,
lighting up reactively and fading when no more keys are pressed.

Take a look!

![Typing on the Keyboard](/qmk/keyboard.mp4)

# Footnotes

[^1]: Quite a bit!

[^2]: I use the [Helix](https://helix-editor.com/) editor, meaning I type
      Escape almost as much as I type Space.
