Hologram is a full-stack isomorphic Elixir web framework. It compiles Elixir to JavaScript for the browser. It is NOT Phoenix LiveView - it has its own template syntax (~HOLO), component model, and state management. Do not use Phoenix/LiveView patterns. Read documentation before attempting to use its features. Do not assume that you have prior knowledge of the framework or its conventions.
For additional details beyond these rules, see deps/hologram/llms-full.txt or https://hologram.page/llms.txt
- Never use Phoenix/LiveView syntax: no
<%= %>, nophx-click, no<.component>, no<:slot>, noto_form, no<.simple_form>, nolive_redirect, nohandle_event, noassign. - Never use HEEx templates. Hologram uses
~HOLOsigil, not~H. - Actions use
put_state, notassign. State is accessed withcomponent.state.key, notsocket.assigns.key. - Commands use
serverstruct, notsocket. Return%Server{}, not{:noreply, socket}. - Cookie keys are strings (
"my_cookie"), session keys are atoms or strings (:user_id). Mixing these up causes errors. init/3for pages receives URL params, not props. Don't confuse with componentinit/3which receives props.- Stateless components cannot handle events. You need a
cidto make a component stateful. - The page cid is
"page", the layout cid is"layout". Don't forget these when targeting actions. - Not all Elixir standard library functions are available client-side yet. Check the Client Runtime reference for coverage.
- Hologram applications are built with two building blocks: Pages (route entry points) and Components (reusable UI elements).
- Actions run on the client (browser). Use them for state updates, navigation, and triggering commands.
- Commands run on the server. Use them for database access, API calls, session/cookie management, and other server-side operations.
- State lives in the browser, not on the server. This enables instant UI updates without network round-trips.
- Client-server communication happens automatically over HTTP/2 persistent connections. You never configure HTTP endpoints or write boilerplate for action-command interactions.
- Hologram automatically determines which code runs on the client vs server and compiles the client portions to JavaScript. You don't manually split code.
- Hologram templates use the
~HOLOsigil, not HEEx. Never use HEEx syntax (<%= %>,<.component>,<:slot>). - Access props and state with
@varsyntax:{@name},{@count}. - Interpolate Elixir expressions with curly braces:
{expression}. Not<%= expression %>. - Component nodes use module names:
<MyComponent prop="value" />. Not<.my_component>. - Conditional rendering uses
{%if condition}...{/if}and{%if condition}...{%else}...{/if}. Not:ifattribute. - Iteration uses
{%for item <- @items}...{/for}. Not:forattribute. - Escape curly braces with backslash:
\{literal\}. - Raw output (no processing):
{%raw}...{/raw}. - When an attribute expression evaluates to
nilorfalse, the attribute is not rendered at all. - All interpolated expressions are automatically HTML-escaped to prevent XSS.
- Components use
use Hologram.Component. Notuse Phoenix.Componentoruse Phoenix.LiveComponent. - Define props with
prop :name, :typeorprop :name, :type, default: value. - Available prop types:
:any,:atom,:boolean,:bitstring,:float,:function,:integer,:list,:map,:pid,:port,:reference,:string,:tuple. - Source props from context:
prop :user, :map, from_context: :current_user. - Stateful components require a
cidattribute:<MyComponent cid="my_id" />. Withoutcid, the component is stateless. - Server-side init uses
init/3(props, component, server). Client-side init usesinit/2(props, component). init/3can return aComponentstruct, aServerstruct, or a{component, server}tuple.- Both
init/3andinit/2are optional. - Use
<slot />for child content. Not<:slot>orinner_block. - Templates can be defined as a
template/0function with~HOLOsigil, or in a colocated.holofile (same name, same directory). - Colocated
.holofiles contain only markup, without the~HOLOsigil wrapper.
- Pages use
use Hologram.Page. Notuse Phoenix.LiveView. - Every page must define a route:
route "/path"orroute "/path/:param". - Every page must specify a layout:
layout MyApp.MainLayoutorlayout MyApp.MainLayout, prop: value. - Pages are always stateful and always initialized server-side with
init/3(params, component, server). init/3receives URL params, not props. Useparam :name, :typeto declare typed route parameters.- Supported param types:
:atom,:float,:integer,:string. - The page's component ID (cid) is always
"page". Usetarget: "page"to target actions at it. - Hologram uses a search tree router, not ordered routing. Static segments always match before parameterized ones. You cannot have two ambiguous parameterized routes at the same level (e.g.
/:usernameand/:post_slug) - use distinct prefixes instead.
- Layouts are regular components using
use Hologram.Component. There is no special layout module or macro. - A layout template must include
<Hologram.UI.Runtime />inside the<head>tag. - A layout template must include
<slot />where page content will be inserted. - The layout's component ID (cid) is always
"layout". Usetarget: "layout"to target actions at it. - Pass props to layouts via
layout MyApp.MainLayout, prop: valueor viaput_state/2in the page'sinit/3.
- Bind events with
$prefix:$click,$change,$submit,$blur,$focus,$mouse_move,$pointer_down,$pointer_up,$pointer_move,$pointer_cancel,$select,$transition_end,$transition_start,$transition_run,$transition_cancel. Notphx-clickorphx-change. - Text syntax (actions only):
$click="my_action". - Shorthand with params (actions only):
$click={:my_action, key: value}. - Longhand (actions or commands):
$click={action: :my_action, target: "cid", params: %{key: value}}. - Trigger commands with longhand:
$click={command: :my_command, params: %{key: value}}. - Delays (actions only):
$click={action: :my_action, delay: 1000}. - Event data is available in
params.eventinside the action/command handler. $changeon an input fires on every keystroke (text inputs) or on selection change (checkboxes, radios, selects). On a form element, it fires on field blur.- Valid targets:
"page","layout", or a component's cid string. Default is the containing stateful component.
- Actions are client-side. Define with
def action(name, params, component). Nothandle_event. - Actions must return a
%Component{}struct. Chain operations with|>. - Update state:
put_state(component, :key, value)orput_state(component, key: val1, key2: val2). - Nested state update:
put_state(component, [:path, :key], value). - Trigger a command:
put_command(component, :cmd_name)orput_command(component, :cmd_name, param: value). - Navigate:
put_page(component, PageModule)orput_page(component, PageModule, param: value). - Update context:
put_context(component, :key, value). - Chain another action:
put_action(component, :action_name)orput_action(component, :action_name, param: value). - Delays are available for actions only (not commands):
put_action(component, name: :my_action, delay: 750).
- Commands are server-side. Define with
def command(name, params, server). Nothandle_eventorhandle_info. - Commands must return a
%Server{}struct. Chain operations with|>. - Commands are always executed asynchronously.
- Trigger a client action from a command:
put_action(server, :action_name)orput_action(server, :action_name, param: value). - Manage session:
put_session(server, :key, value),get_session(server, :key),delete_session(server, :key). - Manage cookies:
put_cookie(server, "key", value),get_cookie(server, "key"),delete_cookie(server, "key"). - Commands can be triggered from templates via longhand event syntax or from actions via
put_command/2/put_command/3.
- Use
Hologram.UI.Linkfor navigation links:<Link to={MyPage}>text</Link>. Not<.link navigate={...}>orlive_redirect. - With params:
<Link to={MyPage, id: 123}>text</Link>. - Programmatic navigation from actions:
put_page(component, MyPage)orput_page(component, MyPage, id: 123). - Hologram prefetches pages on
$pointer_downfor near-instant transitions. - Each page is loaded fresh from the server. Browser history (back/forward) works automatically.
- Use standard HTML
<form>,<input>,<select>,<textarea>elements. Not Phoenix form helpers (to_form,<.simple_form>,<.input>). - Synchronized inputs use
value={@state_var}+$change="handler"on the input element. The component state is the single source of truth. - Non-synchronized inputs omit
$changeon the input. Access values via form-level$changeor$submithandlers fromparams.event. - Text inputs and textareas sync with
valueattribute. Checkboxes and radio buttons sync withcheckedattribute. - Input-level
$changeon text inputs fires on every keystroke. Form-level$changefires on field blur. $submitevent data contains all form field values as a map:params.event=>%{field_name: value}.- Elixir validation code (including Ecto changesets) runs both client-side and server-side since Hologram runs Elixir in the browser.
- Context shares data down the component tree without prop drilling. Not a global store.
- Set context:
put_context(component, :key, value)in actions or init functions. - Namespaced keys to avoid conflicts:
put_context(component, {MyModule, :key}, value). - Access context via props:
prop :user, :map, from_context: :current_user. - Context values are available to all descendant components, not siblings or ancestors.
- Prefer props for data passed to direct children. Use context for deeply nested data sharing.
- Session is server-side secure storage. Use it in
init/3and commands only. - Read:
get_session(server, :key)orget_session(server, :key, default). - Write:
put_session(server, :key, value). - Delete:
delete_session(server, :key). - Session keys must be atoms or strings.
- Sessions can store any Elixir data type (maps, lists, tuples, etc.).
- Session data cannot be read by client-side code. Use cookies if you need client-side access.
- Cookies are managed server-side. Use them in
init/3and commands only. - Cookie keys must be strings (not atoms).
- Read:
get_cookie(server, "key")orget_cookie(server, "key", default). - Write:
put_cookie(server, "key", value)orput_cookie(server, "key", value, opts). - Delete:
delete_cookie(server, "key"). - Cookies can store complex Elixir data structures (maps, lists, etc.) - Hologram handles encoding/decoding automatically.
- Default cookie options:
http_only: true,path: "/",same_site: :lax,secure: true. - Custom options:
http_only,path,same_site(:strict,:lax,:none),secure,max_age,domain. - Use sessions for sensitive data. Use cookies when you need client-side access or specific cookie behavior.
- Add
use Hologram.JSto any module that needs JS interop. Not Phoenix hooks orphx-hook. - Import JS modules:
js_import from: "decimal.js", as: :Decimal(default export) orjs_import :multiply, from: "./helpers.mjs"(named export). - Relative paths (
./,../) resolve relative to the Elixir source file. Bare specifiers resolve as npm packages. - Call a function:
JS.call(:multiply, [4, 6]). Call a method:JS.call(:Math, :round, [3.7]). - Instantiate a class:
JS.new(:Calculator, [10]). Chain with|>::Calculator |> JS.new([10]) |> JS.call(:add, [5]). - Get/set properties:
JS.get(obj, :value),JS.set(obj, :value, 20). - Evaluate JS:
JS.eval("3 + 4"). Execute JS:JS.exec("const x = 2; return x + 3;"). Inline JS:~JS"""...""". - Async: JS Promises become Elixir Tasks. Use
Task.await/1to get the result. - Dispatch actions from JS:
Hologram.dispatchAction("action_name", "page", {key: value}). - Dispatch DOM events from Elixir:
JS.dispatch_event(target, "my:event", detail: %{value: 42}). - Elixir anonymous functions can be passed as JS callbacks.
- JS interop only works in action handlers (client-side). It is a no-op during server-side rendering.
- Prefer
JS.calloverJS.exec/JS.eval. Isolate JS interop behind facade modules.