Caution
This is not even a beta. Breaking changes may occur without notice.
<script src="act.js"></script> <button act@click="alert: `Just act, don't react!`">Click me!</button>
act is a scripting language for HTML.
Table of Contents
What?
-
โคต๏ธ Add it to your HTML with just ascripttag: no compilation, no dependencies, no build process, no shenanigans -
๐ act code is put right into HTML elements using attributes or script tags, embracing locality of behaviour
-
๐ฏ All the code is parsed when the DOM is ready and then executed on standard DOM events
-
๐ Supports literal
{query selectors},#ids,.classes,<tags>,@attributes,*css-propertiesand dimensions (12px,1.2rem,400ms,100%, ...) -
โณ Handles synchronous and asynchronous code with ease
-
๐ค JavaScript classes and functions are available within act code
-
โญ๏ธ Aims to have a compact syntax
-
๐งฉ Easily extensible through a simple JavaScript API
-
๐ act focuses in ease of use, not performance
-
๐งโ๐ป Syntax highlighting for VSCode, Neovim, Zed, TextMate, Visual Studio and JetBrains IDEs
-
๐ซก Inspired by great libraries like _hyperscript, surreal and intercooler.js
ic-action. -
๐ด Simple integration with htmx v2 and v4, using the
act-htmx.jsscript -
๐ซจ It's unstable and highly experimental
Why?
JavaScript, oh JavaScript... Love it or hate it, it's right there.
As it gets more features, I feel that it has lost some focus.
One of JavaScript's basic use cases, DOM manipulation, still feels cumbersome and verbose.
act aims to address this by providing a more concise and expressive way to manage DOM interactions that is right there, in the elements themselves.
Let's look at this simple example:
<button id="hide-btn">When I'm clicked I will hide after 1 second.</button> ... <script> document.addEventListener('DOMContentLoaded', () => { document.getElementById('hide-btn').addEventListener('click', (e) => { setTimeout(() => { e.target.style.display = 'none'; }, 1000); }); }); </script>
Now we are used to it, we've seen or even write code like this for years. But... when you think about it, it's a lot of code and it's not like we want to do some very complex stuff: just hide the button after 1 second.
You could write that with an onclick attribute, but that is frowned upon:
<button onclick="setTimeout(() => { this.style.display = 'none'; }, 1000);">When I'm clicked I will hide after 1 second.</button>
It's like JavaScript is begging to be abstracted away.
<button id="hide-btn">When I'm clicked I will hide after 1 second.</button> ... <!-- A lot of HTML later... --> <script> $(document).ready(function() { $('#hide-btn').click(function(e) { setTimeout(function() { $('#hide-btn').hide(); }, 1000); }); }); </script>
Well... yeah... We've been there before. A little bit of this is fine I guess, but as the code grows (and it doesn't need to grow that much) it gets hard to locate, manage and maintain.
npx create-react-app my-app
Wait, wait... You really wanna go there?
Could there be another way?
grug much prefer put code on the thing that do the thing. now when grug look at the thing grug know the thing what the thing do, alwasy good relief!
-The Grug Brained Developer on Separation of Concerns
With act the behavior is expressed in this compact way within the button itself:
<button act@click="wait: 1s; hide!;">When I'm clicked I will hide after 1 second.</button>
Getting Started
Installation
Simply include the act.js file in your HTML. No build steps required. You can vendor it or use a CDN.
<script src="act.js"></script>
In a nutshell
<button act="fade: out 500ms; fade: in 500ms; repeat;" act@click="kill act" > I will stop fading in and out when you click me! </button> <div id="one" style="width: 100px; height: 100px; background-color: red;"> <script type="text/act"> transition: color to blue in 1s; transition: color to red in 1s; repeat; </script> </div> <div id="two" style="width: 100px; height: 100px; background-color: green;"> <script type="text/act"> transition: color to yellow in 1s; transition: color to green in 1s; repeat; </script> </div> <button act@click="#one toggle!; #two toggle!">Toggle divs</button> <button act@click="#one kill act; #two kill act">Stop animations</button> <h1>AI Dinner suggester 2000</h1> <ul id="dinner-list" act@click:target="confirm: `Remove {:inner_text}?` ? remove!"> <li>Pizza</li> <li>Pasta</li> <li>Salad</li> <li>Sushi</li> <li>Steak</li> </ul> <button class="button"> <script type="text/act" act@click> with new <li>, do $name: prompt: 'Name of the dinner item:'; not $name? stop; << $name; #dinner-list append: me; end; </script> Add a new dinner item </button> <button class="button"> <script type="text/act" act@click> $items: {#dinner-list li}; $decided: random: 0 ($items.length - 1); alert: `Today I suggest {$items[$decided].inner_text} for dinner!`; </script> Decide dinner! </button>
Note
Remember to follow a healthy diet, exercise regularly, and drink plenty of water.
Core Concepts
Events
Act code runs in response to events. You attach code to elements using the act@eventname attribute:
<button act@click="fade: out">Click to fade out</button>
Or use <script type="text/act" act@eventname> tags for longer code:
<button> <script type="text/act" act@click> fade: out; wait: 1s; fade: in; </script> Click me </button>
Common events: click, input, submit, mouseover, etc. You can also use custom events.
The special act event runs when the page loads (on DOMContentLoaded).
โ See detailed Events documentation for modifiers, aliases, and custom events.
Language
Act code is composed of Scopes, containing a list of Sentences. These sentences have Expressions that use Values.
Syntax Fundamentals
Sentences โ See Sentences
Act code is written in sentences. Each sentence typically performs one action:
Sentences end with a terminator (; by default). Multiple sentences are written one after another:
#box1 hide!;
wait: 1s;
#box2 show!;
#box3 fade: in 1s& // & terminator is used to run the sentence asynchronously
#box4 fade: out 1s;
Target and Operation โ See Sentence target
Most sentences follow this pattern: target operation arguments
- Target: What you want to act on (element, variable, etc.). If not specified, it defaults to the element where the act code is located.
- Operation: What you want to do (method, keyword, operator)
- Arguments: Data needed for the operation
#my-element hide!; // Target: #my-element, Operation: call hide
$name: 'John'; // Operation: assignment
alert: 'Hello!'; // Operation: call alert
Calling Methods โ See Call
Methods are called with ! and can take arguments using !::
#element hide!; // No arguments
#element fade!: out 500ms; // With arguments
$array push!: 'item'; // Method on a variable
Accessing Properties โ See Values
Properties use prefixes to indicate what type of property you're accessing:
#input :value; // DOM property (:)
#element *background-color; // CSS property (*)
#element @disabled; // HTML attribute (@)
The colon operator (:) is the Act operation - it automatically infers whether to set or call based on the type of value on the left:
// Set (when left is a property, attribute, CSS property, or variable)
#input :value: 'text'; // Property: sets value
#input value: 'text'; // Word that resolves to a property: sets value
#element *background: red; // CSS property: sets background
#button @disabled: true; // Attribute: sets disabled
$name: 'John'; // Variable: sets variable
// Call (when left is a word/method name)
fade: in 500ms; // Word: calls fade method
alert: 'Hello!'; // Word: calls alert function
Explicit set โ See Set
You can use the = operator to explicitly set a value, without inferring the operation:
#input :value = 'text'; // Property: sets value
#input value = 'text'; // Word that resolves to a property: sets value
#element *background = red; // CSS property: sets background
#button @disabled = true; // Attribute: sets disabled
$name = 'John'; // Variable: sets variable
Working with Variables โ See Data Scopes
Variables start with $:
Variables starting with a capital letter are global:
$Global_var: 'accessible everywhere';
$scoped_var: 'only in this scope';
You can also use the global, local and scoped prefixes to specify explicitly the variable scope:
global $global_var: 'accessible everywhere';
local $local_var: 'only in this element';
scoped $scoped_var: 'only in this scope';
Selecting Elements โ See Values
Elements can be selected using familiar CSS-like syntax:
#my-id // ID selector
.my-class // Class selector (returns collection)
<div> // Tag selector (returns collection)
{div > .item} // Selector template (query)
Keywords โ See Keywords
Use if, each, while, and other keywords for logic:
each $item in $list (
log: $item;
);
if $name is 'John' (
alert: 'Hello John!';
);
Scopes โ See Sentences
Group multiple sentences using parentheses () or do...end.
Scopes allow to create scoped variables, only accessible in the current scope and its children.
(
wait: 1s;
show!;
);
do
$x: 10;
log: $x;
(
log: `I will print 10: {$x}`;
);
end;
do
log: $x; // It will not print 10 as this is a different scope and $x is not defined.
end;
Operators โ See Operators
Common operators work as expected:
$sum: 1 + 2;
$product: 3 * 4;
$is_equal: $a is $b;
$is_greater: $x > 5;
String Templates โ See Values
Use backticks for string interpolation:
$name: 'World';
log: `Hello, {$name}!`; // Interpolate variables
log: `Sum: {1 + 2}`; // Interpolate expressions
Values
| Type | Description | Example |
|---|---|---|
| String | Text wrapped in single or double quotes. Supports escape characters like \n or \t. |
"Hello\nworld!", 'Hello\nworld!' |
| Word | An unquoted string containing only alphanumeric characters, underscores, and dashes. Some words are reserved. | aliceblue, red, hello-world |
| Number | Integers or decimal numbers. | 123, 1.23, -12.3 |
| Boolean | True or false values. | true, false |
| Variable | A word prefixed by $. Used to store and retrieve values. |
$some_variable, $someVariable |
| CSS Property | A CSS property of the target element. Prefixed by *. |
*background-color, *opacity |
| Dimension | A number with a unit (px, rem, em, s, ms, %, etc.). | 123px, 1.23rem, 3s, 50% |
| Attribute | An HTML element attribute. Prefixed by @. |
@disabled, @data-value |
| Property | A property of the target. Prefixed by :. |
:innerText, :value |
| Null | Represents no value. | null |
| DOM REFERENCES | ||
| Id | An HTML element ID. Prefixed by #. Returns the element with that ID. |
#some-id, #header |
| Class | An HTML element class. Prefixed by .. Returns all elements with that class. Append ! to select only the first matching element directly. |
.some-class, .button, .button! |
| Tag | An HTML tag. Wrapped in <>. Returns all elements with that tag. Append ! to select only the first matching element directly. |
<div>, <button>, <div>! |
| COLLECTIONS | ||
| Array | A list of values separated by spaces or commas, wrapped in []. |
[1 2 3], ["a", "b", "c"] |
| Object | Key-value pairs separated by colons, wrapped in []. Keys and values can be separated by spaces or commas. Use [:] as an empty object. |
[key1: value1 key2: value2], [name: 'John', age: 30] |
| TEMPLATES | ||
| String Template | A string with interpolations wrapped in backticks. Interpolations use {} and can contain full sentences. |
`Hello {$name}!`, `Result: {2 + 3}` |
| Selector Template | A CSS query selector wrapped in {}. Returns matching elements. Supports interpolations and prefixes. Append ! to select only the first matching element directly. |
{#some-div}, {p > .class}, {#{$id}}, {> a.active}! |
| EVALUABLES | ||
| Scope | A collection of sentences wrapped in () or do ... end. Can hold values, expressions, or multiple sentences. |
(2 + 3), do wait: 1s; log: 'Done' end |
| Function | An anonymous function defined with ->. Can have arguments. Returns the value of the last sentence or using return. |
->( log: 'Hi' ), -> $x $y ($x + $y) |
| COMMENTS | Yes, comments are not values but I didn't know where to put them | |
| Single line comments | Single line comments start with // and continue until the end of the line. |
// This is a single line comment |
| Multi line comments | Multi line comments start with /* and end with */. They can span multiple lines. |
/* This is a multi line commentThat spans multiple lines */ |
| VALUES WE WISH DIDN'T EXIST | ||
| undefined | ๐คท | undefined |
| Not a number | 'a' * 1 |
NaN |
Note
Automatic Case Conversion & JavaScript Method Access
When accessing properties or calling methods, act automatically converts snake_case to camelCase.
This means you can write :inner_text to access innerText, or query_selector_all to call querySelectorAll.
You can call any native JavaScript method on values using this conversion. For example:
$number.to_string: 16calls the nativetoString(16)method#my-element focus!calls the nativefocus()method$string.to_upper_case!calls the nativetoUpperCase()method
This makes act seamlessly integrate with the entire JavaScript API using the superior snake_case.
Selector Template Prefixes
Selector templates are not just a convenient way to call document.querySelectorAll under the hood, they have prefixes that alter the query behavior.
| Prefix | Description | Example |
|---|---|---|
< ... |
Returns the closest element that matches the CSS query | {< div.button}, {< p} |
> ... |
Returns the elements that match the CSS query inside the current element. Can be used with selectors. | {> p > span.highlighted}, {> a} |
Tip
Append ! after a selector template (or any DOM Reference like .class or <tag>) to select the first matching element directly, instead of returning a NodeList. This is equivalent to using querySelector instead of querySelectorAll.
{> a.active}! // first matching <a class="active"> inside the current element
.button! // first element with class 'button'
<li>! // first <li> element on the page
Data Scopes
Variables in act have three possible scopes: global, local (element), and scoped.
Global Scope
Variables in the global scope are variables that can be accessed from all places within the page.
They are declared with the $ prefix and an initial capital letter.
You can also access and declare global variables using the global prefix.
<div id="user-profile"> <input type="text" name="user-name" act@input="$Name: :value"> <!-- ... --> </div> <!-- ... --> <!-- Somewhere else on the page --> <div id="some-other-div"> <button id="some-other-action"> <script type="text/act" act@click> not $Name? alert: 'Please complete your profile first!' ~ do $Name is 'John'? alert: 'Sorry but John is not allowed to do that.' ~ alert: 'OK!'; end; </script> Do some stuff! </button> </div>
Note
Global variables are stored in the body element. The variable lookup code goes directly to the body element when the variable name starts with a capital letter or the global prefix is used.
Local (Element) Scope
Variables in the local scope are variables that can be accessed only within the element where they are declared and its children.
They are declared using the local prefix, and the name of the variable cannot start with a capital letter.
<div id="some-div" act="local $some_var: 1; $another_var: 1;"><!-- Both variables are 1 --> <button id="some-button" act@click="alert: `$some_var is {$some_var}, and $another_var is {$another_var}`"> Click me! </button> </div> <div act="local $foo: 1"> <!-- Here $foo is 1, $bar is undefined, $baz is undefined --> <div act="local $bar: 2"> <!-- Here $foo is 1, $bar is 2, $baz is undefined --> <div act="local $baz: 3"> <!-- Here $foo is 1, $bar is 2, $baz is 3 --> </div> </div> </div>
Scoped Variables
Scoped variables are variables that can be accessed only within the Scope where they are declared and its nested scopes.
They are declared using the scoped prefix, and the name of the variable cannot start with a capital letter.
Variables that are not declared with any prefix are considered scoped variables.
<section id="some-section" act="local $section_var: 3"> <div id="some-div" act="scoped $some_var: 1; local $another_var: 1; log: $section_var;"> <button id="some-button"> <script type="text/act" act@click> // Here $some_var is undefined, not defined in the same Scope. // $another_var is 1, as it is declared at the local scope. alert: `$some_var is {$some_var}, and $another_var is {$another_var}`; scoped $foo: 10; do log: $foo; // $foo looked up. Logs 10. scoped $foo: 20; // $foo defined as a scoped variable in THIS Scope. log: $foo; // Logs 20. log: $section_var; // Logs 3. $section_var is available as a local variable of a parent element. end; log: $foo; // Logs 10. </script> Click me! </button> </div> </section>
act Blocks
act Blocks are a way to write reusable code that can be invoked within the element where the block is defined or any of its children.
Blocks can be defined in two ways:
- Using a
<script type="text/act">tag with anact-blockattribute (value:block_name $arg1 $arg2 ...) - Using the
defkeyword inline within act code
When the run keyword is used, the block will be executed in the current sentence target, which is the target specified or inherited in the run sentence and not the element where the block is defined. This way the block can be easily reused with different targets.
By default, run looks for the block by walking up the ancestor chain of the source element, then the sentence target. To reach a block that lives in a different part of the DOM (a sibling subtree, a shared component, and so on), use from followed by a selector pointing to the element where the block is defined (or any of its ancestors):
run my_block from #component arg1 arg2;
Block Arguments
Arguments are defined in the act-block attribute after the block name, using variable syntax (prefixed with $). These become named parameters that you pass when calling run.
<div id="user-profile"> <script type="text/act" act-block="check_field $border_color"> if value.length < 2 or :value is 'John' ( *border: 5px solid $border_color; @invalid: true; first {> .field-info} << 'Please enter a valid name'; ) else ( *border: 1px solid green; remove_attribute: invalid; first {> .field-info} << empty!; ); </script> <input type="text" act@input="debounce: 250ms run check_field firebrick;"> <p class="field-info"></p> <input type="text" act@input="debounce: 250ms run check_field orangered;"> <p class="field-info"></p> </div>
Arguments are optional - if an argument is not provided when calling run, the variable will be undefined within the block.
Multiple Arguments Example:
<script type="text/act" act-block="greet $name $title"> alert: `Hello {$title} {$name}!`; </script> <button act@click="run greet 'Smith' 'Dr.'">Greet Dr. Smith</button> <button act@click="run greet 'Johnson' 'Prof.'">Greet Prof. Johnson</button>
Warning
While act blocks are useful, avoid overusing them or you'll end up searching for where things are defined and what they do, which can get messy fast. Don't jQueryit.
Sentences
Values and expressions are placed in sentences, each one may contain an optional target at the beginning and an execution mode at the end that also indicates the end of the sentence.
graph TD
S(Sentence)
T(Target)
E(Expression)
M(Mode)
S --- T
S --- E
S --- M
T --- TV["#some-div (id)"]
L(Left)
O(Operator)
R(Right)
E --- L
E --- O
E --- R
L --- LV["fade (word)"]
O --- OV[": (colon)"]
R --- RV1["out (word)"]
R --- RV2["1s (dimension)"]
M --- MV["; (sync)"]
style TV fill:#fff,stroke:#333,stroke-width:2px,color:#000
style LV fill:#fff,stroke:#333,stroke-width:2px,color:#000
style OV fill:#fff,stroke:#333,stroke-width:2px,color:#000
style RV1 fill:#fff,stroke:#333,stroke-width:2px,color:#000
style RV2 fill:#fff,stroke:#333,stroke-width:2px,color:#000
style MV fill:#fff,stroke:#333,stroke-width:2px,color:#000
Sentence target
The target of a sentence is the value where the expression of the sentence will be applied, the target of the action. This can be an element or any other value. If a target is not specified, the sentence will look up for a target from its ancestors. If no target is found, it will use the element where the code is located (or the target of the event if the target modifier is used on the event).
The use of targets is useful to target other elements using IDs, classes, tags and Query templates:
<div id="just-a-squared-div" class="square" style="width: 100px; height: 100px; background-color: red;"></div> <div id="another-squared-div" class="square" style="width: 100px; height: 100px; background-color: green;"></div> <button class="toggable" act@click="#just-a-squared-div *background-color: red"> Make the first square background red </button> <button class="toggable" act@click="<div>[0] *background-color: green"> Make the first square background green </button> <button class="toggable" act@click="first {.square} *background-color: blue"> Make the first square background blue </button> <button class="toggable" act@click="<div> each (*background-color: `rgb({random: 0 255}, {random: 0 255}, {random: 0 255})`)"> Random background for both divs! </button> <button act@click=".toggable each toggle!"> Toggle the buttons </button>
Sentence mode
The mode of a sentence is defined by the last token of the sentence, which acts as the sentence terminator. These are the tokens that can be used to finish a sentence and define its mode:
| Token | Mode | Description |
|---|---|---|
; |
Synchronous | The standard mode terminator. The sentence executes synchronously - the next sentence waits for this one to complete. |
& |
Asynchronous | The sentence executes asynchronously - the next sentence starts immediately without waiting for this one to complete. |
>> |
Forward | Forwards the sentence result as the target of the next sentence (piping). |
? |
Condition | Conditional sentence. If the result is truthy, the next sentence executes; if falsy, it's skipped. |
~ |
Branch | Conditional branch. Executes if the previous condition was false, skips if it was true. |
Examples:
// Synchronous execution (;) - waits for each step
wait: 0.5s;
log: "I'll be logged after 0.5 seconds.";
#some-element transition: background-color to red in 2s;
log: "I'll be logged after the transition ends (2 seconds).";
// Asynchronous execution (&) - doesn't wait
#some-element transition: background-color to green in 3.5s &
log: "I'll be logged immediately. The transition runs in the background.";
// Conditional execution (?)
$a_variable: true;
$a_variable? log: "I'll be logged because $a_variable is true.";
// Conditional with else
$a_variable?
log: "I'll be logged because $a_variable is true."
~
log: "This sentence will be skipped.";
Language Reference
Expressions
Expressions perform an operation involving a left operand, and zero or more values as right operands (arguments) separated by spaces.
left (operation token) right1 right2 right3
If needed, an expression can be terminated by a comma ,. For instance if you want to use expressions as arguments, you can do it like this:
log: has_attribute: data-someattr, 2 + 3
This will log into the console
trueand5as the comma acts as a terminator for thehas_attribute: data-someattrexpression, and5is treated as the next argument of log and not the next argument of has_attribute.If that is too much for you, wrapping arguments into Scopes is a good alternative:
log: (has_attribute: data-someattr) 2 + 3Or if the expression are calls you can also use the call with parentheses syntax:
log: has_attribute(data-someattr) 2 + 3
log(has_attribute(data-someattr), 2 + 3)
Call
left! or left!: right
It also accepts parentheses syntax:
left() or left(right, right, ...).
The call operation calls the left operand, if callable, or looks up for a function named after the left operand and calls it.
- Looks up a method from the act Library that matches the value as name and the target type.
#some-element empty!
- Call the method with the value as name in the target object.
#some-element remove!;
- Call the method with the value as name in the
windowglobal object.alert!: 'Hello world!';
It can be also used to call an anonymous function.
// Declaring a simple function to use later:
$greet: -> $name (
alert: `Hello {$name}!`;
);
#some-div hide!;
// Calls the hide method for an Element target type from the act Library.
alert: 'Hello world!';
// No method 'alert' found on the Library or the target, falling back to window and calling window.alert.
alert('Hello world!');
// This also works but it's less fun.
$greet: 'Foo';
// Calls the anonymous function with the argument 'Foo'.
$greet('Foo'); // Calls the anonymous function with the argument 'Foo' again, using parentheses syntax.
Set
left = right.
The set operation sets the right operand to the left operand. If the left operand is or evaluates to an Element it will set its inner_html property. If the left operand is a CSS Property it will set all the right operands.
#some-div *border = '1px solid red';
// Sets the CSS property border of #some-div to 1px solid red.
$a = 1;
// Declares or sets the variable $a to 1.
$b[3] = 'foo';
$c.property = 'bar';
// It also sets members of arrays and properties of objects.
<button>[4] @disabled = true;
// Sets the disabled attribute of the 5th button to true.
#long-text inner_html = 'tl;dr';
// Sets the inner_html property of #long-text to 'tl;dr'.
#short-text (inner + HTML) = 'wall of text here';
// Sets the inner_html property of #short-text. The value is a scope that evaluates to the concatenated words 'inner' and 'HTML', so you can effectively use any value as long as it evaluates to a string.
#medium-text `inner{'html'.to_upper_case!}` = 'meh';
// Sets the inner_html property of #medium-text to 'meh'. Again, the value in this case is not a word but a string template.
Act
left: right
๐ญ The act operation is just sweet sweet syntactic sugar ๐ฌ. Depending on the type of the left operand it will either use a set or a call operation.
*border: 1px solid red;
// CSS property as left operand: uses a set operation and sets the border property to 1px solid red.
// With CSS properties the right operands list is joined with a space.
$a: 1;
// Variable as left operand: uses a set operation and sets the variable $a to 1.
@disabled: true;
// Attribute as left operand: uses a set operation and sets the disabled attribute to true.
:property: 5;
// Property as left operand: uses a set operation and sets the property to 5.
fade: in 100ms;
// Word as left operand: uses the call operation and calls the fade method of the act Library with the right operands 'in' and '100ms'.
inner_html: 'bah';
// Word as left operand: uses the call operation, as no method is found sets the inner_html property to 'bah'.
At
left.right.
The at operation is used to access keys or properties of the left operand.
$fruits: [Orange Apple Banana];
$human: [
name: 'John'
say_hello: ->(
log: `My name is {$this.name}!`;
)
];
$human.say_hello!;
// Logs 'My name is John!'.
$human.name: 'Johnny'; // Or $human[name] = 'Johnny';
$human.say_hello!;
// Logs 'My name is Johnny!'.
$fruits[0]: 'Apricot';
$fruits.push: 'Peach';
log: $fruits[4].to_upper_case!;
// It calls the toUpperCase method of the string 'Peach' and logs 'PEACH'.
Safe At
left?.right
Like the at operation, but returns undefined instead of throwing if the left operand is null or undefined. The safe flag propagates through the rest of the chain, so a?.b.c returns undefined if a is null rather than throwing on .c.
Mixed chains work naturally: a.b?.c.d creates two separate access expressions: .b is unsafe (throws if a is null) and ?.c.d is safe (returns undefined if a.b is null).
$user: null;
$user?.name; // undefined, no throw
$data: [meta: null];
$data.meta?.title; // undefined: .meta is safe, .title propagates
Subscript
left[right]
The subscript operation looks up for a property or index named after the right operand (index) and returns it.
The index can be a value or an expression.
Unlike the at operation, the subscript operation only accesses the strict result of the right operand without performing any lookup or case conversion.
Note
One more expression, insert (<<), appends content into a single element or pushes into an array. Because it reads like an operator, it's documented in Operators (#div << 'Content').
Operators
left operator right
Important
Strict Operator Spacing Operators MUST be surrounded by spaces.
- โ
1 + 1 - โ
1+1(This will be parsed as a text string or invalid token, not an addition)
This rule applies to all operators (+, -, =, ==, etc.) to ensure clear and unambiguous parsing as act allows the use of kebab-case keywords.
Caution
Operators have no precedence and are solved left to right.
So 1 - 2 + 3 * 4 + 5 will result in 13 instead of 16.
- 1 - 2 =
-1 - -1 + 3 =
2 - 2 * 4 =
8 - 8 + 5 =
13
Please use parentheses to avoid this:
1 - 2 + (3 * 4) + 5 will be evaluated as 16.
Why? Because as this side project has grown out of control (and sanity), the parser was not meant for the stuff I was shoving into it, and I'm just too lazy to implement it.
Will the laziness ever be defeated? idk.
| Operator | Description | Example |
|---|---|---|
| ARITHMETIC | ||
+ |
Addition and string concatenation | 1 + 2, 'Hello ' + 'world' |
- |
Subtraction | 1 - 2 |
* |
Multiplication | 1 * 2 |
/ |
Division | 1 / 2 |
% |
Modulo (division remainder) | 10 % 3 |
| LOGIC | ||
and |
And | $foo and $bar |
or |
Or | $isLoggedIn or #username-field.has_attribute: disabled |
?? |
Nullish coalescing. Returns the left operand if it is not null or undefined, otherwise the right. Unlike or, falsy values like 0 or '' pass through. |
$user?.name ?? 'Anonymous' |
not |
Logical negation. This is a prefix, not a binary operator, so it takes only a right operand. | not $isLoggedIn |
| COMPARISON | ||
is |
Equal (JavaScript ===) |
1 is 2 |
is_not |
Not equal (JavaScript !==) |
1 is_not 2 |
== |
Like (JavaScript ==) |
'1' == 1 // is true |
!= |
Not like (JavaScript !=) |
'1' != 1 // is false |
> |
Greater than | 1 > 2 |
>= |
Greater than or equal | 1 >= 2 |
< |
Less than | 1 < 2 |
<= |
Less than or equal | 1 <= 2 |
| ASSIGNMENT | ||
= |
Set the left operand to the right operand | Check the Set operation section |
+= |
Add and set. Works with numbers and strings. | $a += 1, $str += ' more' |
-= |
Subtract and set. | $a -= 1 |
| OTHER | ||
as |
Cast the left operand to the type specified in the right operand. Documented in the next section (here). | |
rescue |
If an exception is thrown on the left operand evaluation, the right operand expression or scope will be evaluated. Documented here. | |
is_a or is_an |
Checks if the left operand is an instance of the specified type. | 1 is_a number, $some_array is_an array, #some-element is_an element |
then or | |
Uses the result of the left expression as the target for the right expression. | 'foo' as id then :value, 'foo' as id | :value |
is_in |
Checks if the left operand is in the right operand (collection or string). | 'a' is_in 'abc', 1 is_in [1 2 3] |
is_not_in |
Checks if the left operand is not in the right operand. | 'd' is_not_in 'abc' |
<< |
Inserts the right operand into the left operand. If left is a single element, sets innerHTML. If the right operand is a <template> element, it inserts the template's innerHTML. If left is an array, pushes the value. Collections are not accepted, so select one element with ! or first. |
#div << 'Content', #div << #my-template, .item! << 'Content', $arr << 1 |
Note
All arithmetic and comparison operators can be used with dimensions if both operands are dimensions with the same dimension unit.
100px + 2px // valid, returns 102px
100px > 2rem // not valid, returns null
as operator
left as right
The as operator is used to cast the left operand to the type specified in the right operand.
It accepts the following types:
| Type | Description | Example |
|---|---|---|
string |
Casts the left operand to a string. | 9 as string |
number |
Casts the left operand to a number, if it cannot be cast to a number it returns 0. | '1.2' as number, 'not a number' as number // returns 0 |
float |
Casts the left operand to a float. | '1.2' as float |
int |
Casts the left operand to an integer. | '23' as int |
integer |
Alias for int. |
'23' as integer |
boolean |
Casts the left operand to a boolean. | 'false' as boolean |
id |
Casts the left operand to an id. | '#some-id' as id or 'some-id' as id |
class |
Casts the left operand to a class. | '.some-class' as class or 'some-class' as class |
attribute |
Casts the left operand to an attribute of the target. | '@some-attribute' as attribute or 'some-attribute' as attribute |
css_property |
Casts the left operand to a CSS property of the target. | '*some-property' as css_property or 'some-property' as css_property |
dimension |
Casts the left operand to a dimension. | '1.2rem' as dimension |
variable |
Casts the left operand to a variable name. | 'foo' + 'bar' as variable |
json |
Serializes the left operand to a JSON string using JSON.stringify. |
$obj as json |
fragment |
Parses the left operand as HTML and returns a DocumentFragment. |
'<p>Hello</p>' as fragment |
selector |
Casts the left operand to a selector template. | '.my-class' as selector |
rescue operator
left rescue right
If an exception is thrown on the left operand evaluation, the right operand expression or scope will be evaluated.
If the right operand is a scope, the thrown exception of the left operand will be injected into the scope as the $exception variable.
(#nonexistent-id *color: red) rescue (log: 'Unable to set the color of #nonexistent-id to red, the exception was:' $exception);
This can, of course, also be written in multiple lines:
(
#nonexistent-id *color: red;
) rescue (
log: 'Unable to set the color of #nonexistent-id to red, the exception was:' $exception;
);
Error Handling Patterns
The rescue operator provides similar capabilities to JavaScript's try...catch blocks. Here are common patterns:
Basic Error Recovery:
// Try to fetch data, fallback to default
$data: (fetch: '/api/data' then json!) rescue ['name': 'Default'];
Accessing the Error:
// The $exception variable contains the caught exception
#risky-operation do
// Some operation that might fail
throw 'Something went wrong!';
end rescue (
error: 'Caught exception:' $exception;
// $exception contains the error message or Error object
);
Chaining with throw:
// Validate and throw custom errors
$age: prompt: 'Enter your age:';
($age as number) < 18? throw: 'Must be 18 or older';
// Handle validation errors
(
run validate_form;
) rescue (
#error-message << `Error: {$exception}`;
#error-message show!;
);
Prefixes
Prefixes are special words that modify the value or expression that follows them.
not
Negate the value that follows.
local
Looks up or declares a variable at the local (element) scope.
Note
Scopes are explained in the Data Scopes section.
local $a;
// Declares a variable $a with a null value.
local $b: 2;
// Declares a variable $b with a value of 2.
local $c: 2 + $b;
// Declares a variable $c with the result of the expression: a value of 4.
do
log: $a;
// $a is found in the local scope, logs null.
$a: 10;
// Sets variable $a to 10.
log: $a;
// Logs 10.
end;
log: $a;
// Logs 10.
global
Looks up or declares a variable at the global scope.
Note
Scopes are explained in the Data Scopes section.
global $a: 1;
// Declares a global variable $a with a value of 1.
scoped
Looks up or declares a variable at the scoped scope.
Note
Scopes are explained in the Data Scopes section.
scoped $a: 1;
do
log: $a;
// Looks up for the $a variable; $a is found in the parent Scope, logs 1.
scoped $a;
// Declares a variable $a with a null value in this Scope.
scoped $b: 2;
// Declares a variable $b with a value of 2 in this Scope.
$a: 10;
// Sets scoped variable $a to 10.
log: $a;
// Finds $a variable in this Scope, logs 10.
end;
log: $a;
// Logs 1.
negative
Prefix that negates a number (unary minus operator). The shorthand - can be used directly before any value or expression.
Examples:
negative 5; // Returns -5
-5; // Same
$a: 10;
negative $a; // Returns -10
-$a; // Same
-(2 + 4); // Returns -6
type
Returns the type name of a value as a string.
Examples:
type 5;
// Returns 'number'
type #some-div;
// Returns 'element'
type [1, 2, 3];
// Returns 'array'
first
Returns the first item in a collection.
Examples:
first .item *color: red;
// Selects the first element with class 'item' and sets its color to red.
$my_array: [1 2 3];
first $my_array; // Returns 1
last
Returns the last item in a collection.
Examples:
last .item *color: blue;
// Selects the last element with class 'item' and sets its color to blue.
$my_array: [1 2 3];
last $my_array; // Returns 3
wat
Debug helper that logs detailed information about a value to the console, including its type, value, and origin. The value will be returned.
Examples:
wat $some_variable;
// Logs detailed debug info about $some_variable
... (Spread)
The spread prefix expands arrays into individual values. This is useful for:
- Passing array elements as separate arguments to functions
- Merging arrays together
- Creating new arrays with additional elements
// Store an array
$numbers: [1 2 3];
// Spread as function arguments
log: ...$numbers; // Logs 1, 2, 3 as separate arguments
// Spread into a new array
$more: [...$numbers 4 5 6]; // Creates [1, 2, 3, 4, 5, 6]
// Combine arrays
$a: [1 2];
$b: [3 4];
$combined: [...$a ...$b]; // Creates [1, 2, 3, 4]
Keywords
if
if condition (
// This Scope will be evaluated if the condition is true.
) else if other_condition (
// This Scope will be evaluated if the first condition is false and this condition is true.
) else (
// This Scope will be evaluated if all preceding conditions are false.
);
#some-element if :inner_html.includes: 'foo', (
alert: 'The element has the word "foo" in its innerHTML';
) else if :inner_html.includes: 'bar', (
alert: 'The element has the word "bar" in its innerHTML';
) else (
alert: 'The element does not have the word "foo" or "bar" in its innerHTML';
);
Note
A note on act's simple but expressive syntax.
if, like the other keywords, is just one big expression with multiple Words and Scopes
as lazy evaluated arguments.
act syntax is very simple, but it also allows for some expressive code.
With that said, let's get back to our scheduled programming...
each
Iterate over a collection.
Arguments:
- With an iterable target:
(body: Expression or Scope) - With an iterable argument:
(key: variable) ('in' Word) (iterable value) (body: Scope) - With an Object:
(key: variable) (value: variable) ('in' Word) (Object) (body: Scope)
<p> each (
// The target inside this Scope will be set to each paragraph.
<< inner_html.to_upper_case!;
)
each $item in [1 2 3 4] (
log: $item;
$item is 3? log: 'Three is my favorite number!';
);
// Some object
$act_todo: [
'Lexer': 'Slow but working'
'Parser': 'Done'
'Literal Values': 'Done'
'Scope evaluation': 'Done'
'Fix bugs': false
];
each $feature $status in $act_todo (
if not $status (
warn: `The feature {$feature} is not done yet.`;
) else if $status is_not 'Done' (
log: `The feature {$feature} is done but may have some rough edges: {$status}.`;
) else (
log: `The feature {$feature} is done.`;
);
);
for
A BASIC style for loop.
Arguments:
(variable) [ ('from' Word) (start value) ] ('to' Word) (end value) [ ('step' Word) (step value) ] (body: Scope)
for $i to 10 ( // Starts at 0, ends at 10.
log: $i;
);
for $i from 1 to 10 ( // Specifies a starting value.
log: $i;
);
for $i from 1 to 10 step 2 ( // Specifies a step value that increments the variable on each iteration.
log: $i;
);
for $i from 0 to <p>.length step 2 (
// I will change the color of every odd <p> element.
<p>[$i].*color: red;
);
while
A while loop.
Arguments:
(condition: value) (body: Scope)
$i: 0;
while $i < 10 (
log: `The current index is {$i}.`;
$i += 1;
);
If the condition is initially
false, the body is never executed.
loop
Executes a scope in an infinite loop. Use break to exit the loop.
Arguments:
(Scope)
loop (
warn: 'This loop is running forever!';
);
// Loop with a break condition:
$count: 0;
loop (
$count += 1;
log: `Count: {$count}`;
$count >= 5? break;
);
<div id="looper" act="loop (wait: 1s; log: 'Running...')"> <button act@click="#looper kill act">Stop the loop</button> </div>
def
Defines a named block of code on the target.
Arguments:
(name: Word) [arguments...] (body: Scope)
def hello do
log: 'Hello, world!';
end;
def greet $name (
log: `Hello, {$name}!`;
);
run greet 'World';
// Variadic arguments (rest parameter)
def sum_all ...$numbers (
$sum: 0;
each $n in $numbers ( $sum += $n );
log: $sum;
);
run sum_all 1 2 3 4;
run
Run a named block of code defined with the act-block attribute or the def keyword in the target.
<script type="text/act" act-block="my_block"> log: 'Block executed'; </script> <button act@click="run my_block">Run Block</button>
Blocks can accept named arguments:
<script type="text/act" act-block="greet $name $greeting"> log: `{$greeting}, {$name}!`; </script> <button act@click="run greet 'John' 'Hello'">Greet John</button>
By default, the block is looked up by walking up the ancestor chain of the source element and then the sentence target. Use from to reach a block defined elsewhere in the DOM:
<!-- Block defined in a sidebar, unrelated to the button --> <aside id="shared-blocks"> <script type="text/act" act-block="validate $value"> log: `Validating: {$value}`; </script> </aside> <form> <button act@click="run validate from #shared-blocks 'hello'">Validate</button> </form>
The from argument goes right after the block name and tells run to start the block lookup at that element (walking up its ancestors as usual), instead of walking up from the sentence target. Any arguments after the from element are passed to the block as normal.
โ See act Blocks for detailed documentation on defining and using blocks with arguments.
with
Sets the target of the body scope.
Arguments:
(target: value) (body: Scope)
with #some-element (
fade: out;
// Fades out #some-element.
#another-element hide!;
// Hides #another-element, as the explicit target of this sentence is #another-element and it will not fallback to the target of the "with" operation.
);
// It also works with other values that are not elements:
$some_array: [1 2 3];
with $some_array (
push: 4;
// Pushes value 4 to the end of $some_array.
);
new
Creates a new HTML element or a new instance of a JavaScript class/prototype/or whatever that thing works.
Arguments:
(Tag or Word) [args...]
$a_div: new <div>;
// Creates a new div HTML element.
$date: new Date;
// Creates a new JavaScript Date object.
$custom_date: new Date '2023-12-25';
// Creates a new Date object with arguments.
on
Adds an event listener to the target element.
Warning
Be careful when adding event listeners. Act only stores one event listener per event name per element.
So while adding multiple event listeners that have the same name to an element works, removing an event listener with off will only remove the last one.
Arguments:
(event name: Word) (body: Scope): basic event binding(event name: Word) (options: Object) (body: Scope): with an options object(event name: Word) [...options: Word] (body: Scope): with single-word options(event name: Word) [...options: Word] matching (selector) (body: Scope): event delegation([alias name: event name]: Object) [...options: Word] [matching (selector)] (body: Scope): aliased event
Single-word options: Any word before the scope becomes options[word] = true. Common options:
target: handler receivesevent.targetasmeinstead of the bound elementonce: handler fires only oncecapture,passive: passed through toaddEventListenerprevent: callsevent.preventDefault()stop: callsevent.stopPropagation()only: callsevent.stopImmediatePropagation()
Examples:
// Basic event
#some-element on click (
log: 'Clicked!';
);
// With single-word options
#some-element on click target once (
log: 'Clicked on:' me;
);
// Event delegation with matching
#some-list on click matching {.item} (
log: 'Item clicked!';
*color: red;
);
// Aliased event
#some-element on [myClick: click] (
log: 'Aliased click!';
);
// Combined: alias + options + matching
#container on [itemClick: click] target matching {button.active} (
log: 'Active button clicked:' :inner_text;
);
// With options object
#some-element on scroll [passive: true] (
log: 'Scrolling...';
);
// With modifiers - only trigger on Ctrl+Click
#some-element on click [modifiers: ['ctrl']] (
log: 'Ctrl+Click detected!';
);
// Combined: modifiers with single-word options
#some-element on click target [modifiers: ['ctrl' 'shift']] (
log: 'Ctrl+Shift+Click on:' :tag_name;
);
off
Removes an event listener from the target element.
Arguments:
(event name or alias: Word)
#some-element off click;
#some-element off custom-event;
kill
Halts all currently running contexts of a specific event on the target element immediately. The running sentence is abandoned, and any in-flight abortable operation (like transition or wait) runs its abort handler, reverting the element to its pre-operation state. This is useful for stopping long-running or infinite loops triggered by events.
For a graceful stop that lets the current sentence complete, use finish.
Arguments:
(event name: Word)
Examples:
<div id="looper" act="loop (wait: 1s; log: 'Running...')"> <button act@click="#looper kill act">Stop the loop</button> </div>
Returns true if contexts were running and halted, false otherwise.
finish
The graceful counterpart to kill: the sentence that is currently executing completes normally (a running 3-second transition plays out its 3 seconds), and then execution stops at the next sentence boundary, so no further sentences run. In-flight abortable operations are not aborted.
Arguments:
(event name: Word)
Examples:
<div id="looper" act="loop (transition: opacity to 0.2 in 1s; transition: opacity to 1 in 1s)"> <button act@click="#looper kill act">Stop now (reverts the running transition)</button> <button act@click="#looper finish act">Stop after the current transition</button> </div>
Returns true if contexts were running, false otherwise.
wait_until
Pauses execution until the target element dispatches the specified event, then resumes. Returns the event object. If the running context is killed, the listener is removed.
Arguments:
(event name: Word)
(wait: 2s; trigger: ready) &
wait_until ready;
log: 'The ready event fired!';
#dialog wait_until close;
log: 'The dialog was closed';
throw
Throws an exception. If no exception is specified, it will throw an ActError exception.
Arguments:
[optional exception: Error instance or message]
Examples:
throw;
// Throws a generic ActError
throw 'Something went wrong!';
// Throws ActError with custom message
throw new Error 'Custom error';
// Throws a specific Error instance
case
A switch-like keyword for matching a value against multiple options.
Arguments:
(value) when (match) (body: Scope) [when (match) (body: Scope) ...] [else (body: Scope)]
Use when for strict comparison (===) and like for loose comparison (==):
$color: 'red';
case $color
when 'red' (
log: 'The color is red!';
)
when 'blue' (
log: 'The color is blue!';
)
else (
log: `Unknown color: {$color}`;
);
// Using "like" for loose comparison (==)
$value: 1;
case $value
like '1' (
log: 'Loosely matches the string "1"';
)
like true (
log: 'Loosely matches true';
);
// case returns the result of the matched branch
$message: case $status
when 'ok' 'All good!'
when 'error' 'Something went wrong!'
else 'Unknown status';
log: $message;
Unlike JavaScript's
switch,casehas no fall-through behavior: only the first matching branch executes, and execution continues after thecasestatement.
debounce
Executes a scope or expression with debounce, specifying the time as the first argument.
Arguments:
(duration: dimension) (scope: Scope)
Examples:
debounce 500ms ( log: 'Debounced' );
lock
Locks the current scope or a specific event, preventing further evaluations until it is unlocked.
Arguments:
- No arguments: Locks the current event scope
(boolean): Sets lock state for current event scope(event name): Locks a specific event on the target element(event name, boolean): Sets lock state for specific event
Examples:
lock;
lock click;
lock scroll false;
unlock
Unlocks the current scope or a specific event, allowing further evaluations.
Arguments:
[event: string]
Examples:
is_locked
Checks if the event execution is locked.
Arguments:
[event: string]
Returns true if the event is locked, false otherwise.
Examples:
is_locked? log: 'Locked';
Signals
Act has a few statements called signals that can be used to control the execution flow of a scope. They are keywords that work by throwing a control-flow signal which the enclosing loop, scope, or function catches. They are grouped separately here because they steer execution rather than compute a value.
break
Breaks the execution of the current scope on an each, loop, for or while operation. It may take an optional value that acts as a return value.
Arguments:
[value]
$i: 0;
$result: while $i < 10 (
$i += 1;
if $i == 5 (
break 'Done';
);
);
continue
Ends the current iteration: iterates and continues the execution of the current scope on an each, loop, for or while operation.
$i: 0;
loop (
$i += 1;
$i % 2 is 0? continue;
$i >= 20? break;
log: $i;
// Logs odd numbers 1, 3, 5, ..., 19
);
repeat
Repeats the execution of the current scope.
// ...
log: 'Looping forever!';
repeat;
restart
Restarts the execution of the root scope.
// ...
$name: prompt: 'What is your name?';
$age: prompt: 'How old are you?';
if $name == 'John' and $age < 18 (
restart; // Goes back to the top!
);
// ...
stop
Stops the execution of the current scope. It can also return a value.
Arguments:
[value]
$username == 'John' ? stop 'John found';
// If the username is John, the execution of the current scope will be stopped and the value 'John found' will be returned.
halt
Ends the execution. It can also return a value.
Arguments:
[value]
// ...
$name: (#username-field :value.to_upper_case!);
log: 'Hello {$name}!';
if $name is 'JOHN' and #age-field.value as number < 18 (
halt;
);
// ...
return
End the execution of a Function and returns a value.
Arguments:
[value]
$validate: -> $name (
$name == 'John'? return 'John found';
// ...
return 'John not found';
);
Act Library
The methods listed below are provided by act. Additionally, when calling a method on a value, if the method isn't found in the act library, it falls through to the native JavaScript prototype for that value. This means all native JS methods are also available on any value.
Element Methods
These methods are available on Element targets.
hide
Hides the target element (sets display: none).
Arguments: None
show
Shows the target element (removes display: none).
Arguments: None
transition
Executes a transition on the target element.
Arguments:
(CSS property Word) [('from' Word) (value)] ('to' Word) (value) ('in' Word) (duration: time dimension) [('using' Word) (timing function Word)] [after (delay: time dimension)], ...
Different properties can be specified on the same call.
Examples:
#some-element transition: transform from 'scale(1)' to 'scale(0.75)' in 0.5s
background-color to white in 1s color to black in 1s;
#some-element transition: background-color from red to black in 250ms using ease-in-out after 10s &
// This transition will take 10 seconds + 250 milliseconds to complete, but as the sentence is asynchronous it will not wait for the transition to finish before continuing.
#some-element transition: border-color to #ff0 in 500ms width to (*width + 50px) in 750ms using linear;
move_to
Moves the target element to a new position in the DOM relative to the specified element.
Arguments:
(target: Element) [position: Word]
Position can be: beforebegin, afterbegin, beforeend, afterend. Defaults to beforeend.
Aliases:
before->beforebeginprepend->afterbeginappend->beforeendafter->afterendinside->innerhtml(replaces innerHTML)replace->outerhtml(replaces the element itself)
Examples:
#some-element move_to: #new-parent;
#some-element move_to: #new-parent afterbegin;
{< p} move_to: first <article> append;
empty
Removes all children of the element.
Arguments: None
clone
Creates a deep clone of the element (including all descendants).
Arguments: None
Examples:
$copy: #some-element.clone!;
#container append: #template.clone!;
prepend
Prepends content to the start of the target element (as first child).
Arguments:
(content: string or Element)
Examples:
#some-element prepend: 'Hello';
#some-element prepend: '<p>First paragraph</p>';
#some-element prepend: new <div>;
append
Appends content to the end of the target element (as last child).
Arguments:
(content: string or Element)
Examples:
#some-element append: 'World';
#some-element append: '<p>Last paragraph</p>';
#some-element append: new <div>;
set_html
Replaces the inner HTML of the target element. When sanitize: true is configured (a sanitizer function must be provided), the HTML string is passed through the sanitizer before insertion.
Arguments:
(content: string or Element)
#some-element set_html: '<p>New content</p>';
set_outer_html
Replaces the element itself with the provided content. When sanitize: true is configured (a sanitizer function must be provided), the HTML string is passed through the sanitizer before insertion.
Arguments:
(content: string or Element)
#some-element set_outer_html: '<section>Replacement</section>';
remove
Removes the target element from the DOM, or removes one or more classes/attributes from the element.
Arguments:
- No arguments: removes the element from the DOM
.class(one or more): removes each specified class@attribute(one or more): removes each specified attribute
#some-element remove!; // Removes the element from DOM
#some-element remove: .active; // Removes the 'active' class
#some-element remove: @disabled; // Removes the 'disabled' attribute
#some-element remove: .active .highlighted; // Removes multiple classes at once
has
Checks if the element matches a selector, class, or attribute presence.
Arguments:
(selector or class or attribute)
- Returns true if the element matches the selector or has the class/attribute.
- Returns false otherwise.
Examples:
#some-element has: .active;
// Element has class 'active'
#some-element has: @disabled;
// Element has attribute 'disabled'
toggle
Toggles visibility, class, or attribute on the target element.
Arguments:
- No arguments: Toggles element visibility (show/hide)
(class or attribute): Toggles the specified class or attribute(class or attribute, force: boolean): Sets class/attribute to specific state
Examples:
#some-element toggle!;
// Toggles visibility
#some-element toggle: .active;
// Toggles the 'active' class
#some-element toggle: @disabled;
// Toggles the 'disabled' attribute
#some-element toggle: .selected true;
// Forces the 'selected' class to be added
add
Adds one or more classes or attributes to the target element.
Arguments:
(class or attribute) [class or attribute ...]
- If a class (e.g.,
.my-class) is provided, it adds the class to the element's classList - If an attribute (e.g.,
@data-custom) is provided, it adds an empty attribute to the element
Examples:
#some-element add: .active;
// Adds the 'active' class
#some-element add: @data-enabled;
// Adds the 'data-enabled' attribute with an empty value
#some-element add: .active @data-enabled;
// Adds both in one call
is_in_view
Checks if the target element is visible in the viewport.
Arguments:
[partially: boolean]
Returns:
trueif fully visible'partially'if partially visible (when first argument istrue)falseif not visible
Examples:
#some-element is_in_view! ? log: 'Visible';
#some-element is_in_view: true;
// Returns 'partially' if any part is visible
next
Gets the next element sibling, optionally matching a selector.
Arguments:
[selector]
Examples:
#some-element.next! *color: red;
#some-element.next: .item;
previous
Gets the previous element sibling, optionally matching a selector.
Arguments:
[selector]
Examples:
#some-element previous! *color: blue;
#some-element previous: .item;
parent
Returns the parent node of the target element.
Arguments: None
#some-element parent! *border: 1px solid black;
matches
Checks if the target element matches a CSS selector.
Arguments:
(selector)
Examples:
#some-element matches: .active ? log: 'It is active';
#some-element matches: {div.container > p};
take
Takes an attribute or class from other elements and applies it to the target element, removing it from the source elements.
Arguments:
(attribute or class) [parent: Element]
The optional parent argument is the element to search within for elements that currently hold the class or attribute. Defaults to this.parentNode (siblings).
Examples:
#some-element take: .active;
// Takes 'active' class from siblings (parent defaults to parentNode)
#some-element take: @disabled;
// Takes 'disabled' attribute from siblings
#some-element take: .selected #my-list;
// Takes 'selected' class from elements inside #my-list
fade
Fades the target element in or out.
Arguments:
('in' or 'out' Word) [time dimension = 250ms] [timing function Word = 'linear']
Examples:
#some-element fade: out 1s;
#some-element fade: in 500ms ease-in-out;
#some-element fade: out; // defaults to 250ms linear
Object/Collection Methods
move_to
Moves all elements in the collection to a target element and position. Uses the same position values as the Element method.
Arguments:
(target: Element) [position: Word]
Position can be: beforebegin, afterbegin, beforeend, afterend. Defaults to beforeend.
Aliases:
before->beforebeginprepend->afterbeginappend->beforeendafter->afterendinside->innerhtml(replaces innerHTML)replace->outerhtml(replaces the element itself)
Examples:
last .item move_to: #container;
last .moved-item move_to: #container prepend;
trigger
Dispatches an event on the target.
The target must be an event target: an element, but also window or document. Triggering on a value that cannot dispatch (a plain object, an array) throws.
Arguments:
(eventName: string) [bubbles: boolean] [detail: object]
eventName- The name of the eventbubbles- Whether the event should bubble (default:true)detail- Custom data to pass with the event (default:empty object)
Examples:
#my-element trigger: click;
#my-element trigger: data_updated true [id: 123, status: complete];
#my-element trigger: notification false [message: 'Hello!'];
js trigger: app_ready; // on window
document trigger: theme_changed true [theme: 'dark'];
Array Methods
map
Creates a new array with the results of calling a provided function on every element in the calling array.
Arguments:
(function)
$nums: [1 2 3];
$doubled: $nums.map: -> $x ($x * 2);
filter
Creates a new array with all elements that pass the test implemented by the provided function.
Arguments:
(function)
$nums: [1 2 3 4];
$evens: $nums.filter: -> $x ($x % 2 == 0);
for_each
Executes a provided function once for each array element.
Arguments:
(function)
$nums: [1 2 3];
$nums.for_each: -> $x (log: $x);
find
Returns the first element in the provided array that satisfies the provided testing function.
Arguments:
(function)
$users: [[name: 'Alice' id: 1] [name: 'Bob' id: 2]];
$user: $users.find: -> $u ($u.id == 2);
find_index
Returns the index of the first element in the array that satisfies the provided testing function.
Arguments:
(function)
$nums: [10 20 30];
$idx: $nums.find_index: -> $x ($x > 15); // Returns 1
some
Tests whether at least one element in the array passes the test implemented by the provided function.
Arguments:
(function)
$nums: [1 2 3];
$has_even: $nums.some: -> $x ($x % 2 == 0); // Returns true
every
Tests whether all elements in the array pass the test implemented by the provided function.
Arguments:
(function)
$nums: [2 4 6];
$all_even: $nums.every: -> $x ($x % 2 == 0); // Returns true
String Methods
after
Returns the substring after the first occurrence of the specified value.
Arguments:
(value: string)
'hello world'.after: ' '; // Returns 'world'
before
Returns the substring before the first occurrence of the specified value.
Arguments:
(value: string)
'hello world'.before: ' '; // Returns 'hello'
between
Returns the substring between the first occurrence of the start value and the first occurrence of the end value.
Arguments:
(start: string) (end: string)
'hello [world]'.between: '[' ']'; // Returns 'world'
capitalize
Capitalizes the first character of the string.
Arguments: None
'hello'.capitalize!; // Returns 'Hello'
Global Methods
wait
Waits for a specified duration.
Arguments:
(duration: dimension)
Examples:
wait: 1s;
wait: 150ms;
wait: 500; // integers are treated as miliseconds
log
Logs the provided arguments to the console.
Arguments:
(values...)
Examples:
warn
Logs a warning to the console.
Arguments:
(values...)
Examples:
warn: 'Something is wrong';
error
Logs an error to the console.
Arguments:
(values...)
Examples:
time_to_ms
Converts a time dimension to milliseconds.
Arguments:
(time: dimension)
Examples:
$ms: time_to_ms: 1s; // 1000
random
Generates a random number between min and max.
Arguments:
(min: number) (max: number)
Examples:
tick
Waits for the next animation frame.
Arguments: None
Examples:
listens_to
Checks if an element listens to an event.
Arguments:
(event: string)
Examples:
is_running
Checks if a bound element has a specific event running. If no event is specified, checks if any event is running.
Arguments:
[event: string]
Examples:
#btn is_running! ? log: 'Running';
#btn is_running: click;
log_raw
Logs the raw value (useful for debugging Result instances).
Arguments:
(value: any)
Examples:
Window Functions
If the specified name does not match any property, Library method or target method, it will be called from the global window object.
// Alert
alert: 'Hello world!';
// Confirm
confirm: 'Are you sure?' ? log: 'User confirmed';
// Prompt
$name: prompt: 'What is your name?';
// LocalStorage
local_storage.set_item: 'theme' 'dark';
$theme: local_storage.get_item: 'theme';
Reserved Words
document
The global document object.
window or js
The global window object.
log: window.inner_width;
log: js.inner_width;
Act
The Act library public API.
me
Refers to the current target.
[1 2 3] each ( log: me );
source_element
The element where the code is placed.
original_target
The initial execution target of the current context: the element the act code was dispatched on at the start of execution, before any target changes (e.g. via with).
debugger
Triggers a breakpoint in the browser's developer tools.
true
Boolean true literal.
false
Boolean false literal.
null
The null value.
undefined
The JavaScript undefined value.
$result: undefined;
log: undefined;
NaN
The JavaScript NaN (Not-a-Number) value.
$invalid: NaN;
0 / 0 == NaN; // false (NaN is never equal to itself)
Events Reference
This section provides detailed documentation on how events work in act.
Event Binding
Act code is placed directly on HTML attributes and script elements, and it runs on standard events like click, mouseover and submit. You can use your own custom events too.
Add the code and bind it to an event with an attribute act@eventname:
<button act@click="fade: out">Fade me out!</button>
You can also put the code on a script tag with the type="text/act" attribute, and an empty act@eventname attribute:
<button> <script type="text/act" act@click> fade: out; </script> Fade me out! </button>
Code in script tags will be bound to the element that is the direct parent of the script tag.
The $event variable
Inside any event handler the triggering DOM event is available as $event:
log: $event.type; // e.g. 'click'
log: $event.target; // the element that was clicked
log: $event.detail; // custom data for CustomEvents
For the act initialization event, $event is the synthetic Event object dispatched by act.
Multiple Events
Multiple events can be mapped to the same attribute by separating the event names with commas:
<button act@click,mouseleave="fade: out">Fade me out!</button>
You can add multiple attributes with multiple events and/or multiple script tags:
<button act@mouseleave="*color: gray" act@click="*color: green"> <script type="text/act" act@keyup> *color: red; </script> <script type="text/act" act@mouseenter> *color: blue; </script> Multi-colored button </button>
IntersectionObserver Events
act provides the inview and offview events based on the IntersectionObserver API that trigger when an element is in or out of the viewport:
<p act@inview="fade: in; *color: red;" act@offview="*color: blue; fade: out;"> I fade in when I'm in the viewport and fade out when I'm out of the viewport. </p>
These events support event delegation using the on keyword with the matching option, allowing you to observe visibility changes on child elements:
<ul id="items"> <script type="text/act"> on inview matching <li> (add: .seen); on offview matching <li> (remove: .seen); </script> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul>
Each <li> element will receive the .seen class when it enters the viewport and lose it when it leaves. The observation is set up on child elements that match the selector, and new elements added dynamically will also be observed.
The act Event
The special act event is triggered when Act initializes (on DOMContentLoaded). You can use it to run initialization code:
<div act="log: 'Element initialized'"> <!-- This code runs when Act starts --> </div> <main act="run init_app"> <script type="text/act" act-block="init_app"> // Initialization logic here log: 'App initialized'; </script> </main>
The act event also fires when new elements are initialized via Act.init(root). This is useful for frameworks like htmx that dynamically load HTML content, since newly inserted elements with act attributes will have their act event triggered upon initialization.
Event Modifiers
You can add modifiers to events by appending a :modifier to the event attribute name.
| Modifier | Description | Example |
|---|---|---|
:once |
Adds the once option to the event making the event only trigger once. |
act@keyup:once |
:target |
It sets the act target to the element where the event was triggered instead of defaulting to the element where the act code is located. | act@click:target |
:prevent |
Prevents the default behavior of the event, calling preventDefault(). |
act@submit:prevent |
:stop |
Stops the propagation of the event, calling stopPropagation(). |
act@click:stop |
:only |
Stops the immediate propagation of the event, calling stopImmediatePropagation(). |
act@click:only |
Multiple event modifiers can be added to the same event:
act@click:target:once:stop
Key Modifiers
For keyboard events (like keyup, keydown) and mouse events, you can filter the event by appending specific keys or modifier keys using dots .. Modifiers are case-insensitive (converted to lowercase internally):
| Modifier | Description | Example |
|---|---|---|
.key |
Triggers only when the specified key is pressed. Works with any key name (e.g., enter, escape, a, z). |
act@keyup.enter, act@keydown.escape |
.modifier |
Requires specific modifier keys (shift, ctrl, alt, meta) to be pressed. | act@click.shift, act@keydown.ctrl.s |
You can chain multiple modifiers to create complex key combinations:
<!-- Trigger only on Enter key --> <input act@keyup.enter="submit!"> <!-- Trigger on Shift + Enter --> <textarea act@keyup.shift.enter="new_line!"></textarea> <!-- Trigger on Ctrl + S --> <div act@keydown.ctrl.s="save!">Save</div> <!-- Shift-click to delete --> <button act@click.shift="delete!">Delete (Shift-Click)</button>
Event Aliases
Event aliases allow you to give a different name to an event handler, which is useful when you need multiple handlers for the same event type or want to use descriptive names with kill or off.
Append #alias to the event:
<button id="handlers" act@click#my_handler="log: 'Handler 1'" act@click#another_handler="log: 'Handler 2'"> Click me - both handlers will fire </button> <button act@click="#handlers off my_handler"> Remove only the first handler </button>
This is particularly useful with the act event when you want to stop specific initialization handlers:
<div id="animated" act@act#animation="fade: out 500ms; fade: in 500ms; repeat;"> <button act@click="#animated kill animation">Stop animation</button> </div>
Note
HTML lowercases attribute names, so aliases are effectively case-insensitive: act@click#saveForm registers saveform, and kill saveForm will not find it. Stick to lowercase or snake_case alias names.
Bracketed Event Names (for names containing : or .)
The parts of an event attribute are separated by sigils (:option, .key, #alias), so a bare event name ends at the first : or .. To use those characters inside a name, wrap it in brackets, which take it verbatim:
<!-- every htmx event needs brackets --> <div act@[htmx:before:request]="add: .loading"></div> <div act@[htmx:after:swap]="log: 'swapped'"></div> <!-- a custom event containing a dot --> <div act@[cart.updated]="run refresh_cart"></div>
Any name may be bracketed, including the alias, and brackets combine with the other parts:
<div act@[htmx:after:swap]#[after.swapping]:once="log: 'once'"></div> <!-- event: htmx:after:swap alias: after.swapping option: once -->
Act is strict about this: a name that is not bracketed and contains an unknown :part is reported in the console instead of silently binding to the wrong event.
<div act@my:custom:event="..."></div> <!-- ๐ฃ act: Unknown event option ":custom" in "act@my:custom:event". If it is part of a name, wrap the name in brackets: act@[my:custom] -->
Warning
Attribute names are lowercased by HTML, so an event whose name contains capitals, such as htmx's htmx:before:viewTransition, cannot be bound from an attribute at all. Use the on keyword with a quoted string, which preserves case:
<script type="text/act"> on 'htmx:before:viewTransition' ( log: 'transitioning' ); </script>
Custom Events
Act can listen to any event, including custom events that you can dispatch yourself. Simply use act@your_event_name:
<div id="my-element" act@my_custom_event="log: 'Custom event received!' $event.detail"> Waiting for custom event... </div>
You can dispatch custom events from JavaScript:
document.querySelector('#my-element').dispatchEvent( new CustomEvent('my_custom_event', { detail: { message: 'Hello!' } }) );
Or from Act code using the trigger method:
<button act@click="#my-element trigger: my_custom_event true [message: 'Hello from Act!']"> Trigger from Act </button>
The event data is available through the $event variable: $event.detail carries custom data passed to the CustomEvent.
Lifecycle Events
act dispatches several custom DOM events throughout the execution lifecycle, allowing you to hook into its behaviour. All events bubble.
actready
Dispatched on document.body once when act finishes initializing (after all elements have been scanned and bound). Equivalent to DOMContentLoaded but for act.
document.addEventListener('actready', (e) => { console.log('Act ready. Bound nodes:', e.detail.bindedNodes); });
| Property | Description |
|---|---|
detail.bindedNodes |
Array of DOM elements that were bound during initialization. |
actbind
Dispatched on each element immediately after act has processed and bound it.
document.addEventListener('actbind', (e) => { console.log('Element bound:', e.detail.element, e.detail.binding); });
| Property | Description |
|---|---|
detail.element |
The DOM element that was bound. |
detail.binding |
The Binding object attached to the element. |
actstart
Dispatched on the target element every time an act event handler begins executing. Fires before the code runs.
document.addEventListener('actstart', (e) => { e.target.setAttribute('aria-busy', 'true'); });
| Property | Description |
|---|---|
detail.event |
The triggering DOM event (or null for the act initialization event). |
detail.source |
The act Source object (holds the raw code and element reference). |
detail.eventManager |
The act EventManager instance managing this handler. |
actend
Dispatched on the target element whenever an act event handler finishes executing, whether it completed successfully, was halted, or threw an error. Always fires.
document.addEventListener('actend', (e) => { e.target.removeAttribute('aria-busy'); });
Shares the same detail properties as actstart.
acterror
Dispatched on the target element when an act event handler throws a runtime error.
document.addEventListener('acterror', (e) => { console.error('Act error:', e.detail.error); });
| Property | Description |
|---|---|
detail.error |
The JavaScript error that was thrown. If it is an ActRuntimeError, detail.error.actException holds the original cause and detail.error.actTrace holds the act stack trace. |
detail.event |
The triggering DOM event. |
detail.source |
The act Source object. |
detail.eventManager |
The act EventManager instance. |
actscriptloaded
Dispatched on the parent element of a <script type="text/act" src="..."> tag once its source has been fetched and is ready to run.
document.addEventListener('actscriptloaded', (e) => { console.log('External act script loaded:', e.detail.element); });
| Property | Description |
|---|---|
detail.element |
The <script> element whose source was loaded. |
detail.target |
The parent element of the script. |
detail.binding |
The Binding object of the parent element. |
Advanced
Configuration
Options
The following options can be configured in Act:
| Option | Type | Default | Description |
|---|---|---|---|
convertToCamelCase |
boolean |
true |
Automatically convert snake_case properties and method names to camelCase. |
start |
boolean |
true |
Automatically start Act, parsing the code in the elements, binding events and triggering the act event when the DOM is ready. |
debug |
boolean |
false |
Enable debug mode (logs more info). |
lexerDebug |
boolean |
false |
Enable lexer debug logging. |
parserDebug |
boolean |
false |
Enable parser debug logging. |
startTime |
boolean |
true |
Log timing information for Act.start() to the console. |
sanitize |
boolean |
false |
Enable HTML sanitization for append, prepend, set_html, and set_outer_html. When true, the sanitizer function must be provided. |
sanitizer |
function|null |
null |
Sanitizer function called with (element, html) before any HTML string is inserted. Return a sanitized string to replace the original, or null to skip insertion entirely. Required when sanitize is true. |
Setting Configuration
You can configure Act in two ways:
-
Meta Tags: Add
<meta name="act-{option}" content="{value}">tags in your HTML<head>. Keys are in kebab-case and values are JSON-parsed.<meta name="act-debug" content="true"> <meta name="act-start" content="false"> <meta name="act-convert-to-camel-case" content="false">
-
Act.extend(): Register an extension with aninstallhook. This is the recommended approach for JavaScript-based configuration and extensions.Act.extend({ install(Act) { Act.config.debug = true; Act.config.start = false; } });
Extensions
Act's extension system is built around Act.extend(), a single entry point for registering plugins before (or after) initialization.
Act.extend(plugin)
Registers a plugin and returns Act for chaining.
Act.extend(plugin); // single plugin Act.extend(a).extend(b); // chained
plugin can be:
- A plain object with optional lifecycle hooks and a
name - A function, treated as the
installhook
// Object form Act.extend({ name: 'my-plugin', // optional, useful for debugging install(Act) { // runs before Act.start() }, ready(Act) { // runs after Act.start() }, }); // Function shorthand (treated as install) Act.extend(function (Act) { Act.Library.Element.pinkify = function () { this.style.backgroundColor = 'pink'; }; });
Hooks
| Hook | When it fires | Typical use |
|---|---|---|
install(Act) |
Before Act.start() |
Add library methods, register keywords/prefixes, modify config |
ready(Act) |
After Act.start() |
Access the initialized DOM, set up integrations |
install(Act)
The install hook is used for anything that needs to be in place before Act scans and binds the page. Because parsing happens inside start(), any keywords, prefixes, or library methods you register in install will be available to every Act expression on the page.
Act.extend({ install(Act) { // configure // Enable sanitization Act.config.sanitize = true; // sanitizer receives (element, html); return the clean string, or null to skip insertion Act.config.sanitizer = (element, html) => DOMPurify.sanitize(html); // add a library method Act.Library.Element.shine = function () { this.style.boxShadow = '0 0 12px gold'; }; // add a keyword Act.Library.keywords.unless = async function (ctx, target, opts, args) { const [condition, body] = args; if (!(await ctx.asValueOf(condition, target))) return await ctx.solve(body, target, opts); }; // add a prefix Act.Library.prefixes.maybe = async function (ctx, target, opts, value) { try { return await ctx.asValueOf(value, target); } catch { return undefined; } }; }, });
ready(Act)
The ready hook fires after the initial page scan, equivalent to listening to the actready DOM event, but scoped to your plugin.
Act.extend({ ready(Act) { console.log('Act is ready. Extensions:', Act.extensions.length); }, });
Late registration
If Act.extend() is called after Act.start() has already run (e.g. from a dynamically import()ed module), both install and ready fire immediately:
// Loaded lazily, long after DOMContentLoaded const { myPlugin } = await import('./my-plugin.js'); Act.extend(myPlugin); // โ install() fires right now // โ ready() fires right now // Any new keywords/methods are available for future Act.init() calls
Act.extensions
A public array of all registered extensions, in registration order. Useful for inspection and debugging:
console.log(Act.extensions.map(e => e.name)); // ['act-ext', 'my-plugin', ...]
What you can extend
Library methods (Act.Library.globals, Act.Library.Element, etc.)
Library methods are called with pre-evaluated (Result) arguments and this bound to the target. They are the most common extension point.
A method is looked up in the buckets below in order, and the first one that defines the name wins. Registering the same name in two buckets is how you specialise a method for a narrower target type.
| Object | Consulted when the target is | Order |
|---|---|---|
Act.Library.Element |
a DOM Element |
1 |
Act.Library.Array |
an Array |
2 |
Act.Library.object |
any object | 3 |
Act.Library.string |
a string |
3 |
Act.Library.number |
a number |
3 |
Act.Library.globals |
any target at all | 4 |
Important
Act.Library.object is not limited to plain objects. Step 3 selects the
bucket by typeof target, and typeof reports 'object' for Elements,
Arrays, NodeLists, window and document too. So a method registered there
is reachable from every object target that steps 1 and 2 did not already
resolve. Put a method there only if it makes sense for all of them, or guard
it and throw. trigger is the built-in example: it lives in
object so it can reach window and document, and throws when the target
cannot dispatch events.
Buckets also exist for the remaining typeof results (boolean, function,
bigint, symbol and undefined) and are empty by default.
Act.extend({ install(Act) { // Global method, available on any target Act.Library.globals.double_log = function (...args) { const values = Act.unwrapAll(args); console.log(...values); console.log(...values); }; // Element method, available when target is an Element Act.Library.Element.pinkify = function () { this.style.backgroundColor = 'pink'; this.style.color = 'black'; }; // String method, available when target is a string Act.Library.string.shout = function () { return this.toUpperCase() + '!!!'; }; }, });
<button act@click="double_log: 'hi'; pinkify!;">Click me!</button> <p act@click="log: 'hello'.shout!;">Hover me!</p>
Library methods that need to respond to kill can use Act.abortable. See Act.abortable.
Keyword operations (Act.Library.keywords)
Keywords are words that the Parser recognises as special expression heads. They receive unevaluated (solvable) arguments, giving you full control over evaluation order and laziness.
All keyword methods are async and receive:
| Parameter | Description |
|---|---|
ctx |
Execution context, use ctx.solve(), ctx.asValueOf(), etc. |
target |
The current Act target |
opts |
Options object |
args |
Array of unevaluated solvable arguments |
this |
Bound to the KeywordExpression solvable node |
Act.extend({ install(Act) { // `unless condition body`: inverse of `if` Act.Library.keywords.unless = async function (ctx, target, opts, args) { const [condition, body] = args; if (!(await ctx.asValueOf(condition, target))) return await ctx.solve(body, target, opts); }; // `wait_more duration`: waits 10x longer than `wait` Act.Library.keywords.wait_more = async function (ctx, target, opts, args) { const { wait, time_to_ms } = Act.Library.globals; if (!args[0]) return await wait.call(target, '60s'); const solved = await ctx.solve(args[0], target, opts); return await wait.call(target, time_to_ms.call(target, Act.unwrap(solved) * 10)); }; }, });
<div act@click="unless $disabled (log: 'fired!')"></div>
Prefix operations (Act.Library.prefixes)
Prefixes are single-word modifiers that wrap the value immediately following them, for example not, global, first. They receive the pre-evaluated value and return a transformed result.
All prefix methods receive:
| Parameter | Description |
|---|---|
ctx |
Execution context |
target |
The current Act target |
opts |
Options object |
value |
The pre-evaluated value the prefix is applied to |
Act.extend({ install(Act) { // `maybe value`: returns undefined instead of throwing Act.Library.prefixes.maybe = async function (ctx, target, opts, value) { try { return await ctx.asValueOf(value, target); } catch { return undefined; } }; // `double value`: multiplies a number by 2 Act.Library.prefixes.double = async function (ctx, target, opts, value) { return Act.unwrap(await ctx.asValueOf(value, target)) * 2; }; }, });
<div act@click="log: maybe $risky-prop; log: double 21;"></div>
Reserved words (Act.Library.words)
Words are identifier-like values with no arguments; they evaluate to a fixed value when encountered in expressions. Built-in examples: me, true, false, null, document, window.
Word functions receive (ctx, target) and return the value.
Act.extend({ install(Act) { // `viewport_width`: returns window.innerWidth Act.Library.words.viewport_width = () => window.innerWidth; // `is_mobile`: returns a boolean Act.Library.words.is_mobile = () => window.innerWidth < 768; }, });
<div act="log: viewport_width; is_mobile? hide!"></div>
Public API
The global Act object is the JavaScript entry point. It exists for configuration, extensions, programmatic control, and writing library methods.
The Act object
| Member | Description |
|---|---|
Act.version |
The current version string (e.g. '0.2.0'). |
Act.config |
The live configuration object. Mutate it from inside an install hook. |
Act.extensions |
The array of registered extensions. |
Act.Library |
The Library registry. Add methods, keywords, and prefixes here to extend the language. |
Act.globals |
The global data scope (the data bag on <body>). Read or write global variables from JavaScript. |
Act.extend(plugin) |
Register an extension. Returns Act for chaining. |
Act.start() |
Manually start Act: scan document.body, bind events, and dispatch actready. Runs automatically on DOMContentLoaded unless start is false. |
Act.configure() |
Read <meta name="act-*"> tags into Act.config and run install hooks. Runs automatically before start(). |
Act.init(root, bindRoot = true, force = false) |
Scan and bind a DOM subtree. Use it to activate act code in dynamically-inserted HTML; the htmx integration relies on it. bindRoot also binds root itself; force re-binds even if root was already bound. |
Act.run(target, code) |
Parse and execute a string of act code against target, returning the run's promise. For programmatic, one-off execution. |
Helper functions
These mirror what library methods work with internally and are useful when writing extensions. See Results and Solving.
| Function | Description |
|---|---|
Act.unwrap(value) |
Return the raw JS value from a Result, or the value unchanged if it isn't one. |
Act.unwrapAll(array) |
unwrap every element of an array. |
Act.is(value, ...types) |
Type guard. Each type is a constructor or a typeof string; returns true if value matches any of them. |
Act.from(value) |
The source Solvable that produced a Result (or undefined). |
Act.through(value) |
Traverse a Result chain back to the original source Result. |
Act.abortable({ perform, abort }) |
Pair async work with cleanup that runs on kill. See Act.abortable. |
Results and Solving
The Result Object
When Act evaluates an expression, it wraps the value in a Result object that carries metadata alongside the raw value. Both library methods (which receive pre-evaluated arguments), keyword and prefix operations (which solve arguments manually via ctx.solve()) deal with Results.
Properties:
| Property / Method | Description |
|---|---|
value |
The actual value (automatically unwraps nested Results) |
from |
The source Solvable that produced this Result |
through |
Traverses the Result chain to the original source |
valueOf() |
Returns the JavaScript primitive value |
toString() |
Returns the string representation |
settable |
Whether the Result can be set (e.g. a variable reference) |
set(value) |
Assign a new value back to the source |
Helper Functions
// Unwrap a Result to get its raw JS value const value = Act.unwrap(result); // Unwrap all values in an array const values = Act.unwrapAll([result1, result2]); // Solve a solvable (use inside keyword and prefix operations) const result = await ctx.solve(solvable, target, opts); // Solve and immediately unwrap const value = Act.unwrap(await ctx.solve(solvable, target, opts));
Act.abortable
Some operations take time: a timeout, an animation, a fetch. While that operation is running, the user might kill it. By default, act stops waiting but has no way to clean up after itself.
Act.abortable solves that. You give it two functions: perform to do the work, and abort to undo it if killed.
Act.abortable({ perform: done => { /* perform the operation, call done() when finished */ }, abort: () => { /* cleanup code that runs only when killed */ }, })
perform receives a done callback; call it when the operation completes. If perform is an async function, act detects the returned Promise and resolves automatically, so done isn't needed in that case.
Note
abort only runs on kill. A graceful finish lets the operation complete normally and never triggers it.
Examples
Highlight: turns an element yellow for a few seconds, and restores it immediately if killed:
Act.extend({ install(Act) { Act.Library.Element.highlight = function (duration) { const original = this.style.backgroundColor; this.style.backgroundColor = 'yellow'; let t; return Act.abortable({ perform: done => { t = setTimeout(() => { this.style.backgroundColor = original; done(); }, Act.unwrap(duration)); }, abort: () => { clearTimeout(t); this.style.backgroundColor = original; }, }); }; }, });
<p id="msg" act@click="highlight: 3s;">Click to highlight for 3 seconds</p> <button act@click="#msg kill click">Cancel highlight</button>
Without Abortable, clicking Cancel would abandon the timeout silently, and the element would stay yellow until the timer fires. With abortable, the cleanup runs immediately on kill and the original color is restored right away.
Typewriter: types text into an element one character at a time. This example shows that cleanup doesn't have to undo the operation; it can also complete it instantly:
Act.extend({ install(Act) { Act.Library.Element.typewrite = function (text, speed) { text = Act.unwrap(text); speed = Act.unwrap(speed) ?? 50; let i = 0, t; return Act.abortable({ perform: done => { const tick = () => { this.textContent += text[i++]; if (i < text.length) t = setTimeout(tick, speed); else done(); }; t = setTimeout(tick, speed); }, abort: () => { clearTimeout(t); this.textContent = text; }, }); }; }, });
<p id="msg" act="typewrite: 'Hello, world!' 80;"> <button act@click="#msg kill act">Skip</button>
Clicking Skip stops the timer and writes the full text at once, rather than leaving the element in an incomplete state. The cleanup decides what "stopping early" means for this operation.
๐ด htmx Integration (act-htmx.js)
act-htmx.js is a tiny glue script that ensures Act bindings work seamlessly with htmx's content swapping. It works with both htmx 2 and htmx 4.
The Problem
When htmx swaps content into the DOM, any Act bindings (act@click, act@mouseover, etc.) in the new content won't be initialized automatically because Act only processes the DOM when it first loads.
The Solution
act-htmx automatically initializes Act bindings on every element htmx processes. This ensures that Act attributes and scripts work immediately on any swapped content, regardless of the swap method used (innerHTML, outerHTML, beforebegin, afterend, etc.). This way htmx takes the wheel, modifies the DOM, and after htmx is done Act continues the job.
Installation
Include both libraries and then the script. That's the whole setup:
<script src="act.js"></script> <script src="htmx.js"></script> <script src="act-htmx.js"></script>
There is nothing to enable per element. Once loaded, every htmx swap on the page initializes any Act code it brought with it.
Usage Example
<!DOCTYPE html> <html> <head> <script src="act.js"></script> <script src="htmx.js"></script> <script src="act-htmx.js"></script> </head> <body> <button hx-get="/new-content" hx-target="#container"> Load Content </button> <div id="container"></div> </body> </html>
When the button is clicked, htmx loads content from /new-content. If that content contains Act attributes or scripts, they'll work automatically:
<!-- Server response from /new-content --> <div> <button act@click="alert: 'I work!'">Click me!</button> <p act@mouseover="*color: red">Hover me!</p> </div>
Notes
- Works with all htmx swap strategies (innerHTML, outerHTML, beforebegin, etc.)
- Requires both htmx and Act to be loaded before the script
- htmx also invokes the callback for its initial pass over the page. That is harmless:
Act.initskips roots that are already bound, so act code never runs twice - Because there is no
hx-extattribute in htmx 4, initialization is page-wide rather than opt-in per element. If you need to exclude a subtree, use htmx's ownhx-ignore
Testing
Act includes a test suite in docs/test.html that validates the correctness of the library's implementation.
Running Tests
Simply open docs/test.html in your browser.
The test suite will automatically execute and display:
- Total tests run
- Passed tests (in green)
- Failed tests (in red)
TypeScript Support
Act includes a TypeScript definition file (act.d.ts) that provides type information for the Act global object and the Act.Library.
You can simply include the act.d.ts file in your project or reference it in your tsconfig.json.
{
"compilerOptions": {
"include": ["act.d.ts"]
}
}Editor Support
Syntax highlighting and language support is available for:
| Editor | .act files |
HTML injection |
|---|---|---|
| VSCode (and derivatives) | โ | โ |
| Neovim (via tree-sitter) | โ | โ |
| Zed | โ | โ |
| TextMate | โ | No |
| Visual Studio | โ | No |
| WebStorm / IntelliJ | โ | No |
Editor support lives in its own repository: oriolmrt/act-editors, which has detailed installation instructions for each editor.
Security
act is a scripting language that executes in the browser, just like JavaScript itself. Be aware that it does not currently implement strict sandboxing or Content Security Policy (CSP) integrations.
As with any scripting language, you should treat act code with the same security considerations as you would raw JavaScript or innerHTML. Ensure that you only execute trusted code and avoid injecting unsanitized user input directly into act attributes or scripts.
HTML Sanitization
When sanitize: true, the append, prepend, set_html, and set_outer_html methods pass HTML strings through the sanitizer. All other HTML assignment paths (inner_html:, <<) are treated as direct, intentional assignments and bypass sanitization. To enable sanitization, set sanitize: true and provide a sanitizer function via an extension:
Act.extend({ install(Act) { Act.config.sanitize = true; // Receives (element, html). Return the sanitized string, or null to skip insertion. Act.config.sanitizer = (element, html) => DOMPurify.sanitize(html); }, });
The sanitizer function receives two arguments:
element: the target DOM element receiving the HTML (useful for context-aware sanitizers)html: the raw HTML string about to be inserted
Return a sanitized string to replace the original, or null to suppress the insertion entirely. If sanitize is true but no sanitizer is provided, act throws a configuration error.
Note
Sanitization only applies to HTML string insertion. Element and DocumentFragment values bypass the hook and are inserted directly.
Contributing
act is a passion project that I work on in my spare time. While I love experimenting with new ideas, my availability for maintenance and feature development can be... let's say organic.
Contributions are welcome If you find bugs (and you will), have ideas for improvements, or want to add features, feel free to open issues or submit pull requests. I'll do my best to review and respond.