{"id":50202603,"url":"https://github.com/alexberce/shift","last_synced_at":"2026-06-01T05:00:32.586Z","repository":{"id":360270928,"uuid":"1249115378","full_name":"alexberce/shift","owner":"alexberce","description":"Animations for Phoenix LiveView, that just work. Real spring physics, smart defaults, one component.","archived":false,"fork":false,"pushed_at":"2026-05-25T22:54:20.000Z","size":66,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-30T03:04:48.255Z","etag":null,"topics":["animation","elixir","hex","liveview","phoenix","phoenix-liveview"],"latest_commit_sha":null,"homepage":"https://shift.devaccent.com","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexberce.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":null,"dco":null,"cla":null}},"created_at":"2026-05-25T11:16:06.000Z","updated_at":"2026-05-25T22:54:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/alexberce/shift","commit_stats":null,"previous_names":["alexberce/shift"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/alexberce/shift","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexberce%2Fshift","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexberce%2Fshift/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexberce%2Fshift/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexberce%2Fshift/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexberce","download_url":"https://codeload.github.com/alexberce/shift/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexberce%2Fshift/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33718446,"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-05-31T02:00:06.040Z","response_time":95,"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":["animation","elixir","hex","liveview","phoenix","phoenix-liveview"],"created_at":"2026-05-25T23:03:13.994Z","updated_at":"2026-05-31T04:00:35.239Z","avatar_url":"https://github.com/alexberce.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Shift\n\n[![Hex.pm](https://img.shields.io/hexpm/v/shift.svg)](https://hex.pm/packages/shift)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-blueviolet)](https://hexdocs.pm/shift)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\n\u003e Animations for Phoenix LiveView, that just work.\n\nOne `\u003c.animated\u003e` component. Real spring physics. Smart defaults that read\nyour intent and fill in the rest. Animate enter, exit, position and size\nstraight from HEEx.\n\n## Install\n\n```elixir\ndef deps do\n  [\n    {:shift, \"~\u003e 0.1\"}\n  ]\nend\n```\n\nIn `assets/js/app.js`:\n\n```js\nimport { init as initShift } from \"../../deps/shift/assets/js/shift.js\"\ninitShift()\n```\n\nThen `import Shift` wherever you want `\u003c.animated\u003e` available — usually once\nin your `MyAppWeb.html_helpers/0`.\n\n## What you get\n\nEverything below runs through the same `\u003c.animated\u003e` component. You declare\nwhat you want; Shift figures out the rest.\n\n**Enter \u0026 exit.** `initial` is the state before entering, `exit` is the state\nwhen LiveView removes the element. Opacity fade is added for free.\n\n```heex\n\u003c.animated :if={@open} initial={%{scale: 0.9}} exit={%{scale: 0.95}}\u003e\n  Modal contents\n\u003c/.animated\u003e\n```\n\n**Smart-resolved targets.** Mention a property in `initial`, leave it out of\n`animate` — Shift fills the target with its natural resting value.\n\n```heex\n\u003c.animated initial={%{y: 16}}\u003e\n  Slides up from y=16 to y=0 (auto-resolved). Opacity fades 0 -\u003e 1 (default).\n\u003c/.animated\u003e\n```\n\n**Real springs, not easings.** A solver runs the ODE and bakes the motion\ninto keyframes. Interrupt, overshoot, settle — it all behaves like a\nphysical object.\n\n```heex\n\u003c.animated\n  initial={%{scale: 0.9}}\n  transition={%{type: :spring, stiffness: 260, damping: 20}}\n\u003e\n  ...\n\u003c/.animated\u003e\n```\n\n**Layout animations (FLIP), automatic.** When an element's position changes\nbetween renders, Shift animates it from the old position to the new one. No\nopt-in needed.\n\n```heex\n\u003c.animated :for={item \u003c- @sorted_items} id={item.id}\u003e\n  {item.label}\n\u003c/.animated\u003e\n```\n\n**Height \u0026 width animations.** Animate from `height: 0` and Shift handles\nthe rest — padding/margin/border collapse with the box so accordions don't\nplateau, `overflow: hidden` is held throughout so content doesn't spill.\n\n```heex\n\u003c.animated :if={@open} initial={%{height: 0}} exit={%{height: 0}}\u003e\n  Expanding panel of any height.\n\u003c/.animated\u003e\n```\n\n**Any HTML tag.** Default is `\u003cdiv\u003e`, but pass `as=` to render anything —\nuseful for animating list items, inline text, semantic sections.\n\n```heex\n\u003cul\u003e\n  \u003c.animated :for={item \u003c- @items} as=\"li\" id={item.id} initial={%{y: 8}}\u003e\n    {item.label}\n  \u003c/.animated\u003e\n\u003c/ul\u003e\n```\n\n## `\u003c.animated\u003e` attributes\n\n| Attribute    | Type   | Default | Purpose                                                                                         |\n| ------------ | ------ | ------- | ----------------------------------------------------------------------------------------------- |\n| `as`         | string | `\"div\"` | HTML tag to render — any valid element name (`\"li\"`, `\"span\"`, `\"section\"`, ...).               |\n| `initial`    | map    | `nil`   | Style values applied before the enter animation. Drives the enter \"from\".                       |\n| `animate`    | map    | `nil`   | Target style values for enter. Auto-resolved from `initial` if omitted.                         |\n| `exit`       | map    | `nil`   | Style values to animate to when LiveView removes the element.                                   |\n| `transition` | map    | `%{}`   | Tween: `%{duration: s, delay: s, easing: \"ease-in-out\"}` — `easing` is any CSS easing string. Spring: `%{type: :spring, stiffness: 260, damping: 20, mass: 1}`. |\n| `disable`    | list   | `[]`    | Opt out of inferred behaviors: `:fade`, `:position`, `:size`.                                   |\n| `class`      | string | `nil`   | Standard HTML class attribute.                                                                  |\n\nAll other HTML attributes (`id`, `data-*`, `aria-*`, ...) pass through to\nthe underlying `\u003cdiv\u003e` via `:global`.\n\n### Transform shorthands\n\nInside any of the value maps you can use shorthand names for common CSS\ntransform functions. They compose into a single CSS `transform` string:\n\n| Shorthand                       | Unit | Becomes                  |\n| ------------------------------- | ---- | ------------------------ |\n| `x`, `y`, `z`                   | px   | `translateX/Y/Z(Npx)`    |\n| `scale`, `scaleX`, `scaleY`     | —    | `scale(N)` / `scaleX(N)` |\n| `rotate`, `rotateX`, `rotateY`  | deg  | `rotate(Ndeg)` etc.      |\n| `skewX`, `skewY`                | deg  | `skewX/Y(Ndeg)`          |\n\nCSS properties (`opacity`, `background-color`, `height`, ...) work as-is.\nBare numbers on layout properties (`height`, `width`, `padding-*`,\n`margin-*`, `border-*-width`) get a `px` suffix.\n\n## How it works\n\nA single `MutationObserver` watches the document. Every element with a\n`data-shift` attribute is tracked through three lifecycle phases:\n\n- **Enter** — when the element first appears (initial render or LiveView\n  patch). Reads `initial` / `animate` from the spec and plays the\n  transition.\n- **Exit** — when LiveView removes the element. Triggered by a `shift:exit`\n  event dispatched from `phx-remove`; LiveView keeps the node alive for the\n  exit duration before actually removing it.\n- **Layout** — between renders, if an element's position or size changed, a\n  FLIP / size animation runs automatically. Opt out per-element with\n  `disable={[:position]}` / `disable={[:size]}`.\n\nThere's some additional finesse for tricky cases — cascading exits stay in\norder even when morphdom shuffles kept-alive nodes, sliding-window stacks\nlift exits out of flow to avoid a phantom slot, and `overflow: hidden` is\nheld through size animations so content doesn't spill mid-grow. None of\nthis surfaces as API; it just works.\n\n## Requirements\n\n- Elixir ~\u003e 1.15\n- Phoenix LiveView ~\u003e 1.0\n- Modern browser with the Web Animations API (every browser since 2020)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexberce%2Fshift","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexberce%2Fshift","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexberce%2Fshift/lists"}