# Surreal is 320 lines of surreal.js with no build step and no package.json

> Surreal is a jQuery-shaped layer over querySelector that adds me() for Locality of Behavior and any() to guarantee an array return, in a single 320 line file with no dependencies and no build. The trade is global namespace and camelCase or snake_case on everything.

**gnat/surreal** — 🗿 Mini jQuery alternative. Dependency-free animations. Locality of Behavior.  Use one element or arrays transparently. Pairs with htmx. Vanilla querySelector() but better!

- Repository: https://github.com/gnat/surreal
- Website: https://gnat.github.io/surreal/example.html
- Stars: 1,747 · Forks: 28
- Language: JavaScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/gnat-surreal

## What the whole repository actually contains

List the top level entries and the project has five top-level items: `.github/`, `LICENSE`, `README.md`, `example.html` and `surreal.js`.

That is the entire library. There is no `package.json`, no build configuration, no test directory, no transpiled output. The README states it plainly twice: Surreal is only 320 lines, with no build step and no dependencies.

Installation follows from that. You download `surreal.js` into your project and add a script tag in your head, or load it from a CDN:

```html
<script src="https://cdn.jsdelivr.net/gh/gnat/surreal@main/surreal.js"></script>
```

The top-level contents do not include an npm package manifest. The documented installation method is to download the single file or load it from a CDN.

The description calls it a mini jQuery alternative with dependency-free animations, Locality of Behavior, transparent handling of one element or arrays, pairing with htmx, and vanilla querySelector but better. That last clause is the honest summary: it is querySelector with a friendlier return value and chainable methods.

The topics confirm the scope. Alongside jquery, jquery-alternative and jquery-like there are dom, dom-manipulation, animation, timeline, hyperscript, cash, umbrella and locality-of-behavior. Naming it after two other tiny libraries is a claim about the intended ceiling.

## me() and any() exist to solve one jQuery ambiguity

The README is unusually clear about the problem it targets. The stated motivation includes hating typing `document.querySelector` over and over, hating typing `addEventListener` over and over, wishing `document.querySelectorAll` had Array functions, and wishing `this` would work in any inline script tag.

The design response is a pair of selectors with different return contracts:

- `me()` is guaranteed to return 1 element, or the first found, or null.
- `any()` is guaranteed to return an array, or an empty array.

That is the whole argument against `$()`, which the README frames as the classic jQuery bloat problem of not knowing whether you got one element or an array. The payoff is stated as no more checks, less code, and code that reads more like self-documenting English.

The Locality of Behavior part is what `me()` does when called with no arguments. It gets the parent element of the `<script>` tag, so no class or id is needed to reach an element from a script that sits inside it. The README's rationale for `me()` is that it gives you `this` but much more flexible.

The selector arguments are more permissive than querySelector. `me()` accepts a CSS selector such as `".button"`, `"#header"` or `"body > .block"`, a variable such as `body`, `e` or `some_element`, an event where `event.currentTarget` is used, or another Surreal selector. Parameter 2 chooses a start location in the DOM, defaulting to `document`, which is how you scope a search:

```js
any('button', me('#header')).classAdd('red')
```

Conversion between the two is direct, with `any(me())` for one into many and `me(any(".something"))` for many into one.

## Every function works on one element or an array

The second structural choice is that all DOM functions accept single elements and arrays interchangeably. The README states that every function can take `me()`, `any()`, a NodeList or an HTMLElement, or arrays of those.

That is why the jQuery selector syntax familiarity in the motivation list is not nostalgia but familiarity. You write `any('.button').classAdd('red')` and `me('.button').classAdd('red')` with the same method name, and you never branch on how many matches there were.

Two calling styles are documented and one is recommended. Style A is `me().classAdd('red')`, the chain form, marked as recommended. Style B is `classAdd(me(), 'red')`, the function form. Then there is the global convenience question: `globalsAdd()` will automatically warn you of clobbering issues, and if you do not want global conveniences you delete that one call, after which you can use `surreal.me().classAdd('red')` or `surreal.classAdd(surreal.me(), 'red')`.

So the whole namespace decision rests on one line in the source. That is both the appeal and the risk, because those globals sit in the same global scope as everything else on the page, and `me` is short enough to collide.

Naming is deliberately flexible too, and the README is direct that there is no forced style: `classAdd`, `class_add`, `addClass` and `add_class` all work. Use `camelCase` for JavaScript or `snake_case` if you prefer the convention from Python, Rust, PHP, Ruby, SQL or CSS.

The quick start section gives the shape of the whole API in a handful of lines: class manipulation with `classAdd` on `me()` or `any()`, events with `.on("click", ev => ...)`, bulk iteration with `any('button').run(_ => { alert(_) })`, styles as a string or an object via `styles`, and attributes through `attribute('active', true)`.

## Animations run on CSS transitions and sleep

The dependency-free animation claim is illustrated by a timeline example that sets a CSS transition and awaits between style changes.

Everything is the browser. The easing and timing come from the CSS transition, and `sleep` is a promise wrapper around setTimeout. The quick start also shows `fadeOut` in an event handler.

Two details in that snippet are worth noticing, because they are the kind of thing that costs an afternoon. First, the comment `Save target because async will lose it`. The event `currentTarget` is only available while the handler is being invoked; capture the element before awaiting if you need it afterwards. This does not mean an async function loses its `this` binding. Second, the transition is set before the first change, which is what makes the browser animate the first step instead of jumping.

The result is that this is a sequencing helper, not a tweening library. There is no interpolation of values, no easing curve control, no timeline scrubbing, no keyframe DSL. If you need a number to animate from 0 to 100 with a custom curve, this is not the tool. If you need a few elements to change style in sequence with a one-second gap, the example can do so without another library.

The DOM manipulation example in the README makes the same point about scope. Inside a `<label>`, the script handles `dragover`, `dragleave` and `drop` with `me()` at every step and never names the uploader div, which is exactly the Locality of Behavior claim made concrete.

## Maintenance, alternatives, and the trade you are making

On activity, the repository is not archived and the last push was on 2026-05-27. Releases are 1.3.4d on 2025-07-21, 1.3.4c on 2025-07-20 and 1.3.4 on 2025-07-20, which shows a patch release iterated across two days and then stopped. The version numbers carry a trailing letter, so these are not standard semver tags and there is no changelog discipline implied by them.

The recommended CDN URL points at `@main`, so the file you load from jsDelivr changes whenever that branch moves. For a dependency you paste into a script tag, that is worth pinning to a commit hash.

There is also a companion project referenced, css-scope-inline, for using `me` in `<style>` tags as well as in scripts, which tells you the Locality of Behavior idea extends beyond JavaScript.

The alternatives are worth naming concretely, because they cover the same ground with different tradeoffs.

Cash, Umbrella and htmx are named in the repository topics. The README specifically presents htmx as a project that Surreal pairs well with, but does not document the features of the other named libraries.

The README positions Surreal as a convenience over vanilla `querySelector`, `querySelectorAll` and `addEventListener` usage. Its trade is fewer checks and chainable methods in exchange for a global convenience layer.

Licence is MIT, allowing the file to be read, modified and vendored under its notice and licence conditions.

## Conclusion

Adopt Surreal for a static page, a prototype, or an inline script block where you want jQuery ergonomics without a dependency and you are willing to let it install globals into the page. For a large application, account for the lack of a package.json and the limited scoping offered by deleting the globalsAdd() call. Verify first that the 320 line claim still matches the file you download, and pin a commit rather than the @main CDN URL, since the last push was on 2026-05-27 and the recommended script tag tracks the moving branch.

## FAQ

### How do I install Surreal?

Download surreal.js into your project and add <script src="/surreal.js"></script> in your head, or load it from a CDN with <script src="https://cdn.jsdelivr.net/gh/gnat/surreal@main/surreal.js"></script>. There is no npm package, no build step and no dependency.

### What is the difference between me() and any() in Surreal?

me() is guaranteed to return one element, or the first found, or null, while any() is guaranteed to return an array, or an empty array. The README presents this as the fix for the jQuery problem of not knowing whether a selector matched one element or many.

### How does me() with no arguments work?

It returns the parent element of the script tag it appears in, which is the Locality of Behavior feature. You can then reach a child element without giving it a class or an id.

### Does Surreal need a build step or any dependencies?

No. The README states Surreal is only 320 lines with no build step and no dependencies, and the repository contains just surreal.js, example.html, LICENSE and README.md.

### How do I avoid Surreal's global names colliding with my code?

Surreal installs global conveniences through a globalsAdd() call, which warns about clobbering. Delete that one call and use the namespaced forms surreal.me().classAdd('red') or surreal.classAdd(surreal.me(), 'red').

## Sources

- [gnat/surreal on GitHub](https://github.com/gnat/surreal)
- [License: MIT](https://github.com/gnat/surreal/blob/main/LICENSE)
- [Project website](https://gnat.github.io/surreal/example.html)
- [README](https://github.com/gnat/surreal/blob/main/README.md)
- [Releases](https://github.com/gnat/surreal/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gnat-surreal
