Docs

Cinder UI

Recipes

Composed LiveView/Phoenix examples built from existing Cinder UI components. These are copyable blocks, not new primitives.

Auth form

A compact sign-in flow using card, field, input, and button composition.

Recipe
Sign in
Use your workspace account to continue.
  <.card class="mx-auto w-full max-w-sm">
  <.card_header>
    <.card_title>Sign in</.card_title>
    <.card_description>Use your workspace account to continue.</.card_description>
  </.card_header>
  <.card_content>
    <.form for={@form} phx-submit="sign_in" class="grid gap-4">
      <.field>
        <:label for="user_email">Email</:label>
        <.input id="user_email" field={@form[:email]} type="email" placeholder="team@example.com" />
      </.field>
      <.field>
        <:label for="user_password">Password</:label>
        <.input id="user_password" field={@form[:password]} type="password" />
      </.field>
      <.button class="w-full">Sign in</.button>
    </.form>
  </.card_content>
</.card>

Settings page

A LiveView-friendly settings layout with server-rendered form controls.

Recipe
Workspace settings
Manage defaults for new members.

Send a Monday summary to workspace admins.

  <div class="grid gap-6 lg:grid-cols-[12rem_1fr]">
  <nav class="flex flex-col gap-1 text-sm">
    <.button variant={:ghost} class="justify-start">Profile</.button>
    <.button variant={:ghost} class="justify-start">Notifications</.button>
    <.button variant={:ghost} class="justify-start">Billing</.button>
  </nav>

  <.card>
    <.card_header>
      <.card_title>Workspace settings</.card_title>
      <.card_description>Manage defaults for new members.</.card_description>
    </.card_header>
    <.card_content class="grid gap-5">
      <.field>
        <:label for="workspace_name">Workspace name</:label>
        <.input id="workspace_name" name="workspace[name]" value="Acme Operations" />
      </.field>
      <.field>
        <:label for="workspace_region">Default region</:label>
        <.select id="workspace_region" name="workspace[region]" value="au">
          <:option value="au" label="Australia" />
          <:option value="us" label="United States" />
          <:option value="eu" label="Europe" />
        </.select>
      </.field>
      <.field class="flex-row items-center justify-between rounded-lg border p-4">
        <:label for="workspace_digest">Weekly digest</:label>
        <:description>Send a Monday summary to workspace admins.</:description>
        <.switch id="workspace_digest" name="workspace[digest]" checked />
      </.field>
    </.card_content>
    <.card_footer class="justify-end border-t">
      <.button variant={:outline}>Cancel</.button>
      <.button>Save changes</.button>
    </.card_footer>
  </.card>
</div>

Admin shell

A sidebar, table, and detail card composed into a small operational view.

Recipe
Customers
Recent customer activity and account health.
Customer Status Spend
Northstar Studio Active $12,400
Atlas Labs Review $8,920
Northstar Studio
Primary account owner and next steps.
NS

Renewal review due in 14 days.

  <.sidebar_layout collapsible={:none} class="min-h-[32rem] rounded-xl border">
  <:sidebar content_class="px-3 py-4">
    <.sidebar_group label="Admin">
      <.sidebar_item icon="layout-dashboard" current>Customers</.sidebar_item>
      <.sidebar_item icon="receipt-text">Invoices</.sidebar_item>
      <.sidebar_item icon="settings">Settings</.sidebar_item>
    </.sidebar_group>
  </:sidebar>

  <:main class="min-w-0 p-6">
    <div class="grid gap-6 xl:grid-cols-[1fr_20rem]">
      <.card>
        <.card_header>
          <.card_title>Customers</.card_title>
          <.card_action><.button size={:sm}>Add customer</.button></.card_action>
          <.card_description>Recent customer activity and account health.</.card_description>
        </.card_header>
        <.card_content>
          <.table>
            <.table_header>
              <.table_row>
                <.table_head>Customer</.table_head>
                <.table_head>Status</.table_head>
                <.table_head class="text-right">Spend</.table_head>
              </.table_row>
            </.table_header>
            <.table_body>
              <.table_row>
                <.table_cell>Northstar Studio</.table_cell>
                <.table_cell>
                  <.badge color={:success} variant={:outline}>Active</.badge>
                </.table_cell>
                <.table_cell class="text-right">$12,400</.table_cell>
              </.table_row>
              <.table_row>
                <.table_cell>Atlas Labs</.table_cell>
                <.table_cell>
                  <.badge color={:warning} variant={:outline}>Review</.badge>
                </.table_cell>
                <.table_cell class="text-right">$8,920</.table_cell>
              </.table_row>
            </.table_body>
          </.table>
        </.card_content>
      </.card>

      <.card>
        <.card_header>
          <.card_title>Northstar Studio</.card_title>
          <.card_description>Primary account owner and next steps.</.card_description>
        </.card_header>
        <.card_content class="grid gap-4">
          <.avatar fallback="NS" />
          <p class="text-sm text-muted-foreground">Renewal review due in 14 days.</p>
          <.button variant={:outline} class="w-full">Open profile</.button>
        </.card_content>
      </.card>
    </div>
  </:main>
</.sidebar_layout>

Data table

A LiveView-managed orders table with server-owned filtering, sorting, selection, and paging.

Recipe
Showing page 1 of 4. Sorting and filtering stay in LiveView assigns.
Customer Status Total Actions
ORD-1001 Mira Chen Paid $240.00
ORD-1002 Ari Patel Pending $88.50
ORD-1003 Levi Buzolic Paid $149.00
  <% filter = @filter || "" %>
<% sort = @sort || %{by: :number, dir: :asc} %>
<% selected_ids = @selected_ids || MapSet.new([1002]) %>
<% page = @page || %{number: 1, total: 4, prev_path: nil, next_path: "/orders?page=2"} %>
<% orders = @orders || [
  %{id: 1001, number: "ORD-1001", customer: "Mira Chen", status: :paid, total: "$240.00"},
  %{id: 1002, number: "ORD-1002", customer: "Ari Patel", status: :pending, total: "$88.50"},
  %{id: 1003, number: "ORD-1003", customer: "Levi Buzolic", status: :paid, total: "$149.00"}
] %>

<div class="space-y-4">
  <form phx-change="filter" phx-submit="filter" class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
    <.input
      id="orders-filter"
      name="q"
      value={filter}
      placeholder="Filter orders..."
      class="sm:max-w-xs"
    />
    <.button type="submit" variant={:outline} size={:sm}>Apply filters</.button>
  </form>

  <.table>
    <.table_caption>
      Showing page {page.number} of {page.total}. Sorting and filtering stay in LiveView assigns.
    </.table_caption>
    <.table_header>
      <.table_row>
        <.table_head class="w-10">
          <.checkbox id="select-all-orders" name="select_all" aria-label="Select all orders" />
        </.table_head>
        <.table_head>
          <.button
            type="button"
            variant={:ghost}
            size={:sm}
            class="-ml-3"
            phx-click="sort"
            phx-value-by="number"
          >
            Order
            <.icon :if={sort.by == :number && sort.dir == :asc} name="chevron-up" class="size-3" />
            <.icon :if={sort.by == :number && sort.dir == :desc} name="chevron-down" class="size-3" />
          </.button>
        </.table_head>
        <.table_head>Customer</.table_head>
        <.table_head>Status</.table_head>
        <.table_head class="text-right">Total</.table_head>
        <.table_head class="w-10"><span class="sr-only">Actions</span></.table_head>
      </.table_row>
    </.table_header>
    <.table_body>
      <.table_row :for={order <- orders} state={if MapSet.member?(selected_ids, order.id), do: "selected"}>
        <.table_cell>
          <.checkbox
            id={"select-order-" <>> to_string(order.id)}
            name="selected_order_ids[]"
            value={to_string(order.id)}
            checked={MapSet.member?(selected_ids, order.id)}
            aria-label={"Select " <> order.number}
          />
        </.table_cell>
        <.table_cell class="font-medium">{order.number}</.table_cell>
        <.table_cell>{order.customer}</.table_cell>
        <.table_cell>
          <.badge color={if(order.status == :paid, do: :success, else: :warning)} variant={:outline}>
            {Phoenix.Naming.humanize(order.status)}
          </.badge>
        </.table_cell>
        <.table_cell class="text-right">{order.total}</.table_cell>
        <.table_cell>
          <.button
            type="button"
            variant={:ghost}
            size={:icon}
            aria-label={"Open actions for " <>> order.number}
          >
            <.icon name="ellipsis-vertical" class="size-4" />
          </.button>
        </.table_cell>
      </.table_row>
    </.table_body>
  </.table>

  <.pagination>
    <.pagination_content>
      <.pagination_item>
        <.pagination_previous href={page.prev_path || "#"} aria-disabled={is_nil(page.prev_path)} />
      </.pagination_item>
      <.pagination_item>
        <.pagination_link href="#" active>{page.number}</.pagination_link>
      </.pagination_item>
      <.pagination_item><.pagination_ellipsis /></.pagination_item>
      <.pagination_item>
        <.pagination_link href={"/orders?page=" <>> to_string(page.total)}>{page.total}</.pagination_link>
      </.pagination_item>
      <.pagination_item>
        <.pagination_next href={page.next_path || "#"} aria-disabled={is_nil(page.next_path)} />
      </.pagination_item>
    </.pagination_content>
  </.pagination>
</div>