https://github.com/amirsaam/elementary-alpine
Alpine.js + Elementary: Reactive web apps with Swift
https://github.com/amirsaam/elementary-alpine
alpinejs server swift swift-on-server web
Last synced: 9 days ago
JSON representation
Alpine.js + Elementary: Reactive web apps with Swift
- Host: GitHub
- URL: https://github.com/amirsaam/elementary-alpine
- Owner: amirsaam
- License: apache-2.0
- Created: 2026-06-14T11:50:29.000Z (11 days ago)
- Default Branch: main
- Last Pushed: 2026-06-15T03:27:25.000Z (10 days ago)
- Last Synced: 2026-06-15T05:05:44.257Z (10 days ago)
- Topics: alpinejs, server, swift, swift-on-server, web
- Language: Swift
- Homepage:
- Size: 30.3 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# ElementaryAlpine: Reactive web apps with Swift
**Ergonomic [AlpineJS](https://alpinejs.dev/) extensions for [Elementary](https://github.com/elementary-swift/elementary)**
```swift
import Elementary
import ElementaryAlpine
// first-class support for all AlpineJS directives
div(.x.data("{ count: 0 }"), .class("counter")) {
button(.x.on("click", "count++")) { "Increment" }
span(.x.text("count")) { "0" }
}
```
```swift
// bind attributes reactively
input(.x.bind("placeholder", "text"), .type(.text))
// bind class with object syntax
div(.x.bindClass("{ 'hidden': !open }"))
// bind style with object syntax
div(.x.bindStyle("{ color: 'red' }"))
```
```swift
// event modifiers (passed as a modifiers array)
form(.x.on("submit", "...", modifiers: [.prevent])) {
button { "Submit" }
}
// keyboard modifiers
input(.x.on("keyup", "submit()", modifiers: [.enter]))
// debounce / throttle (ms)
input(.x.on("input", "fetchResults()", modifiers: [.debounce(500)]))
```
```swift
// two-way binding
input(.x.model("search"))
// model with modifiers
input(.x.model("search", modifiers: [.number, .debounce(300)]))
```
```swift
// loops (semantic name for x-for)
template(.x.loop("item in items")) {
li(.x.text("item")) { "" }
}
// conditional rendering (semantic name for x-if)
template(.x.when("open")) {
div { "Content" }
}
// transitions with modifiers
div(.x.show("open"), .x.transition(modifiers: [.scale(80), .origin(.top)])) {
"Content"
}
```
## Including Alpine.js
This package generates AlpineJS HTML attributes — it does not bundle the Alpine.js runtime. You must include it yourself in your page ``.
This package is built against **Alpine.js v3** (pinned to `3.15.12`).
### From a CDN
```swift
var head: some HTML {
meta(.charset(.utf8))
script(.src("https://cdn.jsdelivr.net/npm/alpinejs@3.15.12/dist/cdn.min.js"), .defer) {}
}
```
### From a local file
Download `alpine.min.js` from the [Alpine.js releases](https://github.com/alpinejs/alpine/releases) and place it in your project's `Public/` folder, then reference it:
```swift
var head: some HTML {
meta(.charset(.utf8))
script(.src("/alpine.min.js"), .defer) {}
}
```
For Hummingbird/Vapor examples, add the file as a resource in your `Package.swift`:
```swift
.executableTarget(
name: "App",
// ...
resources: [
.copy("Public")
]
)
```
## Modifiers
Directives that support modifiers take a `modifiers:` array parameter with a typed enum value:
```swift
// x-show
.x.show("open", modifiers: [.important]) // → x-show.important="open"
// x-on
.x.on("click", "...", modifiers: [.prevent, .stop]) // → x-on:click.prevent.stop="..."
.x.on("keyup", "...", modifiers: [.enter]) // → x-on:keyup.enter="..."
.x.on("input", "...", modifiers: [.debounce(500)]) // → x-on:input.debounce.500ms="..."
.x.on("click", "...", modifiers: [.selfTarget]) // → x-on:click.self="..."
// x-model
.x.model("search", modifiers: [.number, .change, .blur, .enter])
// x-transition
.x.transition(modifiers: [.opacity])
.x.transition(modifiers: [.scale(80), .origin(.topRight)])
.x.transition(modifiers: [.duration(500), .delay(50)])
```
## Globals
Alpine.js global APIs (`Alpine.data`, `Alpine.store`, `Alpine.bind`) are available as `registerGlobal` for registering reusable components, stores, and bound directives:
```swift
import ElementaryAlpine
// In your head:
registerGlobal(.data, on: "dropdown", action: "() => ({ open: false, toggle() { this.open = !this.open } })")
registerGlobal(.store, on: "notifications", action: "{ items: [] }")
registerGlobal(.bind, on: "myButton", action: "() => ({ type: 'button' })")
```
**Generated HTML:**
```html
document.addEventListener('alpine:init', () => { Alpine.data('dropdown', () => ({ open: false, toggle() { this.open = !this.open } })) })
document.addEventListener('alpine:init', () => { Alpine.store('notifications', { items: [] }) })
document.addEventListener('alpine:init', () => { Alpine.bind('myButton', () => ({ type: 'button' })) })
```
**API:**
| Function | Alpine.js call | Use case |
|----------|---------------|----------|
| `registerGlobal(.data, on:, action:)` | `Alpine.data(name, factory)` | Reusable component data (factory function) |
| `registerGlobal(.store, on:, action:)` | `Alpine.store(name, value)` | Global reactive store (direct object) |
| `registerGlobal(.bind, on:, action:)` | `Alpine.bind(name, factory)` | Reusable x-bind object (factory function) |
## Magics
Alpine.js [magics](https://alpinejs.dev/magics) are JS-side helpers that exist inside Alpine expressions. They don't generate HTML attributes or scripts — they appear as **string literals** in directive values:
```swift
// $dispatch — dispatch a custom event
button(.x.on("click", "$dispatch('notify')")) { "Notify" }
// $store — access a global store
div(.x.text("$store.user.name"))
// $refs — reference an element by key
input(.x.ref("myInput"), .type(.text))
button(.x.on("click", "$refs.myInput.focus()")) { "Focus" }
// $watch — reactively watch a property
div(.x.setup("count = 0; $watch('count', value => console.log(value))"))
// $nextTick — wait for next DOM update
div(.x.setup("$nextTick(() => console.log('mounted')"))
// $el, $root, $data, $id — context accessors
div(.x.data("{ open: false }"), .x.text("$el.tagName"))
```
**Available magics:** `$el`, `$refs`, `$store`, `$watch`, `$dispatch`, `$nextTick`, `$root`, `$data`, `$id`, `$persist` (requires the [Persist plugin](#persist))
> No code or attributes are needed for magics — just use the magic name as a string in any Alpine expression.
## Plugins
[Alpine.js plugins](https://alpinejs.dev/plugins) extend the runtime with additional directives. This package ships a separate library, **`ElementaryAlpinePlugins`**, that exposes them as Swift attribute helpers.
> **Alpine.js plugin scripts depend on Alpine.js core.** At the Swift level, `ElementaryAlpinePlugins` has no compile-time dependency on `ElementaryAlpine` — both libraries only depend on `Elementary`. The dependency exists at the **JavaScript runtime** level: plugin CDN scripts hook into the core Alpine instance, so the plugin script tag must be present in your page (and load before Alpine core, per the Alpine.js docs).
**Install plugin scripts** in your `` (BEFORE Alpine core, per Alpine.js docs). Add only the scripts for the plugins you use:
```swift
var head: some HTML {
meta(.charset(.utf8))
// Mask
script(.src("https://cdn.jsdelivr.net/npm/@alpinejs/mask@3.15.12/dist/cdn.min.js"), .defer) {}
// Intersect
script(.src("https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.15.12/dist/cdn.min.js"), .defer) {}
// Resize
script(.src("https://cdn.jsdelivr.net/npm/@alpinejs/resize@3.15.12/dist/cdn.min.js"), .defer) {}
// Persist
script(.src("https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.15.12/dist/cdn.min.js"), .defer) {}
// Focus
script(.src("https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.15.12/dist/cdn.min.js"), .defer) {}
// Collapse
script(.src("https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.15.12/dist/cdn.min.js"), .defer) {}
// Anchor
script(.src("https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.15.12/dist/cdn.min.js"), .defer) {}
// Sort
script(.src("https://cdn.jsdelivr.net/npm/@alpinejs/sort@3.15.12/dist/cdn.min.js"), .defer) {}
// Alpine core (must come after all plugin scripts)
script(.src("https://cdn.jsdelivr.net/npm/alpinejs@3.15.12/dist/cdn.min.js"), .defer) {}
}
```
### Mask
[Mask](https://alpinejs.dev/plugins/mask) formats text input as the user types. Useful for phone numbers, credit cards, dates, account numbers, etc.
**Usage:**
```swift
import Elementary
import ElementaryAlpine
import ElementaryAlpinePlugins
// Static pattern — wildcards: 9 (numeric), a (alpha), * (any)
input(.xMask.pattern("99/99/9999"), .x.model("date"))
input(.xMask.pattern("(999) 999-9999"), .x.model("phone"))
// Dynamic mask — expression receives $input
input(.xMask.dynamic("$money($input)"), .x.model("amount"))
// Dynamic mask — function reference
input(.xMask.dynamic("creditCardMask"), .x.model("card"))
```
**Generated HTML:**
```html
```
**Notes:**
- `x-mask:dynamic` accepts a JavaScript expression or a function name. The expression receives `$input` (the current input value) as a magic.
- The built-in `$money($input, '.', ',', 4)` helper handles currency formatting with optional custom decimal/thousands separators and precision. Pass it as the directive value — no Swift modifier is needed.
- The Mask plugin has **no HTML modifiers** in Alpine.js, so `MaskDynamicModifier` does not exist. All configuration happens in the value string.
### Intersect
[Intersect](https://alpinejs.dev/plugins/intersect) is a convenience wrapper for the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). It runs an expression when an element enters or leaves the viewport — useful for lazy loading, infinite scroll, "view" tracking, etc.
**Usage:**
```swift
import Elementary
import ElementaryAlpine
import ElementaryAlpinePlugins
// Trigger when the element enters the viewport
div(.xIntersect.intersect("shown = true")) {
"I'm in the viewport!"
}
// Trigger only the first time
div(.xIntersect.intersect("loaded = true", modifiers: [.once])) {
"..."
}
// Trigger when at least half of the element is visible
div(.xIntersect.intersect("loaded = true", modifiers: [.half])) {
"..."
}
// Trigger when the whole element is visible
div(.xIntersect.intersect("loaded = true", modifiers: [.full])) {
"..."
}
// Custom threshold (0–100, percentage of element visible)
div(.xIntersect.intersect("loaded = true", modifiers: [.threshold(50)])) {
"..."
}
// Expand the viewport boundary (CSS-margin syntax)
div(.xIntersect.intersect("loaded = true", modifiers: [.margin("200px")])) {
"..."
}
// Trigger on enter (alias of x-intersect)
div(.xIntersect.enter("shown = true")) { "..." }
// Trigger on leave
div(.xIntersect.leave("shown = false")) { "..." }
// Chained modifiers
div(.xIntersect.intersect("loaded = true", modifiers: [.threshold(50), .full])) {
"..."
}
```
**Generated HTML:**
```html
I'm in the viewport!
...
...
...
...
...
...
...
...
```
**Modifier reference:**
| Modifier | Raw value | Notes |
|----------|-----------|-------|
| `.once` | `once` | Fire only the first time |
| `.half` | `half` | Fire at 50% visibility |
| `.full` | `full` | Fire at 99% visibility |
| `.threshold(Int)` | `threshold.N` | Custom percentage (0–100) |
| `.margin(String)` | `margin.` | Expand/contract viewport boundary (CSS-margin syntax) |
### Resize
[Resize](https://alpinejs.dev/plugins/resize) is a convenience wrapper for the [Resize Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API). It exposes `$width` and `$height` magics whenever an element changes size.
**Usage:**
```swift
import Elementary
import ElementaryAlpine
import ElementaryAlpinePlugins
// Track an element's size
div(.xResize.resize("width = $width; height = $height")) {
p(.x.text("'Width: ' + width + 'px'")) { "" }
p(.x.text("'Height: ' + height + 'px'")) { "" }
}
// Track the entire document
div(.xResize.resize("width = $width; height = $height", modifiers: [.document])) { ... }
```
**Generated HTML:**
```html
...
```
**Modifier reference:**
| Modifier | Raw value | Notes |
|----------|-----------|-------|
| `.document` | `document` | Observe the document instead of a specific element |
### Persist
[Persist](https://alpinejs.dev/plugins/persist) saves Alpine state to `localStorage` (or `sessionStorage`) so values persist across page loads. Useful for search filters, active tabs, theme preferences, and other state that users expect to survive a refresh.
Unlike the other plugins, Persist is a **magic**, not a directive — there is no `x-persist` HTML attribute. The API is the `$persist(...)` function used inside `x-data` values.
**Usage:**
```swift
import Elementary
import ElementaryAlpine
import ElementaryAlpinePlugins
// Persist a counter to localStorage
div(.x.data("{ count: $persist(0) }")) {
button(.x.on("click", "count++")) { "Increment" }
span(.x.text("count")) { "" }
}
// Use a custom localStorage key
div(.x.data("{ count: $persist(0).as('my-count') }")) {
button(.x.on("click", "count++")) { "Increment" }
span(.x.text("count")) { "" }
}
// Use sessionStorage instead (cleared when the tab closes)
div(.x.data("{ count: $persist(0).using(sessionStorage) }")) {
button(.x.on("click", "count++")) { "Increment" }
span(.x.text("count")) { "" }
}
```
**Generated HTML:**
```html
Increment
Increment
Increment
```
**Notes:**
- Persist is **not a directive**, so there is no `HTMLAttribute` helper. Write `$persist(...)` as a JS string in your `x-data` value.
- `.as(...)` and `.using(...)` are **JavaScript method calls** on the `$persist(...)` return value, not HTML modifiers — they cannot be type-safe in Swift.
- `$persist` works with primitives, arrays, and objects. If you change the type of a persisted value, clear its localStorage entry first.
### Focus
[Focus](https://alpinejs.dev/plugins/focus) lets you manage focus on a page, including trapping focus within an element (for modals/dialogs), navigating focus with arrow keys, and more.
**Usage:**
```swift
import Elementary
import ElementaryAlpine
import ElementaryAlpinePlugins
// Trap focus inside an element while `open` is true
div(.x.data("{ open: false }")) {
button(.x.on("click", "open = true")) { "Open Dialog" }
span(.x.show("open"), .xFocus.trap("open")) {
p { "..." }
input(.type(.text), .placeholder("Some input..."))
input(.type(.text), .placeholder("Some other input..."))
button(.x.on("click", "open = false")) { "Close Dialog" }
}
}
// Hide all other elements from screen readers while trapped
div(.xFocus.trap("open", modifiers: [.inert])) { ... }
// Disable page scroll while trapped
div(.xFocus.trap("open", modifiers: [.noscroll])) { ... }
// Don't return focus to the previous element on untrap
div(.xFocus.trap("open", modifiers: [.noreturn])) { ... }
// Don't auto-focus the first focusable element on trap
div(.xFocus.trap("open", modifiers: [.noautofocus])) { ... }
// Chained modifiers
div(.xFocus.trap("open", modifiers: [.inert, .noscroll, .noreturn])) { ... }
```
**Generated HTML:**
```html
...
...
...
...
...
...
```
**Modifier reference:**
| Modifier | Raw value | Notes |
|----------|-----------|-------|
| `.inert` | `inert` | Mark other page elements `aria-hidden="true"` while trapped |
| `.noscroll` | `noscroll` | Block page scrolling while trapped |
| `.noreturn` | `noreturn` | Don't return focus on untrap |
| `.noautofocus` | `noautofocus` | Don't auto-focus the first focusable element |
**Notes:**
- The Focus plugin also provides a `$focus` magic (`.next()`, `.previous()`, `.wrap()`, `.first()`, `.last()`, etc.) used as JS strings inside `x-on` handlers — no Swift helper needed.
- The Focus plugin was previously called "Trap" — `x-trap` and its modifiers are unchanged.
### Collapse
[Collapse](https://alpinejs.dev/plugins/collapse) expands and collapses elements with smooth height animations. Unlike `x-transition`, `x-collapse` is dedicated to height-based collapse and works alongside `x-show`.
**Usage:**
```swift
import Elementary
import ElementaryAlpine
import ElementaryAlpinePlugins
// Basic collapse (works with x-show)
p(.x.show("expanded"), .xCollapse.collapse()) {
"..."
}
// Custom duration (ms)
p(.x.show("expanded"), .xCollapse.collapse(modifiers: [.duration(1000)])) {
"..."
}
// Minimum collapsed height (px) — useful for "cut-off" instead of full hide
p(.x.show("expanded"), .xCollapse.collapse(modifiers: [.min(50)])) {
"..."
}
// Chained modifiers
p(.x.show("expanded"), .xCollapse.collapse(modifiers: [.duration(500), .min(50)])) {
"..."
}
```
**Generated HTML:**
```html
...
...
...
...
```
**Modifier reference:**
| Modifier | Raw value | Notes |
|----------|-----------|-------|
| `.duration(Int)` | `duration.Nms` | Animation duration in milliseconds |
| `.min(Int)` | `min.Npx` | Minimum collapsed height in pixels (cuts off rather than fully hides) |
**Notes:**
- `x-collapse` can only exist on an element that already has `x-show`. It animates the height property when `x-show` toggles visibility.
- `x-collapse` has no value — it only accepts modifiers.
### Anchor
[Anchor](https://alpinejs.dev/plugins/anchor) anchors an element's positioning to another element on the page. Built on top of [Floating UI](https://floating-ui.com/), it powers dropdowns, popovers, tooltips, and dialogs.
**Usage:**
```swift
import Elementary
import ElementaryAlpine
import ElementaryAlpinePlugins
// Anchor below the button (default positioning)
div(.x.data("{ open: false }")) {
button(.x.ref("button"), .x.on("click", "open = ! open")) { "Toggle" }
div(.x.show("open"), .xAnchor.anchor("$refs.button")) {
"Dropdown content"
}
}
// Anchor below-right of the button
div(.x.show("open"), .xAnchor.anchor("$refs.button", modifiers: [.bottomStart])) {
"Dropdown content"
}
// Use fixed positioning (escapes overflow:hidden containers)
div(.x.show("open"), .xAnchor.anchor("$refs.button", modifiers: [.fixed])) {
"Dropdown content"
}
// Add an offset (px)
div(.x.show("open"), .xAnchor.anchor("$refs.button", modifiers: [.offset(10)])) {
"Dropdown content"
}
// Prevent auto-flip when there's no room below
div(.x.show("open"), .xAnchor.anchor("$refs.button", modifiers: [.noflip])) {
"Dropdown content"
}
// Apply positioning yourself via $anchor.x/$anchor.y in x-bind:style
div(
.x.show("open"),
.xAnchor.anchor("$refs.button", modifiers: [.noStyle]),
.x.bindStyle("{ position: 'absolute', top: $anchor.y+'px', left: $anchor.x+'px' }")
) {
"Dropdown content"
}
// Anchor to an element by id
div(.x.show("open"), .xAnchor.anchor("document.getElementById('trigger')")) {
"Dropdown content"
}
```
**Generated HTML:**
```html
Dropdown content
Dropdown content
Dropdown content
Dropdown content
Dropdown content
Dropdown content
Dropdown content
```
**Positioning modifiers:**
| Modifier | Raw value | Notes |
|----------|-----------|-------|
| `.top` | `top` | Above the reference, centered |
| `.topStart` | `top-start` | Above the reference, aligned to the start |
| `.topEnd` | `top-end` | Above the reference, aligned to the end |
| `.bottom` | `bottom` | Below the reference, centered |
| `.bottomStart` | `bottom-start` | Below the reference, aligned to the start |
| `.bottomEnd` | `bottom-end` | Below the reference, aligned to the end |
| `.left` | `left` | Left of the reference, centered |
| `.leftStart` | `left-start` | Left of the reference, aligned to the start |
| `.leftEnd` | `left-end` | Left of the reference, aligned to the end |
| `.right` | `right` | Right of the reference, centered |
| `.rightStart` | `right-start` | Right of the reference, aligned to the start |
| `.rightEnd` | `right-end` | Right of the reference, aligned to the end |
**Other modifiers:**
| Modifier | Raw value | Notes |
|----------|-----------|-------|
| `.fixed` | `fixed` | Use `position: fixed` (escapes `overflow: hidden` containers) |
| `.offset(Int)` | `offset.N` | Spacing in pixels between anchored and reference element |
| `.noflip` | `noflip` | Don't auto-flip when there's no room in the chosen direction |
| `.noStyle` | `no-style` | Don't apply positioning styles; access them via `$anchor.x` / `$anchor.y` in `x-bind:style` |
**Notes:**
- `x-anchor` is a thin wrapper around [Floating UI](https://floating-ui.com/). For advanced configuration not exposed by the modifiers, use `x-anchor.noStyle` and apply styles yourself via `x-bind:style` and the `$anchor` magic.
- A `transform`, `filter`, `perspective`, `backdrop-filter`, `will-change`, or `contain` on any ancestor creates a new containing block for `position: fixed` descendants. `.fixed` will behave like `position: absolute` relative to that ancestor.
### Sort
[Sort](https://alpinejs.dev/plugins/sort) lets you re-order elements by dragging them with your mouse. Built on top of [SortableJS](https://github.com/SortableJS/Sortable), it powers Kanban boards, to-do lists, sortable table columns, and more.
**Usage:**
```swift
import Elementary
import ElementaryAlpine
import ElementaryAlpinePlugins
// Basic sortable list
ul(.xSort.sort) {
li(.xSort.item("1")) { "foo" }
li(.xSort.item("2")) { "bar" }
li(.xSort.item("3")) { "baz" }
}
// Sort with a handler that runs on every reorder
ul(.xSort.sort("alert($item + ' - ' + $position)")) {
li(.xSort.item("1")) { "foo" }
li(.xSort.item("2")) { "bar" }
li(.xSort.item("3")) { "baz" }
}
// Group sortable lists — items can be dragged between lists with the same group
ul(.xSort.sort("handle"), .xSort.group("todos")) {
li(.xSort.item("1")) { "foo" }
li(.xSort.item("2")) { "bar" }
li(.xSort.item("3")) { "baz" }
}
ol(.xSort.sort("handle"), .xSort.group("todos")) {
li(.xSort.item("4")) { "foo" }
li(.xSort.item("5")) { "bar" }
li(.xSort.item("6")) { "baz" }
}
// Drag handles — only the handle initiates drag
ul(.xSort.sort) {
li(.xSort.item("1")) {
span(.xSort.handle) { " - " }
"foo"
}
li(.xSort.item("2")) {
span(.xSort.handle) { " - " }
"bar"
}
}
// Ignore elements — buttons inside items stay clickable
ul(.xSort.sort) {
li(.xSort.item("1")) {
"foo"
button(.xSort.ignore) { "Edit" }
}
}
// Show a ghost of the dragged element instead of an empty space
ul(.xSort.sort(modifiers: [.ghost])) {
li(.xSort.item("1")) { "foo" }
li(.xSort.item("2")) { "bar" }
}
// Pass custom SortableJS options
ul(.xSort.sort, .xSort.config("{ animation: 0 }")) {
li(.xSort.item("1")) { "foo" }
}
```
**Generated HTML:**
```html
- foo
- bar
- baz
- foo
- bar
- baz
- foo
- bar
- baz
- foo
- bar
- baz
-
- foo
-
- bar
-
foo
Edit
- foo
- bar
- foo
```
**Modifier reference:**
| Modifier | Raw value | Notes |
|----------|-----------|-------|
| `.ghost` | `ghost` | Show a ghost of the dragged element in its place (default: empty hole) |
**Notes:**
- The Sort handler is called every time sort order changes. Inside the handler, `$item` is the moved item's key (from `x-sort:item`) and `$position` is its new index (starting at `0`). The handler can also be a function reference that receives `(item, position)` as arguments.
- `x-sort:item` keys are typically numeric (`"1"`, `"2"`, …) but can be any string used to identify the item.
- `x-sort:group` lets you drag items between lists. When using `.as` handlers with cross-group drag, only the destination list's handler is called.
- `x-sort:config` accepts any [SortableJS options](https://github.com/SortableJS/Sortable?tab=readme-ov-file#options). Be aware that overwriting `handle`, `group`, `filter`, `onSort`, `onStart`, or `onEnd` may break functionality.
- While dragging, Alpine adds a `.sorting` class to `` — useful for conditional CSS like `body.sorting #warning { display: block; }`.
## Play with it
Example apps will be added in a future release.
## Documentation
The package ships two libraries:
- **`ElementaryAlpine`** — core:
- **Attribute helpers** via the `.x` syntax on all `HTMLElements` for all 17 core [AlpineJS directives](https://alpinejs.dev/directives):
- `x-data`, `x-init` (`.setup`), `x-show`
- `x-bind` / `x-bind:class` / `x-bind:style`
- `x-on` with modifiers (base, keyboard, mouse, advanced)
- `x-text`, `x-html`, `x-model` with modifiers, `x-modelable`
- `x-for` (`.loop`), `x-transition` (all phases), `x-effect`, `x-ignore`, `x-ref`, `x-cloak`
- `x-teleport`, `x-if` (`.when`), `x-id`
- **Global helpers** — `registerGlobal(_:on:action:)` for `Alpine.data()`, `Alpine.store()`, `Alpine.bind()` (see [Globals](#globals))
- **`ElementaryAlpinePlugins`** — Alpine.js plugin wrappers (see [Plugins](#plugins)). Currently ships **Mask** (`.xMask.pattern` / `.xMask.dynamic`), **Intersect** (`.xIntersect.intersect` / `.enter` / `.leave`), **Resize** (`.xResize.resize`), **Persist** (the `$persist` magic — no directive surface), **Focus** (`.xFocus.trap`), **Collapse** (`.xCollapse.collapse`), **Anchor** (`.xAnchor.anchor`), and **Sort** (`.xSort.sort` / `.item` / `.group` / `.handle` / `.ignore` / `.config`).
## Future directions
- Remaining plugin wrapper: Morph (Alpine.morph() global — no directive surface)
PRs welcome.
## License
[Apache 2.0](./LICENSE)