{"id":50917240,"url":"https://github.com/amirsaam/elementary-alpine","last_synced_at":"2026-06-16T16:32:21.303Z","repository":{"id":364913062,"uuid":"1269175109","full_name":"amirsaam/elementary-alpine","owner":"amirsaam","description":"Alpine.js + Elementary: Reactive web apps with Swift","archived":false,"fork":false,"pushed_at":"2026-06-15T03:27:25.000Z","size":31,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-15T05:05:44.257Z","etag":null,"topics":["alpinejs","server","swift","swift-on-server","web"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/amirsaam.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-14T11:50:29.000Z","updated_at":"2026-06-15T03:31:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/amirsaam/elementary-alpine","commit_stats":null,"previous_names":["amirsaam/elementary-alpine"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/amirsaam/elementary-alpine","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amirsaam%2Felementary-alpine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amirsaam%2Felementary-alpine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amirsaam%2Felementary-alpine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amirsaam%2Felementary-alpine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/amirsaam","download_url":"https://codeload.github.com/amirsaam/elementary-alpine/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/amirsaam%2Felementary-alpine/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34415240,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alpinejs","server","swift","swift-on-server","web"],"created_at":"2026-06-16T16:32:20.410Z","updated_at":"2026-06-16T16:32:21.287Z","avatar_url":"https://github.com/amirsaam.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ElementaryAlpine: Reactive web apps with Swift\n\n**Ergonomic [AlpineJS](https://alpinejs.dev/) extensions for [Elementary](https://github.com/elementary-swift/elementary)**\n\n```swift\nimport Elementary\nimport ElementaryAlpine\n\n// first-class support for all AlpineJS directives\ndiv(.x.data(\"{ count: 0 }\"), .class(\"counter\")) {\n    button(.x.on(\"click\", \"count++\")) { \"Increment\" }\n    span(.x.text(\"count\")) { \"0\" }\n}\n```\n\n```swift\n// bind attributes reactively\ninput(.x.bind(\"placeholder\", \"text\"), .type(.text))\n\n// bind class with object syntax\ndiv(.x.bindClass(\"{ 'hidden': !open }\"))\n\n// bind style with object syntax\ndiv(.x.bindStyle(\"{ color: 'red' }\"))\n```\n\n```swift\n// event modifiers (passed as a modifiers array)\nform(.x.on(\"submit\", \"...\", modifiers: [.prevent])) {\n    button { \"Submit\" }\n}\n\n// keyboard modifiers\ninput(.x.on(\"keyup\", \"submit()\", modifiers: [.enter]))\n\n// debounce / throttle (ms)\ninput(.x.on(\"input\", \"fetchResults()\", modifiers: [.debounce(500)]))\n```\n\n```swift\n// two-way binding\ninput(.x.model(\"search\"))\n\n// model with modifiers\ninput(.x.model(\"search\", modifiers: [.number, .debounce(300)]))\n```\n\n```swift\n// loops (semantic name for x-for)\ntemplate(.x.loop(\"item in items\")) {\n    li(.x.text(\"item\")) { \"\" }\n}\n\n// conditional rendering (semantic name for x-if)\ntemplate(.x.when(\"open\")) {\n    div { \"Content\" }\n}\n\n// transitions with modifiers\ndiv(.x.show(\"open\"), .x.transition(modifiers: [.scale(80), .origin(.top)])) {\n    \"Content\"\n}\n```\n\n## Including Alpine.js\n\nThis package generates AlpineJS HTML attributes — it does not bundle the Alpine.js runtime. You must include it yourself in your page `\u003chead\u003e`.\n\nThis package is built against **Alpine.js v3** (pinned to `3.15.12`).\n\n### From a CDN\n\n```swift\nvar head: some HTML {\n    meta(.charset(.utf8))\n    script(.src(\"https://cdn.jsdelivr.net/npm/alpinejs@3.15.12/dist/cdn.min.js\"), .defer) {}\n}\n```\n\n### From a local file\n\nDownload `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:\n\n```swift\nvar head: some HTML {\n    meta(.charset(.utf8))\n    script(.src(\"/alpine.min.js\"), .defer) {}\n}\n```\n\nFor Hummingbird/Vapor examples, add the file as a resource in your `Package.swift`:\n\n```swift\n.executableTarget(\n    name: \"App\",\n    // ...\n    resources: [\n        .copy(\"Public\")\n    ]\n)\n```\n\n## Modifiers\n\nDirectives that support modifiers take a `modifiers:` array parameter with a typed enum value:\n\n```swift\n// x-show\n.x.show(\"open\", modifiers: [.important])              // → x-show.important=\"open\"\n\n// x-on\n.x.on(\"click\", \"...\", modifiers: [.prevent, .stop])  // → x-on:click.prevent.stop=\"...\"\n.x.on(\"keyup\", \"...\", modifiers: [.enter])            // → x-on:keyup.enter=\"...\"\n.x.on(\"input\", \"...\", modifiers: [.debounce(500)])    // → x-on:input.debounce.500ms=\"...\"\n.x.on(\"click\", \"...\", modifiers: [.selfTarget])       // → x-on:click.self=\"...\"\n\n// x-model\n.x.model(\"search\", modifiers: [.number, .change, .blur, .enter])\n\n// x-transition\n.x.transition(modifiers: [.opacity])\n.x.transition(modifiers: [.scale(80), .origin(.topRight)])\n.x.transition(modifiers: [.duration(500), .delay(50)])\n```\n\n## Globals\n\nAlpine.js global APIs (`Alpine.data`, `Alpine.store`, `Alpine.bind`) are available as `registerGlobal` for registering reusable components, stores, and bound directives:\n\n```swift\nimport ElementaryAlpine\n\n// In your head:\nregisterGlobal(.data, on: \"dropdown\", action: \"() =\u003e ({ open: false, toggle() { this.open = !this.open } })\")\nregisterGlobal(.store, on: \"notifications\", action: \"{ items: [] }\")\nregisterGlobal(.bind, on: \"myButton\", action: \"() =\u003e ({ type: 'button' })\")\n```\n\n**Generated HTML:**\n\n```html\n\u003cscript\u003edocument.addEventListener('alpine:init', () =\u003e { Alpine.data('dropdown', () =\u003e ({ open: false, toggle() { this.open = !this.open } })) })\u003c/script\u003e\n\u003cscript\u003edocument.addEventListener('alpine:init', () =\u003e { Alpine.store('notifications', { items: [] }) })\u003c/script\u003e\n\u003cscript\u003edocument.addEventListener('alpine:init', () =\u003e { Alpine.bind('myButton', () =\u003e ({ type: 'button' })) })\u003c/script\u003e\n```\n\n**API:**\n\n| Function | Alpine.js call | Use case |\n|----------|---------------|----------|\n| `registerGlobal(.data, on:, action:)` | `Alpine.data(name, factory)` | Reusable component data (factory function) |\n| `registerGlobal(.store, on:, action:)` | `Alpine.store(name, value)` | Global reactive store (direct object) |\n| `registerGlobal(.bind, on:, action:)` | `Alpine.bind(name, factory)` | Reusable x-bind object (factory function) |\n\n## Magics\n\nAlpine.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:\n\n```swift\n// $dispatch — dispatch a custom event\nbutton(.x.on(\"click\", \"$dispatch('notify')\")) { \"Notify\" }\n\n// $store — access a global store\ndiv(.x.text(\"$store.user.name\"))\n\n// $refs — reference an element by key\ninput(.x.ref(\"myInput\"), .type(.text))\nbutton(.x.on(\"click\", \"$refs.myInput.focus()\")) { \"Focus\" }\n\n// $watch — reactively watch a property\ndiv(.x.setup(\"count = 0; $watch('count', value =\u003e console.log(value))\"))\n\n// $nextTick — wait for next DOM update\ndiv(.x.setup(\"$nextTick(() =\u003e console.log('mounted')\"))\n\n// $el, $root, $data, $id — context accessors\ndiv(.x.data(\"{ open: false }\"), .x.text(\"$el.tagName\"))\n```\n\n**Available magics:** `$el`, `$refs`, `$store`, `$watch`, `$dispatch`, `$nextTick`, `$root`, `$data`, `$id`, `$persist` (requires the [Persist plugin](#persist))\n\n\u003e No code or attributes are needed for magics — just use the magic name as a string in any Alpine expression.\n\n## Plugins\n\n[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.\n\n\u003e **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).\n\n**Install plugin scripts** in your `\u003chead\u003e` (BEFORE Alpine core, per Alpine.js docs). Add only the scripts for the plugins you use:\n\n```swift\nvar head: some HTML {\n    meta(.charset(.utf8))\n    // Mask\n    script(.src(\"https://cdn.jsdelivr.net/npm/@alpinejs/mask@3.15.12/dist/cdn.min.js\"), .defer) {}\n    // Intersect\n    script(.src(\"https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.15.12/dist/cdn.min.js\"), .defer) {}\n    // Resize\n    script(.src(\"https://cdn.jsdelivr.net/npm/@alpinejs/resize@3.15.12/dist/cdn.min.js\"), .defer) {}\n    // Persist\n    script(.src(\"https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.15.12/dist/cdn.min.js\"), .defer) {}\n    // Focus\n    script(.src(\"https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.15.12/dist/cdn.min.js\"), .defer) {}\n    // Collapse\n    script(.src(\"https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.15.12/dist/cdn.min.js\"), .defer) {}\n    // Anchor\n    script(.src(\"https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.15.12/dist/cdn.min.js\"), .defer) {}\n    // Sort\n    script(.src(\"https://cdn.jsdelivr.net/npm/@alpinejs/sort@3.15.12/dist/cdn.min.js\"), .defer) {}\n    // Alpine core (must come after all plugin scripts)\n    script(.src(\"https://cdn.jsdelivr.net/npm/alpinejs@3.15.12/dist/cdn.min.js\"), .defer) {}\n}\n```\n\n### Mask\n\n[Mask](https://alpinejs.dev/plugins/mask) formats text input as the user types. Useful for phone numbers, credit cards, dates, account numbers, etc.\n\n**Usage:**\n\n```swift\nimport Elementary\nimport ElementaryAlpine\nimport ElementaryAlpinePlugins\n\n// Static pattern — wildcards: 9 (numeric), a (alpha), * (any)\ninput(.xMask.pattern(\"99/99/9999\"), .x.model(\"date\"))\ninput(.xMask.pattern(\"(999) 999-9999\"), .x.model(\"phone\"))\n\n// Dynamic mask — expression receives $input\ninput(.xMask.dynamic(\"$money($input)\"), .x.model(\"amount\"))\n\n// Dynamic mask — function reference\ninput(.xMask.dynamic(\"creditCardMask\"), .x.model(\"card\"))\n```\n\n**Generated HTML:**\n\n```html\n\u003cinput x-mask=\"99/99/9999\" x-model=\"date\"\u003e\n\u003cinput x-mask=\"(999) 999-9999\" x-model=\"phone\"\u003e\n\u003cinput x-mask:dynamic=\"$money($input)\" x-model=\"amount\"\u003e\n\u003cinput x-mask:dynamic=\"creditCardMask\" x-model=\"card\"\u003e\n```\n\n**Notes:**\n- `x-mask:dynamic` accepts a JavaScript expression or a function name. The expression receives `$input` (the current input value) as a magic.\n- 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.\n- The Mask plugin has **no HTML modifiers** in Alpine.js, so `MaskDynamicModifier` does not exist. All configuration happens in the value string.\n\n### Intersect\n\n[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.\n\n**Usage:**\n\n```swift\nimport Elementary\nimport ElementaryAlpine\nimport ElementaryAlpinePlugins\n\n// Trigger when the element enters the viewport\ndiv(.xIntersect.intersect(\"shown = true\")) {\n    \"I'm in the viewport!\"\n}\n\n// Trigger only the first time\ndiv(.xIntersect.intersect(\"loaded = true\", modifiers: [.once])) {\n    \"...\"\n}\n\n// Trigger when at least half of the element is visible\ndiv(.xIntersect.intersect(\"loaded = true\", modifiers: [.half])) {\n    \"...\"\n}\n\n// Trigger when the whole element is visible\ndiv(.xIntersect.intersect(\"loaded = true\", modifiers: [.full])) {\n    \"...\"\n}\n\n// Custom threshold (0–100, percentage of element visible)\ndiv(.xIntersect.intersect(\"loaded = true\", modifiers: [.threshold(50)])) {\n    \"...\"\n}\n\n// Expand the viewport boundary (CSS-margin syntax)\ndiv(.xIntersect.intersect(\"loaded = true\", modifiers: [.margin(\"200px\")])) {\n    \"...\"\n}\n\n// Trigger on enter (alias of x-intersect)\ndiv(.xIntersect.enter(\"shown = true\")) { \"...\" }\n\n// Trigger on leave\ndiv(.xIntersect.leave(\"shown = false\")) { \"...\" }\n\n// Chained modifiers\ndiv(.xIntersect.intersect(\"loaded = true\", modifiers: [.threshold(50), .full])) {\n    \"...\"\n}\n```\n\n**Generated HTML:**\n\n```html\n\u003cdiv x-intersect=\"shown = true\"\u003eI'm in the viewport!\u003c/div\u003e\n\u003cdiv x-intersect.once=\"loaded = true\"\u003e...\u003c/div\u003e\n\u003cdiv x-intersect.half=\"loaded = true\"\u003e...\u003c/div\u003e\n\u003cdiv x-intersect.full=\"loaded = true\"\u003e...\u003c/div\u003e\n\u003cdiv x-intersect.threshold.50=\"loaded = true\"\u003e...\u003c/div\u003e\n\u003cdiv x-intersect.margin.200px=\"loaded = true\"\u003e...\u003c/div\u003e\n\u003cdiv x-intersect:enter=\"shown = true\"\u003e...\u003c/div\u003e\n\u003cdiv x-intersect:leave=\"shown = false\"\u003e...\u003c/div\u003e\n\u003cdiv x-intersect.threshold.50.full=\"loaded = true\"\u003e...\u003c/div\u003e\n```\n\n**Modifier reference:**\n\n| Modifier | Raw value | Notes |\n|----------|-----------|-------|\n| `.once` | `once` | Fire only the first time |\n| `.half` | `half` | Fire at 50% visibility |\n| `.full` | `full` | Fire at 99% visibility |\n| `.threshold(Int)` | `threshold.N` | Custom percentage (0–100) |\n| `.margin(String)` | `margin.\u003ccss-margin\u003e` | Expand/contract viewport boundary (CSS-margin syntax) |\n\n### Resize\n\n[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.\n\n**Usage:**\n\n```swift\nimport Elementary\nimport ElementaryAlpine\nimport ElementaryAlpinePlugins\n\n// Track an element's size\ndiv(.xResize.resize(\"width = $width; height = $height\")) {\n    p(.x.text(\"'Width: ' + width + 'px'\")) { \"\" }\n    p(.x.text(\"'Height: ' + height + 'px'\")) { \"\" }\n}\n\n// Track the entire document\ndiv(.xResize.resize(\"width = $width; height = $height\", modifiers: [.document])) { ... }\n```\n\n**Generated HTML:**\n\n```html\n\u003cdiv x-resize=\"width = $width; height = $height\"\u003e\n    \u003cp x-text=\"'Width: ' + width + 'px'\"\u003e\u003c/p\u003e\n    \u003cp x-text=\"'Height: ' + height + 'px'\"\u003e\u003c/p\u003e\n\u003c/div\u003e\n\u003cdiv x-resize.document=\"width = $width; height = $height\"\u003e...\u003c/div\u003e\n```\n\n**Modifier reference:**\n\n| Modifier | Raw value | Notes |\n|----------|-----------|-------|\n| `.document` | `document` | Observe the document instead of a specific element |\n\n### Persist\n\n[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.\n\nUnlike 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.\n\n**Usage:**\n\n```swift\nimport Elementary\nimport ElementaryAlpine\nimport ElementaryAlpinePlugins\n\n// Persist a counter to localStorage\ndiv(.x.data(\"{ count: $persist(0) }\")) {\n    button(.x.on(\"click\", \"count++\")) { \"Increment\" }\n    span(.x.text(\"count\")) { \"\" }\n}\n\n// Use a custom localStorage key\ndiv(.x.data(\"{ count: $persist(0).as('my-count') }\")) {\n    button(.x.on(\"click\", \"count++\")) { \"Increment\" }\n    span(.x.text(\"count\")) { \"\" }\n}\n\n// Use sessionStorage instead (cleared when the tab closes)\ndiv(.x.data(\"{ count: $persist(0).using(sessionStorage) }\")) {\n    button(.x.on(\"click\", \"count++\")) { \"Increment\" }\n    span(.x.text(\"count\")) { \"\" }\n}\n```\n\n**Generated HTML:**\n\n```html\n\u003cdiv x-data=\"{ count: $persist(0) }\"\u003e\n    \u003cbutton x-on:click=\"count++\"\u003eIncrement\u003c/button\u003e\n    \u003cspan x-text=\"count\"\u003e\u003c/span\u003e\n\u003c/div\u003e\n\u003cdiv x-data=\"{ count: $persist(0).as('my-count') }\"\u003e\n    \u003cbutton x-on:click=\"count++\"\u003eIncrement\u003c/button\u003e\n    \u003cspan x-text=\"count\"\u003e\u003c/span\u003e\n\u003c/div\u003e\n\u003cdiv x-data=\"{ count: $persist(0).using(sessionStorage) }\"\u003e\n    \u003cbutton x-on:click=\"count++\"\u003eIncrement\u003c/button\u003e\n    \u003cspan x-text=\"count\"\u003e\u003c/span\u003e\n\u003c/div\u003e\n```\n\n**Notes:**\n- Persist is **not a directive**, so there is no `HTMLAttribute` helper. Write `$persist(...)` as a JS string in your `x-data` value.\n- `.as(...)` and `.using(...)` are **JavaScript method calls** on the `$persist(...)` return value, not HTML modifiers — they cannot be type-safe in Swift.\n- `$persist` works with primitives, arrays, and objects. If you change the type of a persisted value, clear its localStorage entry first.\n\n### Focus\n\n[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.\n\n**Usage:**\n\n```swift\nimport Elementary\nimport ElementaryAlpine\nimport ElementaryAlpinePlugins\n\n// Trap focus inside an element while `open` is true\ndiv(.x.data(\"{ open: false }\")) {\n    button(.x.on(\"click\", \"open = true\")) { \"Open Dialog\" }\n    span(.x.show(\"open\"), .xFocus.trap(\"open\")) {\n        p { \"...\" }\n        input(.type(.text), .placeholder(\"Some input...\"))\n        input(.type(.text), .placeholder(\"Some other input...\"))\n        button(.x.on(\"click\", \"open = false\")) { \"Close Dialog\" }\n    }\n}\n\n// Hide all other elements from screen readers while trapped\ndiv(.xFocus.trap(\"open\", modifiers: [.inert])) { ... }\n\n// Disable page scroll while trapped\ndiv(.xFocus.trap(\"open\", modifiers: [.noscroll])) { ... }\n\n// Don't return focus to the previous element on untrap\ndiv(.xFocus.trap(\"open\", modifiers: [.noreturn])) { ... }\n\n// Don't auto-focus the first focusable element on trap\ndiv(.xFocus.trap(\"open\", modifiers: [.noautofocus])) { ... }\n\n// Chained modifiers\ndiv(.xFocus.trap(\"open\", modifiers: [.inert, .noscroll, .noreturn])) { ... }\n```\n\n**Generated HTML:**\n\n```html\n\u003cdiv x-trap=\"open\"\u003e...\u003c/div\u003e\n\u003cdiv x-trap.inert=\"open\"\u003e...\u003c/div\u003e\n\u003cdiv x-trap.noscroll=\"open\"\u003e...\u003c/div\u003e\n\u003cdiv x-trap.noreturn=\"open\"\u003e...\u003c/div\u003e\n\u003cdiv x-trap.noautofocus=\"open\"\u003e...\u003c/div\u003e\n\u003cdiv x-trap.inert.noscroll.noreturn=\"open\"\u003e...\u003c/div\u003e\n```\n\n**Modifier reference:**\n\n| Modifier | Raw value | Notes |\n|----------|-----------|-------|\n| `.inert` | `inert` | Mark other page elements `aria-hidden=\"true\"` while trapped |\n| `.noscroll` | `noscroll` | Block page scrolling while trapped |\n| `.noreturn` | `noreturn` | Don't return focus on untrap |\n| `.noautofocus` | `noautofocus` | Don't auto-focus the first focusable element |\n\n**Notes:**\n- 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.\n- The Focus plugin was previously called \"Trap\" — `x-trap` and its modifiers are unchanged.\n\n### Collapse\n\n[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`.\n\n**Usage:**\n\n```swift\nimport Elementary\nimport ElementaryAlpine\nimport ElementaryAlpinePlugins\n\n// Basic collapse (works with x-show)\np(.x.show(\"expanded\"), .xCollapse.collapse()) {\n    \"...\"\n}\n\n// Custom duration (ms)\np(.x.show(\"expanded\"), .xCollapse.collapse(modifiers: [.duration(1000)])) {\n    \"...\"\n}\n\n// Minimum collapsed height (px) — useful for \"cut-off\" instead of full hide\np(.x.show(\"expanded\"), .xCollapse.collapse(modifiers: [.min(50)])) {\n    \"...\"\n}\n\n// Chained modifiers\np(.x.show(\"expanded\"), .xCollapse.collapse(modifiers: [.duration(500), .min(50)])) {\n    \"...\"\n}\n```\n\n**Generated HTML:**\n\n```html\n\u003cp x-show=\"expanded\" x-collapse\u003e...\u003c/p\u003e\n\u003cp x-show=\"expanded\" x-collapse.duration.1000ms\u003e...\u003c/p\u003e\n\u003cp x-show=\"expanded\" x-collapse.min.50px\u003e...\u003c/p\u003e\n\u003cp x-show=\"expanded\" x-collapse.duration.500ms.min.50px\u003e...\u003c/p\u003e\n```\n\n**Modifier reference:**\n\n| Modifier | Raw value | Notes |\n|----------|-----------|-------|\n| `.duration(Int)` | `duration.Nms` | Animation duration in milliseconds |\n| `.min(Int)` | `min.Npx` | Minimum collapsed height in pixels (cuts off rather than fully hides) |\n\n**Notes:**\n- `x-collapse` can only exist on an element that already has `x-show`. It animates the height property when `x-show` toggles visibility.\n- `x-collapse` has no value — it only accepts modifiers.\n\n### Anchor\n\n[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.\n\n**Usage:**\n\n```swift\nimport Elementary\nimport ElementaryAlpine\nimport ElementaryAlpinePlugins\n\n// Anchor below the button (default positioning)\ndiv(.x.data(\"{ open: false }\")) {\n    button(.x.ref(\"button\"), .x.on(\"click\", \"open = ! open\")) { \"Toggle\" }\n    div(.x.show(\"open\"), .xAnchor.anchor(\"$refs.button\")) {\n        \"Dropdown content\"\n    }\n}\n\n// Anchor below-right of the button\ndiv(.x.show(\"open\"), .xAnchor.anchor(\"$refs.button\", modifiers: [.bottomStart])) {\n    \"Dropdown content\"\n}\n\n// Use fixed positioning (escapes overflow:hidden containers)\ndiv(.x.show(\"open\"), .xAnchor.anchor(\"$refs.button\", modifiers: [.fixed])) {\n    \"Dropdown content\"\n}\n\n// Add an offset (px)\ndiv(.x.show(\"open\"), .xAnchor.anchor(\"$refs.button\", modifiers: [.offset(10)])) {\n    \"Dropdown content\"\n}\n\n// Prevent auto-flip when there's no room below\ndiv(.x.show(\"open\"), .xAnchor.anchor(\"$refs.button\", modifiers: [.noflip])) {\n    \"Dropdown content\"\n}\n\n// Apply positioning yourself via $anchor.x/$anchor.y in x-bind:style\ndiv(\n    .x.show(\"open\"),\n    .xAnchor.anchor(\"$refs.button\", modifiers: [.noStyle]),\n    .x.bindStyle(\"{ position: 'absolute', top: $anchor.y+'px', left: $anchor.x+'px' }\")\n) {\n    \"Dropdown content\"\n}\n\n// Anchor to an element by id\ndiv(.x.show(\"open\"), .xAnchor.anchor(\"document.getElementById('trigger')\")) {\n    \"Dropdown content\"\n}\n```\n\n**Generated HTML:**\n\n```html\n\u003cdiv x-anchor=\"$refs.button\"\u003eDropdown content\u003c/div\u003e\n\u003cdiv x-anchor.bottom-start=\"$refs.button\"\u003eDropdown content\u003c/div\u003e\n\u003cdiv x-anchor.fixed=\"$refs.button\"\u003eDropdown content\u003c/div\u003e\n\u003cdiv x-anchor.offset.10=\"$refs.button\"\u003eDropdown content\u003c/div\u003e\n\u003cdiv x-anchor.noflip=\"$refs.button\"\u003eDropdown content\u003c/div\u003e\n\u003cdiv x-anchor.no-style=\"$refs.button\" x-bind:style=\"...\"\u003eDropdown content\u003c/div\u003e\n\u003cdiv x-anchor=\"document.getElementById('trigger')\"\u003eDropdown content\u003c/div\u003e\n```\n\n**Positioning modifiers:**\n\n| Modifier | Raw value | Notes |\n|----------|-----------|-------|\n| `.top` | `top` | Above the reference, centered |\n| `.topStart` | `top-start` | Above the reference, aligned to the start |\n| `.topEnd` | `top-end` | Above the reference, aligned to the end |\n| `.bottom` | `bottom` | Below the reference, centered |\n| `.bottomStart` | `bottom-start` | Below the reference, aligned to the start |\n| `.bottomEnd` | `bottom-end` | Below the reference, aligned to the end |\n| `.left` | `left` | Left of the reference, centered |\n| `.leftStart` | `left-start` | Left of the reference, aligned to the start |\n| `.leftEnd` | `left-end` | Left of the reference, aligned to the end |\n| `.right` | `right` | Right of the reference, centered |\n| `.rightStart` | `right-start` | Right of the reference, aligned to the start |\n| `.rightEnd` | `right-end` | Right of the reference, aligned to the end |\n\n**Other modifiers:**\n\n| Modifier | Raw value | Notes |\n|----------|-----------|-------|\n| `.fixed` | `fixed` | Use `position: fixed` (escapes `overflow: hidden` containers) |\n| `.offset(Int)` | `offset.N` | Spacing in pixels between anchored and reference element |\n| `.noflip` | `noflip` | Don't auto-flip when there's no room in the chosen direction |\n| `.noStyle` | `no-style` | Don't apply positioning styles; access them via `$anchor.x` / `$anchor.y` in `x-bind:style` |\n\n**Notes:**\n- `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.\n- 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.\n\n### Sort\n\n[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.\n\n**Usage:**\n\n```swift\nimport Elementary\nimport ElementaryAlpine\nimport ElementaryAlpinePlugins\n\n// Basic sortable list\nul(.xSort.sort) {\n    li(.xSort.item(\"1\")) { \"foo\" }\n    li(.xSort.item(\"2\")) { \"bar\" }\n    li(.xSort.item(\"3\")) { \"baz\" }\n}\n\n// Sort with a handler that runs on every reorder\nul(.xSort.sort(\"alert($item + ' - ' + $position)\")) {\n    li(.xSort.item(\"1\")) { \"foo\" }\n    li(.xSort.item(\"2\")) { \"bar\" }\n    li(.xSort.item(\"3\")) { \"baz\" }\n}\n\n// Group sortable lists — items can be dragged between lists with the same group\nul(.xSort.sort(\"handle\"), .xSort.group(\"todos\")) {\n    li(.xSort.item(\"1\")) { \"foo\" }\n    li(.xSort.item(\"2\")) { \"bar\" }\n    li(.xSort.item(\"3\")) { \"baz\" }\n}\n\nol(.xSort.sort(\"handle\"), .xSort.group(\"todos\")) {\n    li(.xSort.item(\"4\")) { \"foo\" }\n    li(.xSort.item(\"5\")) { \"bar\" }\n    li(.xSort.item(\"6\")) { \"baz\" }\n}\n\n// Drag handles — only the handle initiates drag\nul(.xSort.sort) {\n    li(.xSort.item(\"1\")) {\n        span(.xSort.handle) { \" - \" }\n        \"foo\"\n    }\n    li(.xSort.item(\"2\")) {\n        span(.xSort.handle) { \" - \" }\n        \"bar\"\n    }\n}\n\n// Ignore elements — buttons inside items stay clickable\nul(.xSort.sort) {\n    li(.xSort.item(\"1\")) {\n        \"foo\"\n        button(.xSort.ignore) { \"Edit\" }\n    }\n}\n\n// Show a ghost of the dragged element instead of an empty space\nul(.xSort.sort(modifiers: [.ghost])) {\n    li(.xSort.item(\"1\")) { \"foo\" }\n    li(.xSort.item(\"2\")) { \"bar\" }\n}\n\n// Pass custom SortableJS options\nul(.xSort.sort, .xSort.config(\"{ animation: 0 }\")) {\n    li(.xSort.item(\"1\")) { \"foo\" }\n}\n```\n\n**Generated HTML:**\n\n```html\n\u003cul x-sort\u003e\n    \u003cli x-sort:item=\"1\"\u003efoo\u003c/li\u003e\n    \u003cli x-sort:item=\"2\"\u003ebar\u003c/li\u003e\n    \u003cli x-sort:item=\"3\"\u003ebaz\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cul x-sort=\"alert($item + ' - ' + $position)\"\u003e\n    \u003cli x-sort:item=\"1\"\u003efoo\u003c/li\u003e\n    \u003cli x-sort:item=\"2\"\u003ebar\u003c/li\u003e\n    \u003cli x-sort:item=\"3\"\u003ebaz\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cul x-sort=\"handle\" x-sort:group=\"todos\"\u003e\n    \u003cli x-sort:item=\"1\"\u003efoo\u003c/li\u003e\n    \u003cli x-sort:item=\"2\"\u003ebar\u003c/li\u003e\n    \u003cli x-sort:item=\"3\"\u003ebaz\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003col x-sort=\"handle\" x-sort:group=\"todos\"\u003e\n    \u003cli x-sort:item=\"4\"\u003efoo\u003c/li\u003e\n    \u003cli x-sort:item=\"5\"\u003ebar\u003c/li\u003e\n    \u003cli x-sort:item=\"6\"\u003ebaz\u003c/li\u003e\n\u003c/ol\u003e\n\n\u003cul x-sort\u003e\n    \u003cli x-sort:item=\"1\"\u003e\n        \u003cspan x-sort:handle\u003e - \u003c/span\u003efoo\n    \u003c/li\u003e\n    \u003cli x-sort:item=\"2\"\u003e\n        \u003cspan x-sort:handle\u003e - \u003c/span\u003ebar\n    \u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cul x-sort\u003e\n    \u003cli x-sort:item=\"1\"\u003e\n        foo\n        \u003cbutton x-sort:ignore\u003eEdit\u003c/button\u003e\n    \u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cul x-sort.ghost\u003e\n    \u003cli x-sort:item=\"1\"\u003efoo\u003c/li\u003e\n    \u003cli x-sort:item=\"2\"\u003ebar\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cul x-sort x-sort:config=\"{ animation: 0 }\"\u003e\n    \u003cli x-sort:item=\"1\"\u003efoo\u003c/li\u003e\n\u003c/ul\u003e\n```\n\n**Modifier reference:**\n\n| Modifier | Raw value | Notes |\n|----------|-----------|-------|\n| `.ghost` | `ghost` | Show a ghost of the dragged element in its place (default: empty hole) |\n\n**Notes:**\n- 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.\n- `x-sort:item` keys are typically numeric (`\"1\"`, `\"2\"`, …) but can be any string used to identify the item.\n- `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.\n- `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.\n- While dragging, Alpine adds a `.sorting` class to `\u003cbody\u003e` — useful for conditional CSS like `body.sorting #warning { display: block; }`.\n\n## Play with it\n\nExample apps will be added in a future release.\n\n## Documentation\n\nThe package ships two libraries:\n\n- **`ElementaryAlpine`** — core:\n  - **Attribute helpers** via the `.x` syntax on all `HTMLElements` for all 17 core [AlpineJS directives](https://alpinejs.dev/directives):\n    - `x-data`, `x-init` (`.setup`), `x-show`\n    - `x-bind` / `x-bind:class` / `x-bind:style`\n    - `x-on` with modifiers (base, keyboard, mouse, advanced)\n    - `x-text`, `x-html`, `x-model` with modifiers, `x-modelable`\n    - `x-for` (`.loop`), `x-transition` (all phases), `x-effect`, `x-ignore`, `x-ref`, `x-cloak`\n    - `x-teleport`, `x-if` (`.when`), `x-id`\n  - **Global helpers** — `registerGlobal(_:on:action:)` for `Alpine.data()`, `Alpine.store()`, `Alpine.bind()` (see [Globals](#globals))\n- **`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`).\n\n## Future directions\n\n- Remaining plugin wrapper: Morph (Alpine.morph() global — no directive surface)\n\nPRs welcome.\n\n## License\n\n[Apache 2.0](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famirsaam%2Felementary-alpine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Famirsaam%2Felementary-alpine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famirsaam%2Felementary-alpine/lists"}