AutoHotkey v2 GUI → HTML Mapping
Since WebAssembly requires HTML GUIs I’ve mapped out common AutoHotkey GUI elements and thier HTML counterparts.
Reference for translating AHK v2 GUI controls to their browser equivalents.
Quick Reference Table
| AHK v2 Control | HTML Equivalent | Notes |
|---|---|---|
| Text | <label> or <span> | Use <label for> when it names an input, <span> otherwise |
| Edit | <input type=”text”> | Single-line default |
| Edit +Multi | <textarea> | Different tag, not an attribute |
| Edit +Password | <input type=”password”> | Toggle by swapping the type attribute |
| Edit +Number | <input type=”number”> | Gets its own spinner, so no separate UpDown needed |
| Edit +ReadOnly | <input readonly> | readonly still submits; disabled does not |
| Button | <button> | Add type=”button” so it does not submit a form |
| Checkbox | <input type=”checkbox”> | Label text is a separate element in HTML |
| Radio | <input type=”radio” name=”grp”> | Grouping is by shared name attribute, not by add-order |
| DropDownList (DDL) | <select> + <option> | Closest one-to-one match in the whole list |
| ComboBox | <input list=”x”> + <datalist> | Editable dropdown; datalist is the suggestion source |
| ListBox | <select size=”8″> | size > 1 turns a select into a list box |
| ListBox +Multi | <select multiple> | Read via .selectedOptions |
| ListView | <table> + <thead>/<tbody> | Sorting, selection, and icons are all hand-written JS |
| TreeView | nested <ul>/<li> or <details> | <details> gives free expand/collapse with no JS |
| Progress | <progress> | Omit the value attribute for an indeterminate bar |
| Slider | <input type=”range”> | min / max / step attributes |
| UpDown | <input type=”number”> | Merged into the input; not a separate control in HTML |
| GroupBox | <fieldset> + <legend> | legend renders as the caption, same as AHK |
| Tab3 | buttons + <div> panels | No native tab element; JS toggles panel visibility |
| Picture | <img> | Embed as base64 data URI to keep one self-contained file |
| Link | <a href> | AHK Link already uses <a> markup, so this is near-identical |
| Hotkey | <input> + keydown capture | No native control; capture the event and format it yourself |
| DateTime | <input type=”datetime-local”> | Also date, time, month, week variants |
| MonthCal | <input type=”date”> | Browser supplies its own calendar popup |
| StatusBar | fixed-position <div> | position:fixed; bottom:0 |
| Custom / ActiveX | no equivalent | Rebuild in JS, or use <canvas> for drawn controls |
| MsgBox | alert() / confirm() | Or <dialog> for a styled, non-blocking version |
| InputBox | prompt() | Crude but functional; <dialog> for anything real |
| FileSelect | <input type=”file”> | Returns a File object, never a path. See caveats |
| DirSelect | no real equivalent | webkitdirectory picks a folder’s files, not a path |
| ToolTip | title=”” or CSS :hover | title is free but ugly and slow to appear |
| Menu / MenuBar | <nav> + <ul> + CSS | Fully hand-built; no native menu bar in HTML |
Side-by-Side Code
AHK on the left, HTML on the right. Same control, same intent.
1. The window itself
AutoHotkey v2 MyGui := Gui("+Resize", "My Tool")
MyGui.BackColor := "202020"
MyGui.SetFont("s10 cWhite", "Segoe UI")
MyGui.Show("w400 h300") | HTML + CSS <title>My Tool</title> <body style="background:#202020; color:#fff; font:10pt 'Segoe UI'; width:400px; height:300px;"> </body> |
The browser tab is your title bar. +Resize is the default in a browser, not something you opt into.
2. Text label and Edit box
AutoHotkey v2 MyGui.Add("Text", "x10 y10 w80", "Name:")
NameBox := MyGui.Add("Edit"
, "x100 y10 w200 vUserName")
; read it later
Value := NameBox.Value | HTML + JS <label for="userName">Name:</label>
<input type="text" id="userName"
style="width:200px;">
// read it later
let v = document
.getElementById('userName').value; |
AHK’s vVarName becomes HTML’s id. Both are the handle you grab the value by. Note that .Value and .value line up almost exactly.
3. Button with a click handler
AutoHotkey v2 Btn := MyGui.Add("Button"
, "x10 y50 w100 h30", "Click Me")
Btn.OnEvent("Click", RunIt)
RunIt(Ctrl, Info) {
MsgBox("Clicked!")
} | HTML + JS <button type="button" id="btn"
style="width:100px; height:30px;">
Click Me
</button>
document.getElementById('btn')
.addEventListener('click', () => {
alert('Clicked!');
}); |
.OnEvent(“Click”, Fn) and .addEventListener(‘click’, Fn) are the same idea with different spelling. The handler signature differs: AHK passes (Ctrl, Info), JS passes a single event object.
4. Checkbox and Radio
AutoHotkey v2 Chk := MyGui.Add("Checkbox"
, "x10 y90 Checked", "Enable logging")
MyGui.Add("Radio", "x10 y120 Checked", "Fast")
MyGui.Add("Radio", "x10 y145", "Slow")
If (Chk.Value)
LogIt() | HTML + JS <label><input type="checkbox" id="log"
checked> Enable logging</label>
<label><input type="radio" name="spd"
checked> Fast</label>
<label><input type="radio" name="spd">
Slow</label>
if (document.getElementById('log').checked)
logIt(); |
Trap: AHK groups radios by consecutive add-order. HTML groups them by a shared name attribute. A converter must invent a group name per run of adjacent Radio controls. Also: AHK reads .Value (1 or 0), HTML reads .checked (true/false), not .value.
5. DropDownList
AutoHotkey v2 Dd := MyGui.Add("DDL"
, "x10 y180 w150 Choose2"
, ["Red", "Green", "Blue"])
Picked := Dd.Text ; "Green"
Index := Dd.Value ; 2 | HTML + JS <select id="clr" style="width:150px;">
<option>Red</option>
<option selected>Green</option>
<option>Blue</option>
</select>
let s = document.getElementById('clr');
let picked = s.value; // "Green"
let index = s.selectedIndex; // 1 |
Trap: AHK indexes from 1, JS from 0. Choose2 becomes selected on the second option, and selectedIndex reads 1 for it. Every index in a converter needs an off-by-one adjustment.
6. ListView
AutoHotkey v2 LV := MyGui.Add("ListView"
, "x10 y220 w380 h150"
, ["Name", "Size", "Date"])
LV.Add("", "File1.txt", "2 KB", "8/27")
LV.Add("", "File2.txt", "5 KB", "8/26")
LV.ModifyCol(1, 150)
Row := LV.GetNext()
Txt := LV.GetText(Row, 1) | HTML + JS <table id="lv">
<thead><tr>
<th style="width:150px">Name</th>
<th>Size</th><th>Date</th>
</tr></thead>
<tbody>
<tr><td>File1.txt</td>
<td>2 KB</td><td>8/27</td></tr>
<tr><td>File2.txt</td>
<td>5 KB</td><td>8/26</td></tr>
</tbody>
</table> |
Biggest gap in the whole list. AHK’s ListView gives you sorting, row selection, checkboxes, icons, and column dragging for free. An HTML table gives you rows and columns and nothing else. Every one of those behaviors is JS you write yourself (or a grid library you pull in).
7. GroupBox and Tab3
AutoHotkey v2 MyGui.Add("GroupBox"
, "x10 y10 w200 h80", "Options")
MyGui.Add("Checkbox", "xp+10 yp+25", "A")
Tabs := MyGui.Add("Tab3"
, "x10 y100 w380 h200"
, ["General", "Advanced"])
Tabs.UseTab(1)
MyGui.Add("Text",, "On tab 1")
Tabs.UseTab() ; back to no tab | HTML + JS <fieldset> <legend>Options</legend> <label><input type="checkbox"> A</label> </fieldset> <div> <button onclick="show(0)">General</button> <button onclick="show(1)">Advanced</button> </div> <div class="pane">On tab 1</div> <div class="pane" hidden>On tab 2</div> |
GroupBox maps cleanly. Tab3 does not: HTML has no tab control, so you build one from buttons plus panels plus a show() function. Note also that AHK’s UseTab is stateful (everything added afterward lands on that tab) while HTML nesting is explicit.
8. Slider and Progress
AutoHotkey v2 Sld := MyGui.Add("Slider"
, "x10 y10 w200 Range0-100", 50)
Sld.OnEvent("Change", OnSlide)
Prg := MyGui.Add("Progress"
, "x10 y50 w200 h20", 25)
Prg.Value := 75 | HTML + JS <input type="range" id="sld"
min="0" max="100" value="50"
style="width:200px;">
sld.addEventListener('input', onSlide);
<progress id="prg" max="100" value="25"
style="width:200px;"></progress>
prg.value = 75; |
Two of the cleanest mappings. Range0-100 splits into min and max. Use the input event, not change, to fire while dragging like AHK does.
9. Reading every value at once (Gui.Submit)
AutoHotkey v2 Data := MyGui.Submit(false) ; Data.UserName ; Data.Enabled ; Data.Color | HTML + JS <form id="f"> ...controls... </form>
let data = Object.fromEntries(
new FormData(document.getElementById('f'))
);
// data.userName, data.color ... |
FormData is the closest analog to Submit. It needs each control to carry a name attribute, so a converter should emit both id and name from AHK’s vVarName. Caveat: unchecked checkboxes are omitted from FormData entirely rather than reporting 0.
Positioning: the real work
AHK positions by absolute pixel: x10 y20 w100 h30. HTML flows elements one after another and expects CSS to handle layout. Three ways to bridge that:
| Approach | How | Trade-off |
|---|---|---|
| Absolute | position:absolute; left:10px; top:20px; width:100px | Perfect 1:1 with AHK. Easiest to auto-convert. Does not reflow, so it breaks on phones |
| Flexbox | Group controls into rows/columns, let CSS space them | Responsive and idiomatic. Requires inferring intent from coordinates |
| CSS Grid | Map the x/y grid onto grid-column / grid-row | Good for form-like layouts. Most work to derive automatically |
For a converter: start with absolute positioning. It is a direct arithmetic translation, it renders identically to the AHK original, and it lets you verify the conversion is faithful before worrying about responsiveness.
Relative-position options need resolving before you can emit CSS:
| AHK option | Meaning → what the converter must do |
|---|---|
| xp / yp | Same as previous control. Track the previous control’s box and substitute the number |
| xp+10 / yp+25 | Previous plus offset. Same tracking, plus arithmetic |
| x+10 / y+10 | Right of / below previous control’s edge. Needs the previous width/height |
| xm / ym | Back to the left/top margin. Resolve to the margin value |
| xs / ys | Back to the last Section anchor. Track section state |
| (omitted w/h) | The hard one. AHK auto-sizes from text and font metrics. A static converter cannot know the result without measuring, so let CSS auto-size instead and accept some drift |
Event Mapping
| AHK OnEvent | JS event | Notes |
|---|---|---|
| “Click” | click | Direct match |
| “DoubleClick” | dblclick | Direct match |
| “Change” | input (or change) | Use input to fire per keystroke; change only fires on blur |
| “Focus” / “LoseFocus” | focus / blur | Direct match |
| “ContextMenu” | contextmenu | Call preventDefault() to suppress the browser menu |
| “ItemSelect” (LV) | click on <tr> | Hand-rolled; you track the selected row yourself |
| “ColClick” (LV) | click on <th> | Sorting logic is entirely yours |
| Gui “Close” | beforeunload | Cannot be cancelled silently; browser controls the prompt |
| Gui “Size” | resize / ResizeObserver | Often unnecessary once CSS handles layout |
| Gui “DropFiles” | drop + dragover | Gives File objects, never paths |
| Hotkey / hotstring | keydown, page-scoped only | Works only while the page has focus. No system-wide hotkeys, ever |
Styling and Dark Theme
This is where HTML wins outright. Everything you fight for in an AHK dark theme is one CSS line in a browser.
| AHK v2 | CSS | Comment |
|---|---|---|
| Gui.BackColor := “202020” | background:#202020 | Same |
| SetFont(“s10 cWhite”, “Segoe UI”) | font:10pt ‘Segoe UI’; color:#fff | Same |
| WM_CTLCOLOREDIT subclassing | input { background:#2d2d2d } | Dozens of lines of DllCall become one rule |
| NM_CUSTOMDRAW for LV rows | tr:nth-child(even) { … } | No crash risk, no focus-loss bug |
| Dark LV header hack | th { background:#2d2d30 } | Trivial in CSS |
| Themed button text color (impossible) | button { color:#fff } | Just works |
| DPI scaling math | handled by the browser | Nothing to do |
| Startup flash workaround | n/a | Does not happen |
Add color-scheme: dark; to your CSS and the browser darkens scrollbars, native dropdown popups, and date pickers for you.
No HTML Equivalent
These are not conversion difficulties. They are browser sandbox limits, and no amount of clever code gets around them.
- System-wide hotkeys — JS key events fire only when the page has focus
- Send / Click / MouseMove — a page cannot drive other applications
- WinActivate / WinMove / window management — no access to other windows
- File paths — <input type=”file”> hands you contents, never a path. FileRead of an arbitrary path is impossible
- Registry, INI files, DllCall, COM — no OS access at all
- Tray icon and tray menu — no system tray from a page
- Always-on-top, borderless, click-through, transparency — the browser owns the window chrome
- Multi-monitor positioning — the page cannot place itself on a specific screen
- Clipboard (unrestricted) — partial. Writing needs a user gesture, reading needs explicit permission, and ClipboardAll has no analog
Rule of thumb: the GUI layer converts well. The automation layer does not convert at all. A script whose GUI is a front-end for data in and results out ports cleanly. A script that drives other applications cannot become a web page at any level of effort.
If You Build the Converter
Parsing order that avoids most of the pain:
- Find the Gui() constructor. Pull the title, options, BackColor, and SetFont into page-level CSS.
- Walk every .Add() call in source order, keeping a running cursor of the previous control’s x, y, w, h so xp / yp / x+ / y+ / xm / xs can be resolved to absolute numbers.
- Emit one HTML element per control with position:absolute and the resolved coordinates.
- Map vVarName to both id and name. Auto-generate names for controls that lack a v-option.
- Collect .OnEvent() calls and emit matching addEventListener stubs with empty bodies.
- Leave every handler body as a TODO comment. Do not attempt to translate AHK logic in v1 of the tool.
Known parser traps worth handling early:
- Consecutive Radio controls need a shared generated group name
- Index off-by-one everywhere (AHK is 1-based, JS is 0-based)
- Controls added inside a GroupBox or Tab are parent-relative in AHK, so nesting changes the coordinate origin
- UseTab is stateful and affects every subsequent Add until reset
- Omitted w/h means auto-size from font metrics, which is unresolvable statically
- Controls added in a loop or with computed positions cannot be resolved without running the script
- Continuation sections and line-continued Add calls must be joined before parsing












AHK Hero 🦸
⏩SHARE WITH A FRIEND⏩


