diff --git a/javascript/js-extension/README.md b/javascript/js-extension/README.md
new file mode 100644
index 0000000..e11fe44
--- /dev/null
+++ b/javascript/js-extension/README.md
@@ -0,0 +1,96 @@
+# js-extension (JavaScript)
+
+A tiny, dependency‑free utility class with a few DOM and string helpers that I often reach for in small projects and prototypes.
+
+It includes:
+- `addGlobalEventListener` — delegated events with a CSS selector
+- `createElement` — quick DOM element creation with attributes, class, dataset, and text
+- `qs` / `qsa` — short aliases for `querySelector` / `querySelectorAll` with optional parent
+- `strToHtml` — convert an HTML string to a `DocumentFragment`
+- `isJson` — quick check if a string parses as JSON
+- `sleep` — simple delay (`Promise`-based)
+
+## Quick usage
+
+Minimal examples showing what the helpers do. Adjust to your environment as needed.
+
+```js
+// Assume jsExtension is available in scope
+
+// 1) Delegated event listener
+jsExtension.addGlobalEventListener("click", "[data-action=remove]", (e) => {
+ const btn = e.target.closest("[data-action=remove]");
+ btn?.closest(".item")?.remove();
+});
+
+// 2) Create element with attributes, class, dataset, and text
+const card = jsExtension.createElement("div", {
+ class: "card",
+ id: "card-1",
+ dataset: { id: "1", type: "example" },
+ "aria-label": "Example card",
+ text: "Hello!",
+});
+
+// 3) Shorthand query helpers
+const firstItem = jsExtension.qs(".item");
+const allItems = jsExtension.qsa(".item");
+
+// 4) String to HTML fragment
+const frag = jsExtension.strToHtml(`
+
+`);
+document.body.appendChild(frag);
+
+// 5) JSON check
+jsExtension.isJson('{"a":1}'); // true
+jsExtension.isJson('{a:1}'); // false
+
+// 6) Sleep helper
+await jsExtension.sleep(300);
+```
+
+## API
+
+### `addGlobalEventListener(type, selector, callback, options?, parent=document)`
+Adds a single delegated listener on `parent` and runs `callback` when the event target matches `selector` (or is inside an element that matches).
+
+- `type`: string event type, e.g. `"click"`
+- `selector`: CSS selector to match or `closest()` to
+- `callback`: function receiving the event
+- `options`: optional `addEventListener` options
+- `parent`: `HTMLElement | Document` (defaults to `document`)
+
+### `createElement(type, options={}) => HTMLElement`
+Creates an element and applies the provided options.
+Supported option keys:
+- `class`: string — added via `classList.add`
+- `dataset`: object — assigned to `element.dataset[key] = value`
+- `text`: string — sets `textContent`
+- any other key — assigned as attribute: `setAttribute(key, value)`
+
+### `qs(selector, parent=document) => Element|null`
+Shorthand for `parent.querySelector(selector)`. Returns `null` if `parent` is not a `Document`/`HTMLElement`.
+
+### `qsa(selector, parent=document) => NodeList`
+Shorthand for `parent.querySelectorAll(selector)`. Falls back to `document.querySelectorAll` when `parent` is not a `Document`/`HTMLElement`.
+
+### `strToHtml(str) => DocumentFragment`
+Converts an HTML string into a `DocumentFragment` using `Range#createContextualFragment`.
+
+### `isJson(str) => boolean`
+Returns `true` if `JSON.parse(str)` succeeds; otherwise `false`.
+
+### `sleep(time) => Promise`
+Resolves after `time` milliseconds using `setTimeout`.
+
+## Notes
+- DOM helpers assume a browser environment (i.e., `document` is available).
+- `addGlobalEventListener` uses event delegation; attach it once at a suitable ancestor.
+
+## License
+
+See the repository-level `LICENSE` file.
diff --git a/javascript/js-extension/js-extension.js b/javascript/js-extension/js-extension.js
new file mode 100644
index 0000000..be5a209
--- /dev/null
+++ b/javascript/js-extension/js-extension.js
@@ -0,0 +1,102 @@
+export default class jsExtension {
+ /**
+ * @param {string} type
+ * @param {string} selector
+ * @param {Function} callback
+ * @param options
+ * @param {HTMLElement|Document} parent
+ */
+ static addGlobalEventListener(type, selector, callback, options, parent = document) {
+ parent.addEventListener(type, (eventListener) => {
+ const target = eventListener.target;
+
+ if (!(target instanceof Element)) return;
+
+ if (target.matches(selector) || target.closest(selector)) {
+ callback(eventListener);
+ }
+ }, options);
+ }
+
+ /**
+ * @param {string} type
+ * @param {Object} options
+ * @returns {HTMLElement}
+ */
+ static createElement(type, options = {}) {
+ const htmlElement = document.createElement(type);
+
+ for(const [_key, _value] of /** @type {[string, string][]} */Object.entries(options)) {
+
+ if(_key === "class") {
+ htmlElement.classList.add(_value);
+ break;
+ }
+
+ if(_key === "dataset") {
+ Object.entries(_value).forEach(([dataKey, dataValue]) => {
+ htmlElement.dataset[dataKey] = dataValue;
+ });
+ break;
+ }
+
+ if(_key === "text") {
+ htmlElement.textContent = _value;
+ break;
+ }
+
+ htmlElement.setAttribute(_key, _value);
+ }
+
+ return htmlElement;
+ }
+
+ /**
+ * @param {string} selector
+ * @param {HTMLElement|Document} parent
+ * @returns {Element|null}
+ */
+ static qs(selector, parent = document) {
+ return parent instanceof Document || parent instanceof HTMLElement
+ ? parent.querySelector(selector)
+ : null;
+ }
+
+ /**
+ * @param {string} selector
+ * @param {HTMLElement|Document} parent
+ * @returns {NodeList}
+ */
+ static qsa(selector, parent = document) {
+ return parent instanceof Document || parent instanceof HTMLElement
+ ? parent.querySelectorAll(selector)
+ : document.querySelectorAll(selector);
+ }
+
+ /**
+ * @param {string} str
+ * @return {DocumentFragment}
+ */
+ static strToHtml(str) {
+ return document.createRange().createContextualFragment(str.trim());
+ }
+
+ /**
+ * @param str
+ * @returns {boolean}
+ */
+ static isJson(str) {
+ try { JSON.parse(str); } catch(e) { return false; }
+
+ return true;
+ }
+
+ /**
+ * @param {number} time
+ * @returns {Promise}
+ */
+ static sleep(time) {
+ return new Promise(resolve => setTimeout(resolve, time));
+ }
+}
+