Most MacBooks sold since 2021 contain a sensor almost nobody uses: a magnetometer in the hinge that reports, continuously, exactly how far open the lid is. macOS reads it to decide when to sleep. Almost nothing else does.
On this page
What the sensor actually is
It is not a switch. Every laptop ever made has a closed-lid switch — usually a Hall effect sensor that trips when a magnet in the display comes near a sensor in the base. That gives you one bit: open or shut.
The lid angle sensor is different. It reports a continuous angle, updated many times a second, across the full range of the hinge. Apple appears to use it for graceful sleep timing and for the "closing the lid while a file is saving" behaviour, but it is exposed to userspace as an ordinary HID device, which means anything running on your Mac can read it.
That distinction matters enormously if you want to build something with it. A switch can only trigger an event. An angle can drive an animation.
Which MacBooks have one
Apple has never documented this, so the list below comes from community testing — chiefly the work around Sam Henri Gold's LidAngleSensor, which is where the report format below was first worked out.
| Model | Sensor |
|---|---|
| MacBook Pro 14" and 16", 2021 onward (M1 Pro/Max through M4) | Yes |
| MacBook Pro 16", 2019 (Intel) | Yes |
| MacBook Air M2 (2022), 15" M2 (2023), M4 (2025) | Yes |
| MacBook Air M3 (2024) | Probably |
| MacBook Air M1, all 13" MacBook Pro | No |
The quickest way to check your own machine is to look for the device directly. Open Terminal and run:
ioreg -l -w 0 | grep -c '"ProductID" = 33028'
33028 is 0x8104 in decimal. Any number above zero means the hardware is present. On my MacBook Pro (Mac15,6, M3 Pro) this returns 12, because the sensor publishes several HID interfaces and each appears more than once in the registry tree.
What it reports
The sensor lives on Apple's vendor ID 0x05AC, product 0x8104. The readable interface sits on the standard HID Sensor usage page 0x20, with usage 0x8A (Orientation). The same product ID also publishes several vendor-specific interfaces on usage page 0xFF00 — those exist, respond to nothing useful, and are the single most common reason a first attempt at this returns nothing.
Feature report 1 gives you the angle. The layout is three bytes:
byte 0 report ID (always 1)
byte 1 angle, low byte
byte 2 angle, high byte
So the angle is a little-endian 16-bit integer in degrees. A raw read on my machine with the lid fully open returns:
01 85 00 → 0x0085 = 133°
Shut, it reads close to 0. Fully open on a 14-inch Pro is around 130–135°, and the hinge physically stops there. Values above about 200 are garbage and should be discarded rather than clamped — a bad read is usually a transient failure, not a real extreme angle.
Reading it yourself in Swift
No entitlement, no TCC prompt, no special permission. This is plain IOKit HID:
import IOKit.hid
let none = IOOptionBits(kIOHIDOptionsTypeNone)
func lidAngle() -> Double? {
let match: [String: Any] = [
kIOHIDVendorIDKey as String: 0x05AC,
kIOHIDProductIDKey as String: 0x8104,
kIOHIDDeviceUsagePageKey as String: 0x20,
kIOHIDDeviceUsageKey as String: 0x8A
]
let manager = IOHIDManagerCreate(kCFAllocatorDefault, none)
IOHIDManagerSetDeviceMatching(manager, match as CFDictionary)
guard IOHIDManagerOpen(manager, none) == kIOReturnSuccess else { return nil }
defer { IOHIDManagerClose(manager, none) }
let devices = (IOHIDManagerCopyDevices(manager) as? Set<IOHIDDevice>) ?? []
for device in devices {
guard IOHIDDeviceOpen(device, none) == kIOReturnSuccess else { continue }
defer { IOHIDDeviceClose(device, none) }
var bytes = [UInt8](repeating: 0, count: 8)
var count = CFIndex(bytes.count)
let result = IOHIDDeviceGetReport(device, kIOHIDReportTypeFeature, 1, &bytes, &count)
guard result == kIOReturnSuccess, count >= 3 else { continue }
let degrees = Double(UInt16(bytes[2]) << 8 | UInt16(bytes[1]))
guard (0...200).contains(degrees) else { continue }
return degrees
}
return nil
}
Note the loop. The match returns several devices and only some of them answer; you have to try each one and keep the first that produces a plausible number. Bailing out after the first failure is the second most common reason this doesn't work.
Four gotchas that cost me a day
1. The device set is unordered
IOHIDManagerCopyDevices returns a Set, so iteration order changes between runs. On one launch the readable interface comes first; on the next it comes third. Any code that assumes a position will work intermittently, which is worse than failing outright.
2. Vendor-specific interfaces open successfully and then return nothing
IOHIDDeviceOpen succeeds on the 0xFF00 interfaces. It is IOHIDDeviceGetReport that fails, with 0xE00002C7. So "did the open succeed" is not a useful test for whether you found the right device — only a successful read is.
3. Other processes contend for it
If two processes poll the sensor at once, reads start failing for both. While building Still I had a debug tool polling in a terminal and the app itself polling in the background, and spent a while convinced the app had a bug. It did not. If your reads suddenly stop, check nothing else is holding the device — and design for recovery rather than giving up permanently after N failures.
4. Poll rate
Roughly 30 Hz is plenty. The hinge is a physical object moved by a human hand; there is nothing above that rate to capture. Still polls every 33 ms and smooths the result frame-rate-independently, because raw sensor values are noisy enough to visibly jitter an animation.
Smoothing that survives a frame rate change
Use current + (target − current) × (1 − exp(−rate × dt)) rather than a fixed lerp factor. A plain lerp(current, target, 0.2) per frame moves at a different speed on a 120 Hz display than on a 60 Hz one — which is exactly the kind of bug that only shows up on someone else's machine.
What you can build with it
Very little uses this sensor, which is most of the fun. Things that become possible once you have a continuous angle:
- Animations driven by the hinge. This is what Still does — it folds, blurs and dims the desktop as the lid comes down, so closing the laptop looks like the iPhone Duo folding shut.
- Angle-triggered automation. Pause music below 30°, mute the mic below 45°, run a backup when the lid drops past a threshold rather than waiting for sleep.
- Theremin-style instruments. Map angle to pitch. Genuinely playable, entirely ridiculous.
- Posture and usage telemetry. How far open do you actually keep your laptop? Nobody knows this about themselves.
The constraint worth designing around: the built-in display sleeps before the lid reaches 0°, so anything visual has to do its work in the range roughly between fully open and 20°. Below that, nobody is looking.
Still is the sensor, put to work
An open source menu bar app that folds your desktop as the lid comes down. MIT licensed, nothing leaves your Mac, and the sensor code above is the real thing it ships with. Set up free Duo animation for Mac, try the interactive demo or read the source on GitHub.