Packages
phoenix_live_view
1.2.1
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.3
1.2.0-rc.2
1.2.0-rc.1
1.2.0-rc.0
1.1.32
1.1.31
1.1.30
1.1.29
1.1.28
1.1.27
1.1.26
1.1.25
1.1.24
1.1.23
1.1.22
1.1.21
1.1.20
1.1.19
1.1.18
1.1.17
1.1.16
1.1.15
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.1.0-rc.4
1.1.0-rc.3
1.1.0-rc.2
1.1.0-rc.1
1.1.0-rc.0
1.0.18
1.0.17
1.0.16
1.0.15
1.0.14
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.8
retired
1.0.7
1.0.6
retired
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc.9
1.0.0-rc.8
1.0.0-rc.7
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.18
0.18.17
0.18.16
0.18.15
0.18.14
0.18.13
0.18.12
0.18.11
0.18.10
0.18.9
0.18.8
0.18.7
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.14
0.17.13
0.17.12
0.17.11
0.17.10
0.17.9
0.17.8
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.6.0-dev
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
Rich, real-time user experiences with server-rendered HTML
Current section
Files
Jump to
Current section
Files
lib/phoenix_live_view/colocated_css.ex
defmodule Phoenix.LiveView.ColocatedCSS do
@moduledoc ~S'''
Building blocks for a special HEEx `:type` that extracts any CSS styles
from a colocated `<style>` tag at compile time.
To actually use `ColocatedCSS`, you must define a module including `use Phoenix.LiveView.ColocatedCSS`
and implement the `ColocatedCSS` behaviour.
Note: To use `ColocatedCSS`, you need to run Phoenix 1.8+.
Note: `ColocatedCSS` **must** be defined at the very beginning of the template in which it is used.
Colocated CSS uses the same folder structures as Colocated JS. See `Phoenix.LiveView.ColocatedJS` for more information.
### Using with a bundler
Colocated CSS is meant to be used with a bundler like `:tailwind` or `:esbuild`.
<!-- tabs-open -->
### Tailwind
The following setup requires Tailwind v4.2.3 or newer.
To use colocated CSS with `:tailwind`, it has to be configured to resolve the `phoenix-colocated` folder.
For new Phoenix applications starting with version `1.8.8`, this is already configured by default in your `config.exs`:
config :tailwind,
...,
my_app: [
args: ~w(
--input=assets/css/app.css
--output=priv/static/assets/css/app.css
),
cd: Path.expand("..", __DIR__),
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
]
Then, import the `colocated.css` file in your `app.css` file:
```diff
@import "tailwindcss" source(none);
+ @import "phoenix-colocated/my_app/colocated.css";
@source "../css";
@source "../js";
@source "../../lib/my_app_web";
+ /* Required for Tailwind to automatically pick up changes in colocated CSS files in dev */
+ @source "../../_build/dev/phoenix-colocated/my_app/*/";
```
### Esbuild
To use colocated CSS with `:esbuild`, ensure that the `NODE_PATH` is configured to include `Mix.Project.build_path()`:
config :esbuild,
...
my_app: [
args:
~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
cd: Path.expand("../assets", __DIR__),
env: %{
"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]
}
]
This is the same setup as for `Phoenix.LiveView.ColocatedJS`. Then, import it like this in your `app.js` file:
```javascript
import "phoenix-colocated/my_app/colocated.css"
```
Importing CSS in your `app.js` file will cause esbuild to generate a separate `app.css` file.
To load it, simply add a second `<link>` to your `root.html.heex` file, like so:
```html
<link phx-track-static rel="stylesheet" href={~p"/assets/js/app.css"} />
```
<!-- tabs-close -->
## Global CSS
If all you need is global CSS, which is extracted as is, you can define your ColocatedCSS module like this:
```elixir
defmodule MyAppWeb.ColocatedCSS do
use Phoenix.LiveView.ColocatedCSS
@impl true
def transform("style", _attrs, css, _meta) do
{:ok, css, []}
end
end
```
## Scoped CSS
The idea behind scoped CSS is to restrict the elements that CSS rules apply to
to only the elements of the current template / component.
One way to scope CSS is to use [CSS `@scope` rules](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@scope).
A scoped `ColocatedCSS` module using CSS `@scope` can be implemented like this:
```elixir
defmodule MyAppWeb.ColocatedScopedCSS do
use Phoenix.LiveView.ColocatedCSS
@impl true
def transform("style", attrs, css, meta) do
validate_opts!(attrs)
{scope, css} = do_scope(css, attrs, meta)
{:ok, css, [root_tag_attribute: {"phx-css-#{scope}", true}]}
end
defp validate_opts!(opts) do
Enum.each(opts, fn {key, val} -> validate_opt!({key, val}, Map.delete(opts, key)) end)
end
defp validate_opt!({"lower-bound", val}, _other_opts) when val in ["inclusive", "exclusive"] do
:ok
end
defp validate_opt!({"lower-bound", val}, _other_opts) do
raise ArgumentError,
~s|expected "inclusive" or "exclusive" for the `lower-bound` attribute of colocated css, got: #{inspect(val)}|
end
defp validate_opt!(_opt, _other_opts), do: :ok
defp do_scope(css, opts, meta) do
scope = hash("#{meta.module}_#{meta.line}: #{css}")
root_tag_attribute = root_tag_attribute()
upper_bound_selector = ~s|[phx-css-#{scope}]|
lower_bound_selector = ~s|[#{root_tag_attribute}]|
lower_bound_selector =
case opts do
%{"lower-bound" => "inclusive"} -> lower_bound_selector <> " > *"
_ -> lower_bound_selector
end
css = "@scope (#{upper_bound_selector}) to (#{lower_bound_selector}) { #{css} }"
{scope, css}
end
defp hash(string) do
# It is important that we do not pad
# the Base32 encoded value as we use it in
# an HTML attribute name and = (the padding character)
# is not valid.
string
|> then(&:crypto.hash(:md5, &1))
|> Base.encode32(case: :lower, padding: false)
end
defp root_tag_attribute() do
case Application.get_env(:phoenix_live_view, :root_tag_attribute) do
configured_attribute when is_binary(configured_attribute) ->
configured_attribute
configured_attribute ->
message = """
a global :root_tag_attribute must be configured to use scoped css
Expected global :root_tag_attribute to be a string, got: #{inspect(configured_attribute)}
The global :root_tag_attribute is usually configured to `"phx-r"`, but it needs to be explicitly enabled in your configuration:
config :phoenix_live_view, root_tag_attribute: "phx-r"
You can also use a different value than `"phx-r"`.
"""
raise ArgumentError, message
end
end
end
```
This module transforms a given style tag like
```heex
<%!-- Note that :type accepts aliases as well! --%>
<style :type={MyAppWeb.ColocatedScopedCSS}>
.my-class { color: red; }
</style>
```
into
```css
@scope ([phx-css-abc123]) to ([phx-r]) {
.my-class { color: red; }
}
```
and if `lower-bound` is set to `inclusive`, it transforms it into
```css
@scope ([phx-css-abc123]) to ([phx-r] > *) {
.my-class { color: red; }
}
```
This applies any styles defined in the colocated CSS block to any element between a local root and a component.
It relies on LiveView's global `:root_tag_attribute`, which is an attribute that LiveView adds to all root tags,
no matter if colocated CSS is used or not. When the browser encounters a `phx-r` attribute, which in this case
is assumed to be the configured global `:root_tag_attribute`, it stops the scoped CSS rule.
Another way to implement scoped CSS could be to use PostCSS and apply an attribute to all tags in a template.
'''
@doc """
Callback invoked for each colocated CSS tag.
The callback receives the tag name, the string attributes and a map of metadata.
For example, for the following tag:
```heex
<style :type={MyAppWeb.ColocatedCSS} data-scope="my-scope" foo={@bar}>
.my-class { color: red; }
</style>
```
The callback would receive the following arguments:
* tag_name: `"style"`
* attrs: %{"data-scope" => "my-scope"}
* meta: `%{file: "path/to/file.ex", module: MyApp.MyModule, line: 10}`
The callback must return either `{:ok, scoped_css, directives}` or `{:error, reason}`.
If an error is returned, it will be logged and the CSS will not be extracted.
The `directives` needs to be a keyword list that supports the following options:
* `root_tag_attribute`: A `{key, value}` tuple that will be added as
an attribute to all "root tags" of the template defining the scoped CSS tag.
See the section on root tags below for more information.
* `tag_attribute`: A `{key, value}` tuple that will be added as an attribute to
all HTML tags in the template defining the scoped CSS tag.
## Root tags
In a HEEx template, all outermost tags are considered "root tags" and are
affected by the `root_tag_attribute` directive. If a template uses components,
the slots of those components are considered as root tags as well.
Here's an example showing which elements would be considered root tags:
```heex
<div> <---- root tag
<span>Hello</span> <---- not a root tag
<.my_component>
<p>World</p> <---- root tag
</.my_component>
</div>
<.my_component>
<span>World</span> <---- root tag
<:a_named_slot>
<div> <---- root tag
Foo
<p>Bar</p> <---- not a root tag
</div>
</:a_named_slot>
</.my_component>
```
"""
@callback transform(tag_name :: binary(), attrs :: map(), css :: binary(), meta :: map()) ::
{:ok, binary(), keyword()} | {:error, term()}
defmacro __using__(_) do
# implements the MacroComponent behaviour
# but we don't add @behaviour to prevent users to need to differentiate
# @impl true for the ColocatedCSS behaviour itself
quote do
@behaviour unquote(__MODULE__)
def transform(ast, meta) do
Phoenix.LiveView.ColocatedCSS.__transform__(ast, meta, __MODULE__)
end
end
end
@behaviour Phoenix.LiveView.ColocatedAssets
@doc false
def __transform__({"style", attributes, [text_content], _tag_meta} = _ast, meta, module) do
validate_phx_version!()
opts = Map.new(attributes)
case extract(opts, text_content, meta, module) do
{data, directives} ->
# we always drop colocated CSS from the rendered output
{:ok, "", data, directives}
nil ->
{:ok, ""}
end
end
def __transform__(_ast, _meta, _module) do
raise ArgumentError, "ColocatedCSS can only be used on style tags"
end
defp validate_phx_version! do
phoenix_version = to_string(Application.spec(:phoenix, :vsn))
if not Version.match?(phoenix_version, "~> 1.8.0") do
raise ArgumentError, ~s|ColocatedCSS requires at least {:phoenix, "~> 1.8.0"}|
end
end
defp extract(opts, text_content, meta, module) do
transform_meta = %{
module: meta.env.module,
file: meta.env.file,
line: meta.env.line
}
case module.transform("style", opts, text_content, transform_meta) do
{:ok, styles, directives} when is_binary(styles) and is_list(directives) ->
filename = "#{meta.env.line}_#{hash(styles)}.css"
data =
Phoenix.LiveView.ColocatedAssets.extract(
__MODULE__,
meta.env.module,
filename,
styles,
nil
)
{data, directives}
{:error, reason} ->
IO.warn(
"ColocatedCSS module #{inspect(module)} returned an error, skipping: #{inspect(reason)}"
)
nil
other ->
raise ArgumentError,
"expected the ColocatedCSS implementation to return {:ok, scoped_css, directives} or {:error, term}, got: #{inspect(other)}"
end
end
defp hash(string) do
string
|> then(&:crypto.hash(:md5, &1))
|> Base.encode32(case: :lower, padding: false)
end
@impl Phoenix.LiveView.ColocatedAssets
def build_manifests(files) do
if files == [] do
[{"colocated.css", ""}]
else
[
{"colocated.css",
Enum.reduce(files, [], fn %{relative_path: file}, acc ->
line = ~s[@import "./#{file}";\n]
[acc | line]
end)}
]
end
end
end