Surface-UI: Passing slots through to a child component


I have been working with Surface for a little while and have been pretty thrilled with the experience and its ease of use, however, I have recently run across a use case whose implementation is unclear to me.

I have grabbed the example Table component from the Surface-UI docs to work with in my side project and its worked great.

However, I’d like to render a bunch of tables which all share the same formatting but can continue accept a <:cols> slot, so that I can individually specify the content for each, using the same <Col> component as the original table.

So here is my first attempt which does not work, but I’m hoping that someone could help me understand how this could work:

defmodule OOTPUtilityWeb.Components.Player.Attributes.Table do
  use Surface.Component

  alias OOTPUtilityWeb.Components.Shared.Table

  slot cols, args: [item: ^data], required: true
  prop attributes, :keyword, required: true
  prop id, :string, required: true

  def render(assigns) do
      <Table id={@id} data={{name, ratings} <- @attributes} header_class={&header_class/2} column_class={&column_class/2}>
          <#cols />

  def header_class(_standing, 0) do

  def header_class(_col, _index),
    do: do_header_class([])

  def do_header_class(extra_classes \\ []) do
    extra_classes ++ [


  def column_class(_standing, 0) do

  def column_class(_standing, _index) do

  defp do_column_class(extra_classes) do
    Enum.join(extra_classes ++ [ "p-px", "md:p-1", "whitespace-nowrap" ], " ")

and then try to use it in a way like this:

defmodule OOTPUtilityWeb.Components.Player.Attributes.Pitches do
  use Surface.Component

  alias OOTPUtilityWeb.Components.Player.Attributes.Table
  alias OOTPUtilityWeb.Components.Shared.Table.Column

  prop pitches, :keyword, required: true

  def render(assigns) do
      <Table id={"player-pitch-ratings"} attributes={@pitches}>
        <Column label={"Pitches"}>

        <Column label="Ability">
          {as_number(Keyword.get(ratings, :ability))}

        <Column label="Talent">
          {as_number(Keyword.get(ratings, :talent))}

  def attribute_name(attribute) do

  def as_number(rating) do
    assigns = %{rating: rating}

      <span class={"text-rating-#{@rating * 2}"}>{@rating}</span>

But I see the following compilation error:

== Compilation error in file lib/ootp_utility_web/components/player/attributes/table.ex ==
** (FunctionClauseError) no function clause matching in Code.ensure_compiled/1

    The following arguments were given to Code.ensure_compiled/1:

        # 1
        {:cols, [line: 1], nil}

    Attempted function clauses (showing 1 out of 1):

        def ensure_compiled(module) when is_atom(module)

    (elixir 1.13.2) lib/code.ex:1519: Code.ensure_compiled/1
    (surface 0.7.0) lib/surface/compiler/helpers.ex:236: Surface.Compiler.Helpers.check_module_loaded/2
    (surface 0.7.0) lib/surface/compiler/helpers.ex:260: Surface.Compiler.Helpers.validate_component_module/2
    (surface 0.7.0) lib/surface/compiler.ex:677: Surface.Compiler.convert_node_to_ast/3
    (surface 0.7.0) lib/surface/compiler.ex:198: anonymous fn/3 in Surface.Compiler.to_ast/2
    (elixir 1.13.2) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
    (surface 0.7.0) lib/surface/compiler.ex:197: Surface.Compiler.to_ast/2
    (surface 0.7.0) lib/surface/compiler.ex:433: Surface.Compiler.convert_node_to_ast/3

since this is a compiler error, I’m wondering if this is why Surface.quote_surface/2 exists, but TBH I’m still struggling to understand how it might help me if it did.

If anyone could help me to at least understand why Surface seems to hate this so much or how I might leverage Surface.quote_surface/2 to fix this I’d be grateful.

Thank you,


You’re not passing it to the cols slot. Use the default <#slot /> in your inner table instead.

Or put the columns inside a <:cols> in your pitches table, but that seems unnecessary in this example.