macos-keyboard-backlight
v1.1.0
Published
Control the MacBook keyboard backlight from the command line or Node - get/set brightness and toggle ambient auto-brightness. No native build step.
Downloads
146
Maintainers
Readme
Control the MacBook keyboard backlight from the command line.
npm i -g macos-keyboard-backlight then kbdlight off
macos-keyboard-backlight.vercel.app
What this is
macOS gives you a keyboard backlight and no way to script it. There is a slider in System Settings and two keys on the keyboard, and that is the whole interface. You cannot turn it off from a shell script, a scheduled job, or a Node program.
This package adds that missing interface, as a kbdlight command and as a small
Node API.
kbdlight get # 0.5000
kbdlight set 0.2
kbdlight offThe command is real. The MacBook is an illustration: the backlight is hardware, so no screen recording can show it. Full quality version
There is no native build step. No node-gyp, no Xcode, no compiler on the installing machine. It calls the system API directly from JavaScript through koffi, which ships prebuilt for both Apple Silicon and Intel.
Install
npm install -g macos-keyboard-backlightThat installs a kbdlight command. To run it once without installing:
npx macos-keyboard-backlight getAs a library in a project:
npm install macos-keyboard-backlightCommand line
kbdlight get print brightness (0.0-1.0) of each keyboard
kbdlight set <0.0-1.0> set brightness on all keyboards
kbdlight off backlight off (alias for: set 0)
kbdlight max full brightness (alias for: set 1)
kbdlight auto print auto-brightness state (1/0) per keyboard
kbdlight auto off disable the ambient light sensor
kbdlight auto on re-enable it
kbdlight list print keyboard backlight IDs
kbdlight sun on follow sunrise and sunset (see below)
kbdlight --helpThe help is colour-coded when it is going to a terminal, and plain when it is
piped. NO_COLOR, FORCE_COLOR and --no-color all work.
Holding a fixed level
This is the part that surprises people. Auto-brightness keeps re-adjusting the backlight from the ambient light sensor, so a level you set drifts back on its own. Turn the sensor off first, and the value sticks:
kbdlight auto off && kbdlight set 0.5Re-enable the sensor later with kbdlight auto on.
Sun mode
kbdlight sun on puts the backlight on the sky's schedule: off through the day,
up to a level you pick after sunset, back down at sunrise.
kbdlight sun on # dark by day, 0.6 after sunset
kbdlight sun on --night 0.4 --day auto
kbdlight sun # today's times and what is armed
kbdlight sun off # stop, and put back the state it foundsun mode: on
where 52.52, 13.41 (Europe/Berlin)
sunrise 05:26 sunset 20:58
now night, level 0.60
next 2.8.2026, 05:28:02
agent loaded (checks every 5 min)Sunrise and sunset are computed locally with the NOAA solar algorithm, so the
switch tracks the season instead of a clock time that is right in March and an
hour wrong in June. Coordinates come from the machine's timezone, which is a
city name and therefore accurate to well under the minute that matters here.
Pass --at 48.14,11.58 once if you want your own, and --rise-offset /
--set-offset to shift either switch by a number of minutes.
A launchd agent (local.kbdlight.sun) re-checks every five minutes rather
than firing at two computed times. Sunrise moves daily, so a clock-time job has
to rewrite itself every day and can drift, double up, or quietly disappear; a
poll cannot. It also catches up on the first wake if the Mac slept through the
moment.
Between two checks the keyboard is yours: the agent only writes on a phase
change, so turning the backlight down in the evening sticks. kbdlight sun off
unloads the agent and restores the exact brightness and sensor state from before
sun mode started.
auto and sun are independent and compose. auto is the hardware ambient
sensor reacting to the room right now; sun is a schedule. --day auto hands
the daylight hours back to the sensor and keeps the evening on a fixed level.
Node API
const kbd = require('macos-keyboard-backlight');
kbd.get(); // -> 0.5 brightness of the first keyboard
kbd.set(0.5); // -> 0.5 set all keyboards, clamped to 0.0-1.0
kbd.isAuto(); // -> true is auto-brightness enabled?
kbd.setAuto(false); // -> false disable the ambient sensor
kbd.keyboardIDs(); // -> [95159106] IDs of backlit keyboardsget, set, isAuto and setAuto take an optional trailing keyboardID to
target one keyboard instead of all of them. keyboardIDs() takes no arguments;
it is what enumerates them:
const [id] = kbd.keyboardIDs();
kbd.set(0.2, id);All functions throw a descriptive Error on a non-macOS host, when
CoreBrightness cannot be reached, or when no backlit keyboard is present. Catch
it rather than feature-detecting:
try {
kbd.set(0);
} catch (e) {
console.error('no backlight here:', e.message);
}Recipes
Backlight off during the day, lit in the evening. This used to be a shell
script comparing date +%H against two numbers you had to keep re-tuning as the
seasons moved. It is now kbdlight sun on; see below.
Dim the keyboard while a screen recording runs, then restore it:
const kbd = require('macos-keyboard-backlight');
const before = { level: kbd.get(), auto: kbd.isAuto() };
kbd.setAuto(false);
kbd.set(0);
await record();
kbd.set(before.level);
kbd.setAuto(before.auto);Always snapshot isAuto() alongside the level. Restoring the brightness but
leaving the sensor disabled looks fine for a minute, then silently stops adapting
to the room.
Your keyboard as a status light
Once brightness is scriptable, the backlight becomes an output device for
anything your machine knows. Three working programs live in
examples/.
Know when Claude Code is done, without watching the terminal
You send a prompt, switch to a browser, and then keep checking back to see
whether the agent has finished. Add one line to ~/.claude/settings.json and
the keyboard blinks four times when the answer lands.
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "kbdlight pulse --detach" }] }
]
}
}The rhythm is the shipped default, frame for frame. The MacBook is an illustration: the backlight is hardware, so no screen recording can show it.
Hard on, hard off, four times. Not a fade: in peripheral vision a ramp gets integrated into a vague glow and you miss it, which defeats the entire purpose.
The timing is slow for the same reason, and the hardware is not the constraint.
Measured through backlightLevelForKeyboard:, which reports the driver's real
output instead of the value you asked for, the LEDs reach full within 26 ms and
drop just as fast. What needs the second of lit time is the room.
kbdlight pulse # 4 blinks, then exactly the state it found
kbdlight pulse 2 --peak 0.6 # quieter
kbdlight pulse --mute-until 09:00 # nothing until morning
kbdlight pulse --unmuteThe hook returns in about 50 ms. Claude Code waits for Stop hooks before the
turn ends, so --detach hands the blinking to a background process instead of
holding your session for a second. A lock file keeps two overlapping runs from
fighting over the hardware: without it, the second run would snapshot the
keyboard mid-blink and "restore" it to dark on the way out.
Works the same in any hook that runs a command, for a long build or a finished
test run. examples/claude-code-pulse.js is the
hackable version of the same thing if you would rather edit a script than pass
flags.
Keep a dark room dark, and get the light back
node examples/night-mode.js --until 10:00 # dark now, restore at 10:00
node examples/night-mode.js --status
node examples/night-mode.js --cancelThe restore is the point. It snapshots the level and the auto-brightness flag
per keyboard and schedules the exact reverse as a self-removing launchd agent,
because a setTimeout seven hours out dies with the terminal, the logout, or
the reboot, and "dark until 10:00" with no way back on is just "dark".
Flash on the kick drum
node examples/music-sync.js --verboseffmpeg decodes a loopback device (or the microphone) to mono 8 kHz, a one-pole
low-pass isolates the bass band, and a frame counts as a beat when its energy
beats the last second's average. Against a synthetic 120 bpm track it reports
beats 480 to 512 ms apart, averaging exactly 120 bpm.
What is not possible
A MacBook backlight is a single zone: one channel for the whole board, no
per-key addressing, no colour. Every selector on Apple's private
KeyboardBrightnessClient takes a keyboard ID and nothing finer. So brightness
over time is the only axis there is, and per-key effects need an external RGB
keyboard driven over its own vendor protocol.
The examples are in the git repository, not in the npm package.
How it works
macOS exposes keyboard backlight control through KeyboardBrightnessClient in
the private CoreBrightness framework. This package resolves that class and its
selectors at runtime and calls them through objc_msgSend using koffi's foreign
function interface. Because the calls happen at runtime, nothing is compiled at
install time.
Two consequences worth stating plainly:
- It uses a private Apple framework. It works across current macOS releases,
and Apple can change or remove that interface in any update without notice.
The failure mode is a thrown
Errorrather than a crash, but plan for it in anything unattended. - Each Objective-C signature needs its own prototype.
objc_msgSendis variadic in C, so calling it through a mismatched prototype reads the wrong registers silently. That is why the code declares one typed entry per selector instead of reusing a close-enough one.
Requirements
- macOS, Apple Silicon or Intel
- Node.js 18 or newer
- A Mac with a backlit keyboard
Development
git clone https://github.com/noluyorAbi/macos-keyboard-backlight.git
cd macos-keyboard-backlight
npm install
npm testnpm test drives real hardware: it snapshots the current brightness and sensor
state, exercises the API, and restores both afterwards. On a machine with no
backlit keyboard, including every hosted CI runner, it skips itself rather than
reporting a pass it did not earn.
See CONTRIBUTING.md before opening a pull request, and SECURITY.md to report a vulnerability privately.
License
MIT. See LICENSE.
