Components Cheatsheet

View Source

Quick reference for all 55 Sutra UI components.

For design rules and composition guidance, see SKILL.md, skill/patterns.md, and skill/recipes.md. For form-specific patterns, use forms.cheatmd.

Browse the live component examples at sutraui.gurujada.com.

Icon examples use inline SVG intentionally. Sutra UI does not provide a general icon component or bundled icon set; use your app's existing icon helper if it has one, otherwise use inline accessible SVG.

Foundation

Button

<.button>Click me</.button>
<.button variant="secondary">Secondary</.button>
<.button variant="destructive">Delete</.button>
<.button variant="outline">Outline</.button>
<.button variant="ghost">Ghost</.button>
<.button variant="link">Link</.button>

Button Sizes

<.button size="sm">Small</.button>
<.button size="default">Default</.button>
<.button size="lg">Large</.button>
<.button size="icon" aria-label="Settings">
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
</.button>

Button Navigation

<.button navigate={~p"/users"}>Navigate</.button>
<.button patch={~p"/users/1"}>Patch</.button>
<.button href="https://example.com">Link</.button>

Button States

<.button loading>Loading...</.button>
<.button disabled>Disabled</.button>

Badge

<.badge>Default</.badge>
<.badge variant="secondary">Secondary</.badge>
<.badge variant="destructive">Error</.badge>
<.badge variant="outline">Outline</.badge>

Spinner

<.spinner />
<.spinner size="sm" />
<.spinner size="lg" />

Kbd

<.kbd>⌘</.kbd>
<.kbd>Ctrl</.kbd> + <.kbd>C</.kbd>

Form Controls

Input

<.input type="text" name="name" />
<.input type="email" name="email" placeholder="you@example.com" />
<.input type="password" name="password" />
<.input type="number" name="quantity" min="0" max="100" />

Input Types

TypeUsage
textGeneral text
emailEmail addresses
passwordPasswords
numberNumeric values
telPhone numbers
urlURLs
searchSearch fields
dateNative date input
timeNative time input
datetime-localNative date/time input

Textarea

<.textarea name="bio" rows="4" />
<.textarea name="notes" placeholder="Enter notes..." />

Label

<.label for="email">Email</.label>

Checkbox

<.checkbox id="terms" name="terms" />
<.checkbox id="newsletter" name="newsletter" checked />

Switch

<.switch id="notifications" name="notifications" />
<.switch id="dark-mode" name="dark_mode" checked />

Radio Group

<.radio_group name="plan">
  <:radio value="free" label="Free" />
  <:radio value="pro" label="Pro" />
  <:radio value="enterprise" label="Enterprise" />
</.radio_group>

Select

<.select id="country" name="country" value={@country}>
  <:trigger>{@country_label || "Select country"}</:trigger>
  <.select_option value="us" label="United States" />
  <.select_option value="uk" label="United Kingdom" />
  <.select_option value="ca" label="Canada" />
</.select>

Slider

<.slider id="volume" name="volume" min="0" max="100" value="50" />

Range Slider

<.range_slider 
  id="price-range"
  name="price"
  min="0" 
  max="1000" 
  value_min="100"
  value_max="500"
/>

Live Select

<.live_component
  module={SutraUI.LiveSelect}
  id="users"
  field={@form[:user_id]}
  placeholder="Search users..."
/>
def handle_event("live_select_change", %{"text" => text, "id" => id}, socket) do
  users = Accounts.search_users(text)
  options = Enum.map(users, &{&1.name, &1.id})

  send_update(SutraUI.LiveSelect, id: id, options: options)
  {:noreply, socket}
end

Input Group

<.input_group>
  <:prefix>$</:prefix>
  <input type="number" name="amount" class="input pl-7 pr-12" />
  <:suffix>.00</:suffix>
</.input_group>

<.input_group>
  <:prefix type="icon">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
  </:prefix>
  <input type="search" name="q" class="input pl-9" placeholder="Search..." />
</.input_group>

Input OTP

<.input_otp id="mfa-code" name="code" groups={[3, 3]} />
<.input_otp id="pin" name="pin" length={4} pattern="[0-9]" />

File Upload

<.file_upload upload={@uploads.avatar} label="Upload avatar" description="PNG or JPG up to 2 MB" />

Form (LiveView)

<.form for={@form} class="form" phx-change="validate" phx-submit="save">
  <.input field={@form[:name]} label="Name" />
  <.input field={@form[:email]} type="email" label="Email" />
  <.button type="submit">Save</.button>
</.form>

Filter Bar

<.filter_bar on_change="filter">
  <:filter>
    <.input type="search" name="q" placeholder="Search..." />
  </:filter>
  <:filter>
    <.input type="select" name="status" options={@status_options} />
  </:filter>
</.filter_bar>

Layout

Card

<.card>
  <:header>
    <h2>Card Title</h2>
    <p>Card description</p>
  </:header>
  <:content>
    Card content goes here
  </:content>
  <:footer>
    <.button>Action</.button>
  </:footer>
</.card>
<.header>
  Page Title
  <:subtitle>Optional subtitle</:subtitle>
  <:actions>
    <.button>Action</.button>
  </:actions>
</.header>

Table

<.data_table id="users" rows={@users}>
  <:col :let={user} label="Name">{user.name}</:col>
  <:col :let={user} label="Email">{user.email}</:col>
  <:action :let={user}>
    <.button size="sm" variant="ghost">Edit</.button>
  </:action>
</.data_table>

Item

<.item>
  <:leading>
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
  </:leading>
  <:title>Document Title</:title>
  <:description>Description text</:description>
  <:trailing><.button size="sm">View</.button></:trailing>
</.item>

Drawer

<%# Basic drawer with toggle button %>
<.drawer_trigger for="main-drawer" />
<.drawer id="main-drawer">
  <:header>
    <img src="/logo.svg" alt="Logo" />
  </:header>
  <.drawer_group label="Navigation">
    <.drawer_item href="/">Home</.drawer_item>
    <.drawer_item href="/users">Users</.drawer_item>
  </.drawer_group>
  <:footer>
    <.drawer_item href="/settings">Settings</.drawer_item>
  </:footer>
</.drawer>

<%# Initially open on desktop %>
<.drawer_trigger for="app-drawer" />
<.drawer id="app-drawer" open>
  <.drawer_group label="Main">
    <.drawer_item href="/">Home</.drawer_item>
    <.drawer_item href="/dashboard">Dashboard</.drawer_item>
  </.drawer_group>
</.drawer>

<%# Right-side drawer with custom trigger %>
<.drawer_trigger for="filter-drawer" variant="outline">
  Filters
</.drawer_trigger>
<.drawer id="filter-drawer" side="right">
  <.drawer_group label="Filters">
    <.drawer_item href="/active">Active</.drawer_item>
  </.drawer_group>
</.drawer>

Stepper

<.stepper current={2}>
  <:step>
    <h4>Profile</h4>
    <p>Add your details</p>
  </:step>
  <:step>
    <h4>Workspace</h4>
    <p>Configure defaults</p>
  </:step>
  <:step>
    <h4>Invite</h4>
    <p>Bring in teammates</p>
  </:step>
</.stepper>

Stepper Wizard

<.form for={@form} phx-change="validate" phx-submit="submit_wizard">
  <.stepper_wizard id="checkout" current={@step} errors={@step_errors}>
    <:step id="shipping" label="Shipping">
      <.shipping_fields form={@form} />
    </:step>
    <:step id="payment" label="Payment">
      <.payment_fields form={@form} />
    </:step>
    <:step id="confirm" label="Confirm">
      <.order_review order={@order} />
    </:step>

    <:actions>
      <.button type="button" phx-click="previous">Back</.button>
      <.button type="submit">Continue</.button>
    </:actions>
  </.stepper_wizard>
</.form>

Tree View

<.tree_view id="project-files" label="Project files">
  <.tree_item label="lib" expanded>
    <.tree_item label="app.ex" />
  </.tree_item>
  <.tree_item label="README.md" href="/readme" />
</.tree_view>

Feedback

Alert

<.alert>
  <:title>Default alert message</:title>
</.alert>
<.alert variant="destructive">
  <:title>Error message</:title>
</.alert>
<.alert>
  <:title>Heads up!</:title>
  <:description>You can add components to your app using the CLI.</:description>
</.alert>

Flash

<.flash kind={:info} flash={@flash} />
<.flash kind={:error} flash={@flash} title="Something went wrong" />

Progress

<.progress value={33} />
<.progress value={66} />

Skeleton

<.skeleton class="h-4 w-[250px]" />
<.skeleton class="h-12 w-12 rounded-full" />

Empty

<.empty>
  <:icon>
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-12 w-12" aria-hidden="true"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
  </:icon>
  <:title>No messages</:title>
  <:description>Get started by sending your first message.</:description>
  <:actions><.button>Send Message</.button></:actions>
</.empty>

Loading State

<.loading_state message="Loading data..." />
<.loading_state message="Please wait..." wrap_card={false} />

Toast

<.toast_container flash={@flash} />

<.toast id="saved-toast" variant="default">
  <:title>Changes saved</:title>
</.toast>

<.toast id="error-toast" variant="destructive">
  <:title>Something went wrong</:title>
</.toast>

Overlay

Dialog

<.button phx-click="open_confirm">Open Dialog</.button>

<.dialog id="confirm-dialog" show={@show_confirm} on_cancel="close_confirm">
  <:title>Confirm Action</:title>
  <:description>Are you sure?</:description>
  Dialog content here
  <:footer>
    <.button variant="outline" phx-click="close_confirm">
      Cancel
    </.button>
    <.button phx-click="confirm">Confirm</.button>
  </:footer>
</.dialog>

Dialog JS Commands

<.button phx-click={SutraUI.Dialog.show_dialog("my-dialog")}>Open</.button>
<.button phx-click={SutraUI.Dialog.hide_dialog("my-dialog")}>Close</.button>

Popover

<.popover id="info-popover">
  <:trigger>
    Info
  </:trigger>
  <p>Popover content here</p>
</.popover>

Tooltip

<.tooltip id="help-tip" tooltip="Helpful tip">
  <.button>Hover me</.button>
</.tooltip>
<.dropdown_menu id="actions-menu">
  <:trigger>
    Actions
  </:trigger>
  <.dropdown_item phx-click="edit">Edit</.dropdown_item>
  <.dropdown_item phx-click="duplicate">Duplicate</.dropdown_item>
  <.dropdown_separator />
  <.dropdown_item phx-click="delete" variant="destructive">Delete</.dropdown_item>
</.dropdown_menu>

Context Menu

<.context_menu id="message-menu">
  <:trigger>
    <div class="rounded-lg border p-6">Right click here</div>
  </:trigger>
  <.context_menu_item>Reply</.context_menu_item>
  <.context_menu_item shortcut="⌘K">Copy link</.context_menu_item>
  <.context_menu_separator />
  <.context_menu_item variant="destructive">Delete</.context_menu_item>
</.context_menu>

Command

<.command id="command-palette" placeholder="Type a command...">
  <.command_group heading="Suggestions">
    <.command_item id="calendar">Calendar</.command_item>
    <.command_item id="search">Search</.command_item>
    <.command_item id="settings">Settings</.command_item>
  </.command_group>
</.command>

Hover Card

<.hover_card id="user-preview">
  <:trigger><button>@sutra_ui</button></:trigger>
  <div class="space-y-1">
    <p class="font-medium">Sutra UI</p>
    <p class="text-sm text-muted-foreground">Phoenix LiveView components.</p>
  </div>
</.hover_card>

Tabs

<.tabs id="settings-tabs" default_value="account">
  <:tab value="account">Account</:tab>
  <:tab value="password">Password</:tab>
  <:tab value="notifications">Notifications</:tab>
  <:panel value="account">Account settings...</:panel>
  <:panel value="password">Password settings...</:panel>
  <:panel value="notifications">Notification preferences...</:panel>
</.tabs>

Accordion

<.accordion id="faq">
  <:item value="q1" title="What is Sutra UI?">
    A Phoenix LiveView component library.
  </:item>
  <:item value="q2" title="Is it accessible?">
    Components use semantic markup and ARIA patterns where appropriate.
  </:item>
</.accordion>
<.breadcrumb>
  <:item href="/">Home</:item>
  <:item href="/products">Products</:item>
  <:item>Current Page</:item>
</.breadcrumb>

Pagination

<.pagination 
  page={@page} 
  total_pages={@total_pages}
  on_page_change="page_changed"
/>

Tab Nav

Responsive server-side routed tab navigation. It collapses to a dropdown on small screens by default.

<.tab_nav id="settings-tabs-nav">
  <:tab patch={~p"/settings/profile"} active={@tab == :profile}>
    Profile
  </:tab>
  <:tab patch={~p"/settings/account"} active={@tab == :account}>
    Account
  </:tab>
</.tab_nav>

Use collapse="never" when the tab row should stay visible on small screens.

Display

Avatar

<.avatar src="/avatar.jpg" alt="User" />
<.avatar src={nil} initials="JD" />
<.avatar size="sm" src="/avatar.jpg" />
<.avatar size="lg" src="/avatar.jpg" />
<.carousel id="images">
  <:item>
    <img src="/image1.jpg" alt="Image 1" />
  </:item>
  <:item>
    <img src="/image2.jpg" alt="Image 2" />
  </:item>
  <:item>
    <img src="/image3.jpg" alt="Image 3" />
  </:item>
</.carousel>

Calendar

<.calendar year={2026} month={6} selected="2026-06-17" select_event="select_date" />

Timeline

<.timeline>
  <:item time="09:00">
    <h3>Created</h3>
    <p>Workspace created</p>
  </:item>
  <:item time="10:30" icon="✓">
    <h3>Published</h3>
    <p>Live now</p>
  </:item>
</.timeline>

<.timeline>
  <:item time="Today" icon="✓">
    <.link navigate="/releases/1">v0.4.0 shipped</.link>
    <p>Release notes and migration details.</p>
  </:item>
</.timeline>

Marquee

<.marquee>
  <:item>Free shipping on all orders</:item>
  <:item>New components released weekly</:item>
  <:item>24/7 support available</:item>
</.marquee>

<.marquee speed="slow" gap="lg" direction="right">
  <:item><img src="/logo-1.svg" alt="Company 1" /></:item>
  <:item><img src="/logo-2.svg" alt="Company 2" /></:item>
</.marquee>

Separator

<.separator />
<.separator decorative={false} />
<.separator orientation="vertical" />

Theme Switcher

<.theme_switcher id="theme-toggle" />

AI

Response

<.response value={@answer} />
<.response id="answer" value={@answer} streaming reveal="word" />
<.response value={@streamed_markdown} format="markdown" streaming />

Activity

<.activity>
  <:item status="complete">Searched documentation</:item>
  <:item status="running">
    Checking relevant examples.
  </:item>
  <:item status="pending">Preparing summary</:item>
</.activity>