# `attr` turns component bugs into compile errors

A function component is just a function taking `assigns`, which means a typo'd or forgotten assign fails the way any missing map key fails: at render time, in production, on whichever page happens to call it wrong. `attr` and `slot` move that check to compile time, and they check the *call site* — the place that actually has the bug.

```elixir
defmodule MyAppWeb.Components do
  use Phoenix.Component

  attr :user, :map, required: true
  attr :size, :string, default: "md", values: ~w(sm md lg)
  slot :actions

  def user_card(assigns) do
    ~H"""
    <div class={"card card-#{@size}"}>
      <span>{@user.name}</span>
      <div :for={action <- @actions}>{render_slot(action)}</div>
    </div>
    """
  end
end
```

Call it wrong and the compiler tells you before you ever load the page:

```heex
<.user_card size="xl" />
```

```
warning: missing required attribute "user" for component MyAppWeb.Components.user_card/1
warning: attribute "size" in component MyAppWeb.Components.user_card/1 must be one of ["sm", "md", "lg"], got: "xl"
```

Both warnings point at the caller's line, not at the component. That is the whole value: the component is fine, the call site is wrong, and the error names the call site.

## What each option buys you

`required: true` catches the forgotten assign. `default:` removes the `assigns[:size] || "md"` dance from the body — the default is applied before your function runs, so `@size` is always set. `values:` is the one people miss: it turns a free-form string into a closed set, so a renamed variant produces warnings at every stale call site instead of silently rendering a `card-xl` class that no CSS defines.

Typing the attribute as a struct tightens it further:

```elixir
attr :user, MyApp.Accounts.User, required: true
attr :on_close, :any, default: nil
attr :rest, :global, include: ~w(form target)
```

`:global` is what lets callers pass `phx-click`, `data-*`, or `aria-*` straight through to your root element via `{@rest}` without you declaring each one — and `include:` extends the allowed set for attributes Phoenix does not treat as global by default.

## Slots get the same treatment

```elixir
slot :inner_block, required: true

slot :column, doc: "a table column" do
  attr :label, :string, required: true
  attr :sortable, :boolean
end
```

Attributes declared inside a `slot` block are validated on each slot entry, so `<:column>` without a `label` reports the same way:

```
warning: missing required attribute "label" in slot "column" for component MyAppWeb.Components.table/1
```

There is a second payoff beyond correctness. Because these declarations are introspectable, `mix docs` renders a full attribute table for every component automatically — the documentation stops being a thing you maintain by hand and starts being a thing you cannot forget to update.

The migration is incremental. Adding `attr` to one component changes nothing for the others, and each one you annotate immediately starts reporting its own bad call sites.

[`Phoenix.Component.attr/3` docs](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#attr/3)


---

Created by: almirsarajcic
Date: August 22, 2026
URL: https://elixirdrops.net/d/tEmaCWdG
