SM_TD is a QMK user library that makes Home Row Modifiers (HRMs) and Tap Dance reliable during fast typing. It improves tap vs. hold decisions by analyzing key releases (not just presses).
Typing often involves overlapping keypresses. For example:
↓h ↓i ↑h ↑i
This happens when you type "hi" quickly. But QMK’s default behavior may misinterpret ↓h as a hold, not a tap, because ↓i occurred before ↑h.
This leads to bugs when using keys like LT(1, KC_H) for home row mods — triggering layer_move(1) instead of typing h.
SM_TD solves this by:
- Interpreting keys based on release timing
- Respecting natural typing habits
- Avoiding false holds during fast sequences
This library follows the natural overlap that happens when we type quickly. In the hi example, most people press i before releasing h — i.e., ↓h, ↓i, ↑h, ↑i.
Stock QMK often interprets these in strict press order, which can misclassify a tap-hold key (e.g., LT(1, KC_H)) as a hold, leading to layer_move(1) instead of a tap.
SM_TD respects your habits rather than forcing you to change them. It pays attention to the time between key releases and interprets them accordingly:
↓h,↓i,↑h(tiny pause),↑i→ treat as a combo-like overlap: hold/action onh+ tapi↓h,↓i,↑h(long pause),↑i→ treat as sequential taps: taph+ tapi
- Human-friendly tap+tap vs. hold+tap interpretation for MT and LT
- Per-key behavior tuning (e.g., hold after N taps in a row)
- Immediate Tap Dance-style responses (no extra timeout needed)
- Configurable timeouts per key or globally
- Feature flags per key or globally
- Debugging tools
- Caps Word: full integration with QMK Caps Word (shifts letters, ends on word-breaking keys, respects
caps_word_press_user) - Standard QMK
MT()/LT()keycodes support (viaSMTD_ENABLE_QMK_TAPHOLD) - Chordal hold ("opposite-hands rule", opt-in via
SMTD_CHORDAL_HOLD): a tap-hold settles as hold only with an opposite-hand key, so same-hand rolls stay taps - Plays well with other QMK features: sm_td taps go through the regular
process_record()pipeline - Combos: partial support for QMK Combos
There are two ways to install SM_TD:
- In
rules.mk, addDEFERRED_EXEC_ENABLE = yesandSRC += sm_td.c. - In
config.h, add#define MAX_DEFERRED_EXECUTORS 10(or increase if already defined). - Copy
sm_td/sm_td.handsm_td/sm_td.cinto yourkeymaps/<your_keymap>/folder (next tokeymap.c). - Add
#include "sm_td.h"in yourkeymap.c. - Check
process_smtd(...)first inprocess_record_user(...)like this:
bool process_record_user(uint16_t keycode, keyrecord_t *record) {
if (!process_smtd(keycode, record)) {
return false;
}
// your code here
return true;
}-
In
keymap.json, add:{ "modules": ["stasmarkin/sm_td"] }
That’s it — proceed to Configuration.
-
Create an
on_smtd_action()function in yourkeymap.cthat handles extra actions for keycodes. For example, to useKC_A,KC_S,KC_D, andKC_Ffor Home Row Mods:smtd_resolution on_smtd_action(uint16_t keycode, smtd_action action, uint8_t tap_count) { switch (keycode) { SMTD_MT(KC_A, KC_LEFT_GUI) SMTD_MT(KC_S, KC_LEFT_ALT) SMTD_MT(KC_D, KC_LEFT_CTRL) SMTD_MT(KC_F, KC_LSFT) } return SMTD_RESOLUTION_UNHANDLED; }
Optional: if you want to keep standard QMK
MT()/LT()in your keymap (noSMTD_MT/SMTD_LT), add#define SMTD_ENABLE_QMK_TAPHOLD 1to yourconfig.h. This routes QMK mod-tap and layer-tap keycodes through sm_td timing. (Advanced features like tap-count thresholds still requireSMTD_MT/SMTD_LT.) See the Customization Guide and practical Examples for more patterns.Optional: enable the chordal-hold "opposite-hands rule" with
#define SMTD_CHORDAL_HOLD 1in yourconfig.h. A tap-hold then settles as hold only when an opposite-hand key is involved; same-hand rolls stay taps. Provide achordal_hold_layout[MATRIX_ROWS][MATRIX_COLS]marking each key'L'/'R'/'*'(left / right / neutral thumb), or overridechar smtd_chordal_handedness(keypos_t key)to compute handedness yourself. -
(optional) Add global configuration parameters to your
config.hfile (see timeouts and feature flags). -
(optional) Add per-key configuration (see timeouts and feature flags).
| Macro | Description |
|---|---|
SMTD_MT(KC_A, KC_LEFT_GUI) |
Basic mod-tap: Tap KC_A → single tap, Hold KC_A → KC_LEFT_GUI hold |
SMTD_MT(KC_A, KC_LEFT_GUI, 2) |
Tap count mod-tap: Same as above, but hold after 2 sequential taps results in KC_A hold• ↓KC_A, ↑KC_A, ↓KC_A... → KC_A tap + KC_LEFT_GUI hold• ↓KC_A, ↑KC_A, ↓KC_A, ↑KC_A, ↓KC_A... → 2× KC_A tap + KC_A hold |
SMTD_MT(KC_A, KC_LEFT_GUI, 1, false) |
Caps Word disabled: Basic mod-tap with QMK’s Caps Word feature disabled |
SMTD_MTE(KC_A, KC_LEFT_GUI) |
Eager mod-tap: Holds KC_LEFT_GUI immediately on press• Quick release → KC_LEFT_GUI released + KC_A tapped• Continue holding → KC_LEFT_GUI held, no KC_A tap• Useful for fast mod+mouse clicks |
SMTD_MTE(KC_A, KC_LEFT_GUI, 2) |
Eager with tap count: Eager version of tap count mod-tap |
SMTD_MTE(KC_A, KC_LEFT_GUI, 1, false) |
Eager caps disabled: Eager version with Caps Word disabled |
SMTD_LT(KC_A, 2) |
Layer tap: Momentary layer switching (layer 2), works like SMTD_MT but switches layers instead of modifiers |
SMTD_LT(KC_A, 2, 3) |
Layer tap with count: Hold after 3 sequential taps results in KC_A hold• ↓KC_A, ↑KC_A, ↓KC_A... → KC_A tap + layer 2 activation• ↓KC_A, ↑KC_A, ↓KC_A, ↑KC_A, ↓KC_A, ↑KC_A, ↓KC_A... → 3× KC_A tap + KC_A hold |
SMTD_LT(KC_A, 2, 1, false) |
Layer tap caps disabled: Same as above with Caps Word disabled |
SMTD_MT_ON_MKEY(CKC_A, KC_A, KC_LEFT_GUI) |
Mod-tap with custom keycode: Uses custom keycode CKC_A (do not forget to declare it) in keymap while treating it as KC_A tap and KC_LEFT_GUI hold• Might be used if you need different behavior of KC_A on different layers• Useful for migration from older SM_TD versions or when you need custom keycodes |
SMTD_LT_ON_MKEY(CKC_A, KC_A, 2) |
Layer tap with custom keycode: Uses custom keycode CKC_A (do not forget to declare it) in keymap while treating it as KC_A tap and layer 2 activation• Might be used if you need different behavior of KC_A on different layers• Useful for migration from older SM_TD versions or when you need custom keycodes |
There is a /docs folder with extensive documentation.
Also, you may check my layout for a real-world example of using this library.
Start with GitHub issues or pull requests for questions and ideas.
You can also join the SM_TD Discord channel, or reach me on Reddit (u/stasmarkin) or Discord (stasmarkin).
Also, you may email me or tag/text me on Reddit (u/stasmarkin) or Discord (stasmarkin).
If you find this library helpful, consider supporting the project:
Crypto support:
- USDT on TRON:
TE4QifvjnPSQoT4oJXYnYAnZxBKAvwUFCN - ByBit ID:
230327759
Your support helps me continue developing and maintaining this project. Thank you for using SM_TD!
- Full-pipeline taps + Caps Word (#23) and
SMTD_ENABLE_QMK_TAPHOLDfor nativeMT()/LT()(0.5.6), QMK community module integration (0.5.1 / 0.5.5), split into.h+.c(0.5.4), 3+ finger roll interpretation and a collection of macros (0.5.0 / 0.5.3), AVR fix (#48, 0.5.2), and assorted bug fixes
- Feature: dynamic release timeout derived from your typing rhythm (fixes #45); a new global flag tunes the window (set to 0 for the old fixed-timeout behavior)
- Fix: guard against state double-removal crash in
smtd_apply_stage - Fix:
SMTD_TK/SMTD_TTOmacro expansion - New:
smtd_reset()for test harnesses
- Fix:
SMTD_LTuses nativelayer_on/layer_off, so it no longer wipes foreign layer bits on release (fixes #57, unblocks tri-layer #44) - Fix: a held key released under a stacked key now finalizes instead of hanging its modifier (fixes #58)
- Feature: chordal hold ("opposite-hands rule") via
SMTD_CHORDAL_HOLD— same-hand rolls stay taps, cross-hand chords hold; handedness from achordal_hold_layoutarray or an overridablesmtd_chordal_handedness()(#60) - Fix: custom / derived keycode taps now feed the QMK leader buffer, so Leader sequences see them (fixes #29)
- Feature:
SMTD_GLOBAL_RELEASE_PERCENTcontrols the dynamic release window (min(p1, p2) * percent / 100) with fine, single-percent granularity. Behavior change: the default is nowSMTD_GLOBAL_RELEASE_PERCENT 30(a slightly wider window — fewer hold→tap-tap misfires); setSMTD_GLOBAL_RELEASE_PERCENT 20to restore the previous behavior
- Fix: chordal hold holds (not taps) when a neutral (
'*') key follows a mod-tap, matching the hold-timeout path (#62)
- better combo support
- other feature requests (see issues)
- stable API
- memory optimizations (on storing active states)
- memory optimizations (on state machine stack size)
- teddybear for docs fixes
- MrMustardTBC for docs fixes
- mikenrafter for cool macros
- alextverdyy for qmk module support
- TickKleiner for community module multiple definition error fix
- gitbyflux for chordal hold support and the neutral-key fix
- Azzam S.A
- Thiago Alves
- Julian Hirn
- Beau Haan
- Str8Razor
- PineappleOfD!scord
- Alexander Spitaler
- Josh Stobbs
- Yousef Hadder
- WhoAmiI
- Slava
(please, let me know, if I have forgotten someone)
