defmodule AshuiWeb.DocsLive do
use AshuiWeb, :live_view
@impl true
def mount(params, _session, socket) do
section = Map.get(params, "section", "getting-started")
component = Map.get(params, "component", nil)
socket =
socket
|> assign(section: section, component: component)
|> assign(rich_text_content: nil)
|> assign(form: to_form(%{"content" => nil}))
|> assign(sidebar_open: true)
|> assign(sidebar_section_states: build_sidebar_section_states())
|> assign(:github_star_count, "2.4k")
{:ok, socket}
end
@impl true
def handle_params(params, _url, socket) do
if Map.has_key?(params, "section") do
section = Map.get(params, "section", "getting-started")
component = Map.get(params, "component", nil)
{:noreply, assign(socket, section: section, component: component)}
else
{:noreply,
socket
|> assign(section: "getting-started", component: nil)
|> push_navigate(to: ~p"/docs/getting-started")}
end
end
@impl true
def handle_event("validate", %{"content" => content}, socket) do
{:noreply, assign(socket, rich_text_content: content)}
end
@impl true
def handle_event("validate", _params, socket) do
{:noreply, socket}
end
@impl true
def handle_event("toggle-sidebar", _params, socket) do
current = Map.get(socket.assigns, :sidebar_open, true)
{:noreply, assign(socket, sidebar_open: not current)}
end
@impl true
def handle_event("toggle-nav-section", %{"section" => section}, socket) do
states = socket.assigns[:sidebar_section_states] || %{}
updated_states = Map.update(states, section, true, &(!&1))
{:noreply, assign(socket, sidebar_section_states: updated_states)}
end
@impl true
def handle_event("expand-all-sections", _params, socket) do
{:noreply, assign(socket, sidebar_section_states: build_sidebar_section_states(true))}
end
@impl true
def handle_event("collapse-all-sections", _params, socket) do
{:noreply, assign(socket, sidebar_section_states: build_sidebar_section_states(false))}
end
@impl true
def handle_event("update-rich-text", %{"content" => content}, socket) do
{:noreply, assign(socket, rich_text_content: content)}
end
@impl true
def handle_event("carousel-prev", %{"id" => id}, socket) do
current = Map.get(socket.assigns, :"#{id}_current_slide", 0)
new_current = max(0, current - 1)
{:noreply, assign(socket, :"#{id}_current_slide", new_current)}
end
@impl true
def handle_event("carousel-next", %{"id" => id}, socket) do
current = Map.get(socket.assigns, :"#{id}_current_slide", 0)
{:noreply, assign(socket, :"#{id}_current_slide", current + 1)}
end
@impl true
def handle_event("carousel-go-to", %{"id" => id, "index" => index}, socket) do
{:noreply, assign(socket, :"#{id}_current_slide", String.to_integer(index))}
end
@impl true
def handle_event("wizard-prev", %{"id" => id}, socket) do
# Wizard navigation would need to be handled by updating step active states
{:noreply, socket}
end
@impl true
def handle_event("wizard-next", %{"id" => id}, socket) do
# Wizard navigation would need to be handled by updating step active states
{:noreply, socket}
end
@impl true
def handle_event("toggle-filter-panel", %{"id" => id}, socket) do
current = Map.get(socket.assigns, :"#{id}_collapsed", false)
{:noreply, assign(socket, :"#{id}_collapsed", !current)}
end
@impl true
def handle_event("filter-changed", _params, socket) do
{:noreply, socket}
end
@impl true
def handle_event("clear-filters", %{"id" => _id}, socket) do
{:noreply, socket}
end
@impl true
def handle_event("autocomplete-search", %{"id" => id, "value" => value}, socket) do
# Filter options based on search value
# In a real app, this would filter from server-side data
{:noreply, assign(socket, :"#{id}_search_value", value)}
end
@impl true
def handle_event("sortable-reorder", %{"id" => id, "order" => order}, socket) do
# Handle reordering - in a real app, update the items list
{:noreply, assign(socket, :"#{id}_order", order)}
end
@impl true
def handle_event("chat-send", %{"id" => _id, "message" => _message}, socket) do
# Handle chat message - in a real app, broadcast to channel
{:noreply, socket}
end
@impl true
def handle_event("chat-typing", %{"id" => _id}, socket) do
# Handle typing indicator - in a real app, broadcast typing status
{:noreply, socket}
end
@impl true
def handle_event(
"feed-action",
%{"id" => _id, "item-id" => _item_id, "action" => _action},
socket
) do
# Handle feed item action
{:noreply, socket}
end
@impl true
def handle_event("toggle-connection", %{"id" => _id}, socket) do
# Handle connection toggle
{:noreply, socket}
end
@impl true
def handle_event("view-all-connections", %{"type" => _type}, socket) do
# Handle view all connections
{:noreply, socket}
end
@impl true
def handle_event("toggle-drawer", %{"id" => id}, socket) do
current_state = Map.get(socket.assigns, :"#{id}_open", false)
{:noreply, assign(socket, :"#{id}_open", !current_state)}
end
@impl true
def handle_event("close-drawer", %{"id" => id}, socket) do
{:noreply, assign(socket, :"#{id}_open", false)}
end
@impl true
def handle_event("open-lightbox", %{"id" => id, "index" => index}, socket) do
images = Map.get(socket.assigns, :"#{id}_images", [])
{:noreply,
socket
|> assign(:"#{id}_show", true)
|> assign(:"#{id}_current_index", String.to_integer(index))
|> assign(:"#{id}_images", images)}
end
@impl true
def handle_event("close-lightbox", %{"id" => id}, socket) do
{:noreply, assign(socket, :"#{id}_show", false)}
end
@impl true
def handle_event("lightbox-prev", %{"id" => id}, socket) do
current_index = Map.get(socket.assigns, :"#{id}_current_index", 0)
new_index = max(0, current_index - 1)
{:noreply, assign(socket, :"#{id}_current_index", new_index)}
end
@impl true
def handle_event("lightbox-next", %{"id" => id}, socket) do
current_index = Map.get(socket.assigns, :"#{id}_current_index", 0)
images = Map.get(socket.assigns, :"#{id}_images", [])
new_index = min(length(images) - 1, current_index + 1)
{:noreply, assign(socket, :"#{id}_current_index", new_index)}
end
@impl true
def render(assigns) do
~H"""
AshUI is a modular, reusable UI library for Phoenix LiveView applications.
<.button variant={:primary}>Click me</.button>
<.input type="text" label="Name" />
<.alert variant={:success}>Success!</.alert>
def deps do
[
{:ashui, path: "../ashui"}
]
end
In your lib/your_app_web.ex
file, add imports:
import Ashui.Components.Button
import Ashui.Components.Input
import Ashui.Components.Alert
# ... more components
Once imported, use components in your LiveView templates:
defmodule MyAppWeb.UserLive do
use MyAppWeb, :live_view
def render(assigns) do
~H"""
<.button variant={:primary} phx-click="save">Save</.button>
<.input type="text" label="Name" field={@form[:name]} />
"""
end
end
Button component with multiple variants and states.
<.button variant={:primary}>Primary</.button>
<.button variant={:secondary}>Secondary</.button>
<.button variant={:ghost}>Ghost</.button>
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | :primary | :secondary | :ghost | :danger | :success | :primary | Button style variant |
| size | :sm | :md | :lg | :xl | :md | Button size |
| disabled | boolean | false | Disable the button |
| loading | boolean | false | Show loading spinner |
Input field component with validation states and helper text.
<.input type="text" label="Name" placeholder="Enter your name" />
<.input type="email" label="Email" helper="We'll never share your email" />
<.input type="password" label="Password" error="Password is required" />
Card component for content containers with customizable padding and shadows.
This card has default padding and shadow.
Custom padding with larger shadow.
<.card>
<h3>Card Title</h3>
<p>Card content</p>
</.card>
| Prop | Type | Default | Description |
|---|---|---|---|
| padded | boolean | true | Add padding to card |
| shadow | :none | :sm | :md | :lg | :xl | :md | Shadow size |
Select dropdown component with options and validation states.
<.select label="Country">
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</.select>
Textarea component for multi-line text input with validation.
<.textarea label="Message" placeholder="Enter your message" rows={4} />
<.textarea label="Description" helper="Maximum 500 characters" rows={6} />
Checkbox component with label and validation support.
<.checkbox label="Accept terms" />
<.checkbox label="Subscribe" checked={true} />
<.checkbox label="Required" required={true} />
Progress bar component for showing completion status.
<.progress value={75} max={100} />
<.progress value={50} variant={:success} />
<.progress value={90} variant={:error} show_label={true} />
| Prop | Type | Default | Description |
|---|---|---|---|
| value | integer | required | Current progress value |
| max | integer | 100 | Maximum value |
| variant | :primary | :success | :warning | :error | :primary | Progress bar color variant |
| show_label | boolean | false | Show percentage label |
Responsive navbar component with mobile menu support and customizable navigation items.
<.navbar brand="MyApp">
<:item href="/">Home</:item>
<:item href="/about">About</:item>
<:item href="/contact">Contact</:item>
</.navbar>
<.navbar brand="MyApp">
<:item href="/" active={true}>Home</:item>
<:item href="/about" active={false}>About</:item>
<:item href="/contact" active={false}>Contact</:item>
</.navbar>
<.navbar brand="MyApp">
<:item href="/">Home</:item>
<:item href="/about">About</:item>
<:actions>
<.button variant={:primary} size={:sm}>Sign In</.button>
</:actions>
</.navbar>
| Prop | Type | Default | Description |
|---|---|---|---|
| brand | string | "Brand" | Brand name displayed in navbar |
| class | string | "" | Additional CSS classes |
| item (slot) | slot | - |
Navigation items. Each item accepts:
|
| actions (slot) | slot | - | Content displayed on the right side of the navbar (e.g., buttons, user menu) |
The navbar automatically includes a mobile menu toggle button. To handle the mobile menu toggle, add a LiveView event handler:
def handle_event("toggle-mobile-menu", _params, socket) do
{:noreply, assign(socket, :mobile_menu_open, !socket.assigns[:mobile_menu_open])}
end
Note: You'll need to add JavaScript or LiveView logic to show/hide the mobile menu based on the state.
Tabs component for navigation between different content sections.
<.tabs>
<:tab id="tab-1" label="Tab 1" active={true}>Content 1</:tab>
<:tab id="tab-2" label="Tab 2">Content 2</:tab>
</.tabs>
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | "tabs" | Unique identifier for tabs container |
| tab (slot) | slot | required | Tab items with id, label, and active attributes |
Badge component for status indicators and labels.
<.badge variant={:success}>Active</.badge>
<.badge variant={:warning} size={:lg}>Pending</.badge>
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | :default | :primary | :success | :warning | :error | :info | :default | Badge color variant |
| size | :sm | :md | :lg | :md | Badge size |
Modal dialog component for overlays and confirmations.
Modal requires LiveView state management. See code example below.
<.modal id="confirm-modal" show={@show_modal}>
<:title>Confirm Action</:title>
<p>Are you sure you want to proceed?</p>
<:footer>
<.button variant={:secondary} phx-click="close-modal">Cancel</.button>
<.button variant={:primary} phx-click="confirm">Confirm</.button>
</:footer>
</.modal>
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | required | Unique identifier for modal |
| show | boolean | false | Control modal visibility |
| size | :sm | :md | :lg | :xl | :md | Modal size |
| title (slot) | slot | - | Modal title |
| footer (slot) | slot | - | Modal footer actions |
Data table component with sorting and pagination support.
<.table rows={@users}>
<:col label="Name" field={:name} />
<:col label="Email" field={:email} />
<:col label="Actions">
<.button size="sm">Edit</.button>
</:col>
</.table>
| Prop | Type | Default | Description |
|---|---|---|---|
| rows | list | [] | List of row data (maps) |
| col (slot) | slot | required | Column definitions with label and optional field |
Collapsible sections with buttery-smooth transitions that keep dense information organised. Perfect for FAQs, advanced settings, or progressive disclosure flows.
Compose rich content within each item and decide which panels open by default for a guided story.
Focus on progressive disclosure to keep your surfaces calm. Use accordions to tuck away secondary controls while keeping essential actions visible.
<:item id="accordion-content" title="Content patterns" open={false}>The component ships with subtle easing to avoid jarring jumps. Extend with Tailwind transitions for deeper motion stories.
<.accordion class="space-y-3">
<:item id="accordion-design" title="Design strategy" open={true}>
...
</:item>
<:item id="accordion-content" title="Content patterns" open={false}>
...
</:item>
</.accordion>
Small heuristics to keep accordion experiences intuitive and accessible.
Drive the accordion with expressive slot attributes while inheriting accessible semantics out of the box.
| Prop | Type | Default | Description |
|---|---|---|---|
| item (slot) | slot | required |
Accordion rows with id, title, and optional open.
|
Breadcrumb navigation component for showing page hierarchy.
<.breadcrumbs>
<:item href="/">Home</:item>
<:item href="/products">Products</:item>
<:item active={true}>Current Page</:item>
</.breadcrumbs>
| Prop | Type | Default | Description |
|---|---|---|---|
| item (slot) | slot | required | Breadcrumb items with href and optional active attribute |
Grid layout component for responsive layouts using CSS Grid.
<.grid cols={3} gap={4}>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</.grid>
<.grid cols={1} cols_md={2} cols_lg={4} gap={4}>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
<div>Item 4</div>
</.grid>
This grid will show 1 column on mobile, 2 columns on medium screens, and 4 columns on large screens.
Content for card 1
Content for card 2
<.grid cols={1} cols_md={2} gap={6}>
<.card>
<h3>Card 1</h3>
<p>Content</p>
</.card>
<.card>
<h3>Card 2</h3>
<p>Content</p>
</.card>
</.grid>
| Prop | Type | Default | Description |
|---|---|---|---|
| cols | integer | 1 | Number of columns (base/default) |
| cols_sm | integer | - | Number of columns on small screens (≥640px) |
| cols_md | integer | - | Number of columns on medium screens (≥768px) |
| cols_lg | integer | - | Number of columns on large screens (≥1024px) |
| cols_xl | integer | - | Number of columns on extra large screens (≥1280px) |
| gap | integer | 4 | Gap between grid items (Tailwind gap value: 0-12) |
| class | string | "" | Additional CSS classes |
Data list component for displaying structured information with titles and content.
<.list>
<:item title="Name">John Doe</:item>
<:item title="Email">john@example.com</:item>
<:item title="Role">Administrator</:item>
</.list>
<.list>
<:item title="Post Title">{@post.title}</:item>
<:item title="Views">{@post.views}</:item>
<:item title="Published">{@post.published_at}</:item>
</.list>
<.list>
<:item title="Status">
<.badge variant={:success}>Active</.badge>
</:item>
<:item title="Priority">
<.badge variant={:warning}>High</.badge>
</:item>
</.list>
| Prop | Type | Default | Description |
|---|---|---|---|
| item (slot) | slot | required |
List items. Each item accepts:
|
Stats component for displaying statistics and metrics in a clean, organized layout.
<.stats>
<:stat title="Total Users" value="1,234" desc="21% more than last month" />
<:stat title="Revenue" value="$45,678" desc="12% increase" />
<:stat title="Orders" value="567" desc="8% decrease" />
</.stats>
<.stats horizontal={false}>
<:stat title="Page Views" value="89,400" desc="21% more than last month" />
<:stat title="Unique Visitors" value="12,345" desc="5% increase" />
</.stats>
<.stats>
<:stat title="Active Users" value={@active_users} desc="Last 30 days" />
<:stat title="Total Sales" value={@total_sales} desc="This month" />
</.stats>
| Prop | Type | Default | Description |
|---|---|---|---|
| horizontal | boolean | true | Layout stats horizontally or vertically |
| class | string | "" | Additional CSS classes |
| stat (slot) | slot | required |
Stat items. Each stat accepts:
|
Avatar component for displaying user profile pictures, initials, or custom content.
<.avatar name="John Doe" />
<.avatar name="Jane Smith" />
<.avatar name="AB" />
<.avatar name="XS" size={:xs} />
<.avatar name="SM" size={:sm} />
<.avatar name="MD" size={:md} />
<.avatar name="LG" size={:lg} />
<.avatar name="XL" size={:xl} />
<.avatar src="/images/user.jpg" name="User Name" />
<.avatar src="/images/user.jpg" name="User Name" size={:lg} />
<.avatar name="User" online={true} />
<.avatar name="User" online={false} />
<.avatar name="User" />
<.avatar>
<.icon name="hero-user" class="w-6 h-6" />
</.avatar>
<.avatar>
<span>👤</span>
</.avatar>
| Prop | Type | Default | Description |
|---|---|---|---|
| src | string | nil | Image source URL. If provided, displays image instead of initials |
| name | string | nil | Name for generating initials (first letter of each word, max 2 characters) |
| size | :xs | :sm | :md | :lg | :xl | :md | Avatar size |
| online | boolean | nil | Show online (true) or offline (false) status indicator. nil hides indicator |
| class | string | "" | Additional CSS classes |
DatePicker component for selecting dates, times, and date-time values with validation support.
<.datepicker label="Birth Date" field={@form[:birth_date]} />
<.datepicker label="Start Date" value="2024-01-15" />
<.datepicker label="Appointment" type={:datetime_local} field={@form[:appointment]} />
<.datepicker label="Event Time" type={:time} field={@form[:time]} />
<.datepicker label="Start Date" min="2024-01-01" max="2024-12-31" />
<.datepicker label="End Date" min="2024-01-01" max="2024-12-31" />
<.datepicker label="Required Date" required={true} field={@form[:date]} />
<.datepicker label="Date with Error" error="This field is required" />
<.datepicker label="Date with Helper" helper="Select a date in the future" />
| Prop | Type | Default | Description |
|---|---|---|---|
| type | :date | :datetime_local | :time | :month | :week | :date | Input type for date/time selection |
| field | Phoenix.HTML.FormField | - | Form field struct from to_form/2 |
| label | string | nil | Label text displayed above the input |
| value | string | nil | Default date/time value (ISO format) |
| min | string | nil | Minimum selectable date/time (ISO format) |
| max | string | nil | Maximum selectable date/time (ISO format) |
| error | string | nil | Error message to display |
| helper | string | nil | Helper text displayed below the input |
| required | boolean | false | Mark field as required |
| disabled | boolean | false | Disable the datepicker |
Slider component for selecting a numeric value within a range with visual feedback.
<.slider label="Volume" value={50} />
<.slider label="Brightness" value={75} />
<.slider label="Opacity" value={25} />
<.slider label="Temperature (°C)" value={20} min={-10} max={40} />
<.slider label="Price Range ($)" value={500} min={0} max={1000} step={50} />
<.slider label="Rating" value={3} min={1} max={5} step={1} />
<.slider label="Volume" value={50} show_value={false} />
<.slider label="Speed" value={60} show_value={false} />
<.slider label="Required Value" value={50} error="This field is required" />
<.slider label="Value with Helper" value={30} helper="Adjust the slider to set your preferred value" />
<.slider label="Disabled Slider" value={50} disabled={true} />
When used with a form field, the slider integrates with Phoenix forms:
<.form for={@form} phx-change="validate" phx-submit="save">
<.slider label="Volume" field={@form[:volume]} />
<.button type="submit">Save</.button>
</.form>
| Prop | Type | Default | Description |
|---|---|---|---|
| field | Phoenix.HTML.FormField | - | Form field struct from to_form/2 |
| label | string | nil | Label text displayed above the slider |
| value | integer | 50 | Current slider value |
| min | integer | 0 | Minimum selectable value |
| max | integer | 100 | Maximum selectable value |
| step | integer | 1 | Step increment for slider value |
| error | string | nil | Error message to display |
| helper | string | nil | Helper text displayed below the slider |
| disabled | boolean | false | Disable the slider |
| show_value | boolean | true | Show current value next to the slider |
Toast component for displaying temporary notifications that appear at fixed positions on the screen.
Toasts are fixed position notifications. Here are examples of different variants:
<.toast variant={:success}>Operation successful!</.toast>
<.toast variant={:error}>Something went wrong</.toast>
<.toast variant={:info}>Information message</.toast>
<.toast variant={:warning}>Warning message</.toast>
Toasts can be positioned at different locations on the screen:
<.toast variant={:info} position={:top_start}>Top Start</.toast>
<.toast variant={:info} position={:top_center}>Top Center</.toast>
<.toast variant={:info} position={:top_end}>Top End</.toast>
<.toast variant={:info} position={:bottom_start}>Bottom Start</.toast>
<.toast variant={:info} position={:bottom_center}>Bottom Center</.toast>
<.toast variant={:info} position={:bottom_end}>Bottom End</.toast>
Toasts can automatically dismiss after a specified duration:
<.toast variant={:success} duration={3000}>Auto-dismiss in 3 seconds</.toast>
<.toast variant={:info} duration={0}>No auto-dismiss</.toast>
Toasts can be made non-dismissible by setting dismissible={false}:
<.toast variant={:warning} dismissible={false}>Non-dismissible toast</.toast>
Toasts work seamlessly with LiveView. You can show/hide them using JS commands:
# In your LiveView
def handle_event("show-success", _params, socket) do
{:noreply, socket}
end
# In your template
<button phx-click="show-success" phx-click-js={JS.show(to: "#toast-success", transition: "fade-in-scale")}>
Show Success Toast
</button>
<.toast id="toast-success" variant={:success}>
Operation completed successfully!
</.toast>
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | auto-generated | Unique ID for the toast (required for JS.show/hide) |
| variant | :info | :success | :warning | :error | :info | Visual variant of the toast |
| position | :top_start | :top_center | :top_end | :middle_start | :middle_center | :middle_end | :bottom_start | :bottom_center | :bottom_end | :top_end | Position of the toast on the screen |
| dismissible | boolean | true | Show close button to manually dismiss |
| duration | integer | 5000 | Auto-dismiss duration in milliseconds (0 to disable) |
| class | string | "" | Additional CSS classes |
Stepper component for displaying multi-step processes and progress indicators.
<.stepper>
<:step label="Step 1" status={:completed} />
<:step label="Step 2" status={:active} />
<:step label="Step 3" status={:pending} />
<:step label="Step 4" status={:pending} />
</.stepper>
<.stepper orientation={:vertical}>
<:step label="Account Setup" status={:completed} />
<:step label="Profile Information" status={:completed} />
<:step label="Preferences" status={:active} />
<:step label="Review" status={:pending} />
</.stepper>
<.stepper>
<:step label="Cart" status={:completed} icon="hero-shopping-cart" />
<:step label="Shipping" status={:completed} icon="hero-truck" />
<:step label="Payment" status={:active} icon="hero-credit-card" />
<:step label="Review" status={:pending} icon="hero-check-circle" />
</.stepper>
<.stepper>
<:step label="Success" status={:completed} variant={:success} />
<:step label="Warning" status={:active} variant={:warning} />
<:step label="Error" status={:pending} variant={:error} />
</.stepper>
Pending (default):
Active:
Completed:
<:step label="Pending Step" status={:pending} />
<:step label="Active Step" status={:active} />
<:step label="Completed Step" status={:completed} />
| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | :horizontal | :vertical | :horizontal | Layout orientation of the stepper |
| class | string | "" | Additional CSS classes |
| Step Slot Props | |||
| label | string | nil | Label text for the step |
| status | :pending | :active | :completed | :pending | Status of the step |
| variant | :primary | :secondary | :accent | :info | :success | :warning | :error | :primary | Color variant for active/completed steps |
| icon | string | nil | Heroicon name (e.g., "hero-check-circle") |
Chart component for displaying various types of data visualizations including Line, Bar, Pie, Donut, Area, Stacked, and Sparkline charts.
<.chart type={:line} data={[10, 20, 30, 40, 50, 45, 35]} labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]} />
<.chart type={:bar} data={[20, 35, 25, 45, 30]} labels={["Q1", "Q2", "Q3", "Q4", "Q5"]} />
<.chart type={:pie} data={[30, 25, 20, 15, 10]} labels={["A", "B", "C", "D", "E"]} />
<.chart type={:donut} data={[40, 30, 20, 10]} labels={["Desktop", "Mobile", "Tablet", "Other"]} />
<.chart type={:area} data={[15, 25, 35, 30, 40, 35, 45]} />
<.chart type={:sparkline} data={[10, 15, 12, 18, 14, 20, 16]} width={200} height={50} />
| Prop | Type | Default | Description |
|---|---|---|---|
| type | :line | :bar | :pie | :donut | :area | :stacked | :sparkline | - | Chart type |
| data | list | - | Array of numeric data points |
| labels | list | [] | Labels for data points |
| width | integer | 400 | Chart width in pixels |
| height | integer | 300 | Chart height in pixels |
| colors | list | nil | Custom color array (hex codes) |
| title | string | nil | Chart title |
Gauge component for displaying KPI metrics and progress indicators with circular progress visualization.
<.gauge value={75.0} max={100.0} label="CPU Usage" />
<.gauge value={60.0} max={100.0} label="Memory" />
<.gauge value={80.0} variant={:success} label="Success" />
<.gauge value={65.0} variant={:warning} label="Warning" />
<.gauge value={50.0} variant={:error} label="Error" />
<.gauge value={70.0} size={:sm} label="Small" />
<.gauge value={70.0} size={:md} label="Medium" />
<.gauge value={70.0} size={:lg} label="Large" />
| Prop | Type | Default | Description |
|---|---|---|---|
| value | float | - | Current value |
| max | float | 100.0 | Maximum value |
| label | string | nil | Label text below gauge |
| variant | :primary | :success | :warning | :error | :primary | Color variant |
| size | :sm | :md | :lg | :md | Gauge size |
Heatmap component for displaying data density and user interactions using color intensity visualization.
<.heatmap data={%{"2024-01-01" => 5, "2024-01-02" => 10, "2024-01-03" => 15}} />
Blue (default):
Green:
Red:
Purple:
<.heatmap data={heatmap_data} color_scheme={:blue} />
<.heatmap data={heatmap_data} color_scheme={:green} />
<.heatmap data={heatmap_data} color_scheme={:red} />
<.heatmap data={heatmap_data} color_scheme={:purple} />
| Prop | Type | Default | Description |
|---|---|---|---|
| data | map | - | Map of keys (dates/labels) to numeric values |
| color_scheme | :blue | :green | :red | :purple | :blue | Color scheme for intensity visualization |
Interactive map component for displaying locations and events. Uses OpenStreetMap tiles for rendering.
<.map latitude={37.7749} longitude={-122.4194} zoom={13} />
<.map
latitude={40.7128}
longitude={-74.0060}
zoom={12}
markers={[
%{lat: 40.7128, lng: -74.0060, label: "New York"},
%{lat: 40.7589, lng: -73.9851, label: "Times Square"}
]}
/>
<.map latitude={51.5074} longitude={-0.1278} zoom={10} width="600px" height="300px" />
| Prop | Type | Default | Description |
|---|---|---|---|
| latitude | float | - | Center latitude coordinate |
| longitude | float | - | Center longitude coordinate |
| zoom | integer | 13 | Zoom level (1-18) |
| markers | list | [] | List of marker maps with :lat, :lng, and optional :label |
| width | string | "100%" | Map width (CSS value) |
| height | string | "400px" | Map height (CSS value) |
The map component requires a JavaScript hook (MapHook) for full interactivity.
For production use, consider integrating with Mapbox, Google Maps, or Leaflet.js for enhanced features.
File upload component with drag & drop support and live progress tracking. Uses Phoenix LiveView's built-in file upload functionality.
Note: This component requires LiveView upload configuration in your LiveView module.
File upload component preview
Configure uploads in your LiveView to see the full component
<.file_upload uploads={@uploads.avatar} label="Upload Avatar" />
<.file_upload uploads={@uploads.documents} accept=".pdf,.doc" max_entries={5} />
def mount(_params, _session, socket) do
{:ok,
socket
|> allow_upload(:avatar, accept: ~w(.jpg .jpeg .png), max_entries: 1, max_file_size: 10_000_000)}
end
def handle_event("validate", _params, socket) do
{:noreply, socket}
end
def handle_event("cancel-upload", %{"ref" => ref}, socket) do
{:noreply, cancel_upload(socket, :avatar, ref)}
end
| Prop | Type | Default | Description |
|---|---|---|---|
| uploads | map | - | Uploads map from socket (e.g., @uploads.avatar) |
| accept | string | "*/*" | Accepted file types (e.g., "image/*", ".pdf,.doc") |
| max_entries | integer | 1 | Maximum number of files |
| max_file_size | integer | 10485760 | Maximum file size in bytes (default: 10MB) |
| drag_label | string | "Drag and drop files here..." | Custom drag & drop label text |
Rich text editor / WYSIWYG component for blogs, comments, and content editing with formatting controls.
Try the editor below - use the toolbar buttons or keyboard shortcuts to format your text!
Current HTML Output:
{@rich_text_content}
<.rich_text_editor field={@form[:content]} label="Content" />
<.rich_text_editor
field={@form[:content]}
value={"<h2>Welcome</h2><p>This is <strong>pre-filled</strong> content!</p>"}
label="Pre-filled Editor"
/>
The rich text editor uses a JavaScript hook (RichTextEditorHook) to sync content with the hidden input field for form submission.
All formatting is handled client-side using the browser's
document.execCommand
API. The HTML content is automatically synced to a hidden input field for form submission.
| Prop | Type | Default | Description |
|---|---|---|---|
| field | Phoenix.HTML.FormField | - | Form field struct |
| label | string | nil | Label text |
| placeholder | string | "Start typing..." | Placeholder text |
| value | string | nil | Initial HTML content |
Multi-select component with chips/tags display for selecting multiple items from a list.
<.multi_select field={@form[:tags]} options={["Option 1", "Option 2", "Option 3"]} />
<.multi_select field={@form[:categories]} options={categories} label="Categories" />
<.multi_select
field={@form[:tags]}
options={[
{"Frontend", "frontend"},
{"Backend", "backend"},
{"DevOps", "devops"}
]}
/>
The multi-select component requires a JavaScript hook (MultiSelectHook) for dropdown toggle functionality.
Selected values are submitted as an array in the form params.
| Prop | Type | Default | Description |
|---|---|---|---|
| field | Phoenix.HTML.FormField | - | Form field struct |
| options | list | - | List of options (strings or label-value tuples) |
| placeholder | string | "Select options..." | Placeholder text when no items selected |
Rating / Star input component for reviews and feedback with interactive star selection.
<.rating field={@form[:rating]} />
Small:
Medium (default):
Large:
<.rating field={@form[:score]} size={:sm} />
<.rating field={@form[:score]} size={:md} />
<.rating field={@form[:score]} size={:lg} />
<.rating value={4} readonly={true} />
<.rating field={@form[:score]} max={10} />
The rating component requires a JavaScript hook (RatingHook) for interactive star selection.
| Prop | Type | Default | Description |
|---|---|---|---|
| field | Phoenix.HTML.FormField | nil | Form field struct (optional) |
| value | integer | 0 | Current rating value |
| max | integer | 5 | Maximum rating (number of stars) |
| size | :sm | :md | :lg | :md | Star size |
| readonly | boolean | false | Display-only mode (no interaction) |
Password input component with live strength validation and suggestions.
<.password_strength_meter id="password-input" field={@form[:password]} />
<.password_strength_meter id="signup-password" field={@form[:password]} show_suggestions={true} />
Tag / Chips input component for adding and removing multiple tags dynamically.
<.tag_input id="tags-input" tags={@tags} />
<.tag_input id="skills-input" tags={@skills} placeholder="Add skills..." max_tags={5} />
Signature pad / drawing canvas component for approvals or annotations.
<.signature_pad id="signature-pad" width={400} height={200} />
<.signature_pad id="approval-signature" width={600} height={300} clear_label="Clear" />
Color picker / palette selector component for design dashboards or admin apps.
<.color_picker id="primary-color" value="#3b82f6" />
<.color_picker id="theme-color" value="#000000" show_palette={true} />
Conditional / dynamic form component where fields appear/disappear based on user input.
<.conditional_form id="dynamic-form">
<:field name="user_type" type="select" options={["individual", "business"]} />
<:conditional_field show_if={%{"user_type" => "business"}}>
<div class="mb-4">
<label for="company_name-input">Company Name</label>
<input type="text" id="company_name-input" name="company_name" />
</div>
</:conditional_field>
</.conditional_form>
Carousel component for displaying images, testimonials, or content cards in a sliding carousel with navigation controls.
First slide content
Second slide content
Third slide content
<.carousel id="my-carousel">
<:slide>
<div class="p-8">Slide 1</div>
</:slide>
<:slide>
<div class="p-8">Slide 2</div>
</:slide>
</.carousel>
<.carousel id="autoplay-carousel" autoplay={true} interval={3000} />
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique ID for the carousel |
| autoplay | boolean | false | Automatically advance slides |
| interval | integer | 5000 | Autoplay interval in milliseconds |
| show_indicators | boolean | true | Show slide indicators |
| show_arrows | boolean | true | Show navigation arrows |
Wizard component for multi-step forms or flows with navigation, progress tracking, and validation support.
Enter your personal details here.
Set up your account information.
Review and confirm your information.
<.wizard id="signup-wizard">
<:step id="step1" title="Personal Info" active={true}>
<p>Step 1 content</p>
</:step>
<:step id="step2" title="Account Details">
<p>Step 2 content</p>
</:step>
</.wizard>
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique ID for the wizard |
| step.id | string | - | Unique ID for each step |
| step.title | string | - | Step title |
| step.active | boolean | false | Whether the step is currently active |
| step.completed | boolean | false | Whether the step is completed |
Resizable panels component for split layouts like IDEs or dashboards. Users can drag to resize panels horizontally or vertically.
Drag the divider to resize
Resizable content area
<.resizable_panels id="split-layout" orientation={:horizontal}>
<:panel min_width="200px" default_width="300px">
<div class="p-4">Left Panel</div>
</:panel>
<:panel>
<div class="p-4">Right Panel</div>
</:panel>
</.resizable_panels>
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique ID for the panels |
| orientation | :horizontal | :vertical | :horizontal | Layout orientation |
| panel.min_width | string | "100px" | Minimum panel width |
| panel.default_width | string | nil | Initial panel width |
Filter panel component for faceted search and real-time filtering of data tables with collapsible sections.
<.filter_panel id="product-filters">
<:filter label="Category" field="category">
</:filter>
<:option field="category" value="electronics" label="Electronics" count={42} />
<:option field="category" value="clothing" label="Clothing" count={28} />
<:filter label="Price Range" field="price" type={:radio}>
</:filter>
<:option field="price" value="0-50" label="$0 - $50" />
</.filter_panel>
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique ID for the filter panel |
| title | string | "Filters" | Panel title |
| collapsible | boolean | true | Allow collapsing the panel |
| filter.type | :checkbox | :radio | :range | :checkbox | Filter input type |
Skeleton component for loading placeholders during async data loading. Provides visual feedback while content is being fetched.
<.skeleton variant={:text} size={:md} />
<.skeleton variant={:text} lines={3} />
<.skeleton variant={:circle} size={:md} />
<.skeleton variant={:rect} width="200px" height="100px" />
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | :text | :circle | :rect | :text | Skeleton shape variant |
| size | :sm | :md | :lg | :md | Size for text and circle variants |
| lines | integer | 1 | Number of lines for text variant |
| width | string | nil | Custom width (e.g., "200px") |
| height | string | nil | Custom height (e.g., "100px") |
Full-page skeleton layouts for dashboard, article, and profile loading states.
<.skeleton_layout variant={:dashboard} />
<.skeleton_layout variant={:article} class="mt-6" />
Autocomplete component with live search and suggestion list while typing. Provides keyboard navigation and debounced search.
<.autocomplete id="search-users" field={@form[:user]} options={users} />
<.autocomplete id="product-search" field={@form[:product]} options={products} min_chars={2} />
<.autocomplete
id="user-search"
options={[{"John Doe", "1"}, {"Jane Smith", "2"}]}
max_results={5}
/>
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique ID for the autocomplete |
| field | Phoenix.HTML.FormField | nil | Form field struct (optional) |
| options | list | - | List of options (strings or label-value tuples) |
| min_chars | integer | 1 | Minimum characters before showing suggestions |
| max_results | integer | 10 | Maximum number of suggestions to show |
Sortable list component for drag & drop reordering of items. Users can visually rearrange items by dragging.
{item.description}
<.sortable_list id="todo-list" items={@todos}>
<:item :let={todo}>
<div class="p-4 border rounded">
<%= todo.title %>
</div>
</:item>
</.sortable_list>
<.sortable_list id="items" items={@items} item_id={:uuid}>
<:item :let={item}>
<%= item.name %>
</:item>
</.sortable_list>
The sortable list component requires a JavaScript hook (SortableListHook) for drag & drop functionality. The order is automatically tracked in a hidden input field.
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique ID for the sortable list |
| items | list | - | List of items to display |
| item_id | atom | :id | Key to use as unique identifier for items |
Live chat component with typing indicators and real-time messaging. Supports message bubbles, user identification, and typing status.
<.live_chat id="chat-room" messages={@messages} current_user={@current_user} />
<.live_chat
id="chat-room"
messages={@messages}
current_user={@current_user}
typing_users={@typing_users}
/>
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique ID for the chat |
| messages | list | [] | List of message maps with :text, :user_id, :user_name, :timestamp |
| typing_users | list | [] | List of users currently typing |
Live feed component for scrollable real-time updates and notifications. Automatically scrolls to new items and tracks user scroll position.
<.live_feed id="notifications" items={@notifications} />
<.live_feed id="feed" items={@items} max_height="500px" auto_scroll={true} />
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique ID for the feed |
| items | list | [] | List of feed items with :title, :content, :timestamp, :avatar |
| auto_scroll | boolean | true | Automatically scroll to new items |
Countdown timer component for events, offers, or games. Updates in real-time and dispatches events when complete.
<.countdown id="sale-timer" target_date={~U[2024-12-31 23:59:59Z]} />
<.countdown id="event-timer" target_date={@event_date} format={:detailed} />
<.countdown
id="timer"
target_date={@target_date}
on_complete="countdown-complete"
/>
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique ID for the countdown |
| target_date | DateTime | NaiveDateTime | string | - | Target date/time for countdown |
| format | atom | :compact | Display format (:compact or :detailed) |
| on_complete | string | nil | Event name to dispatch when countdown completes |
Kanban / Task Board component with draggable columns and cards for project management apps.
tasks = [
%{
id: "task-1",
name: "Project Planning",
start_date: ~D[2024-01-01],
end_date: ~D[2024-01-05],
progress: 30
}
]
<.gantt id="project-timeline" tasks={@tasks} />
<.gantt
id="project-timeline"
tasks={@tasks}
start_date={~D[2024-01-01]}
end_date={~D[2024-12-31]}
/>
Interactive Gantt chart for project timelines with drag, resize, and dependency visuals.
tasks = [
%{
id: "task-1",
name: "Kickoff",
start_date: ~D[2024-01-02],
end_date: ~D[2024-01-05],
progress: 100
},
%{
id: "task-2",
name: "Design Sprint",
start_date: ~D[2024-01-06],
end_date: ~D[2024-01-12],
dependencies: ["task-1"]
}
]
<.gantt
id="product-roadmap"
tasks={@tasks}
start_date={~D[2024-01-01]}
end_date={~D[2024-01-31]}
/>
Live Data Table component with virtual scrolling for handling huge datasets efficiently. Supports PubSub updates, sorting, filtering, and row selection.
columns = [
%{key: "id", label: "ID", sortable: true},
%{key: "name", label: "Name", sortable: true},
%{key: "email", label: "Email", sortable: true}
]
<.live_data_table
id="users-table"
rows={@users}
columns={@columns}
row_height={50}
visible_rows={10}
/>
<.live_data_table
id="users-table"
rows={@users}
columns={@columns}
enable_selection={true}
selected_rows={@selected_rows}
/>
The component dispatches the following events to your LiveView:
filter-table
- When filter value changes
sort-column
- When column header is clicked
toggle-row-selection
- When row checkbox is toggled
select-all-rows
- When header checkbox is clicked
User profile card component with online/offline status, avatar, and user information. Perfect for displaying user details in a card format.
<.user_profile_card
name="John Doe"
email="john@example.com"
avatar="/images/avatar.jpg"
online={true}
/>
<.user_profile_card
name="Jane Smith"
actions={[
%{label: "Message", variant: :primary, on_click: "message-user"},
%{label: "Follow", variant: :outline, on_click: "follow-user"}
]}
/>
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | - | User's full name |
| online | boolean | false | Online status indicator |
| avatar | string | nil | URL to user's avatar image |
Avatar group component for displaying multiple users in a row with overlapping avatars. Supports online status indicators and overflow count.
<.avatar_group users={@users} max_visible={5} />
Small
Medium
Large
| Prop | Type | Default | Description |
|---|---|---|---|
| users | list | - | List of user maps with :name, :avatar, :online |
| max_visible | integer | 5 | Maximum number of avatars to show |
| size | atom | :md | Avatar size (:sm, :md, :lg) |
Social-style connections list with avatars, status indicators, mutual counts, and custom actions.
<.connections_widget
connections={@connections}
current_user={@current_user_id}
type={:followers}
max_display={5}
>
<:action>
<button class="btn btn-secondary">View All</button>
</:action>
</.connections_widget>
type
attribute.
Gamified badge card for achievements, loyalty programs, and progress tracking.
<.badge_card
title="Collaboration Pro"
description="Invite five teammates"
icon="hero-users"
rarity={:rare}
progress={60}
points={120}
/>
rarity
attr to theme border and icon colors.
progress.
points
and unlocked_at
for richer context.
Accessible toggle powered by the `ThemeToggleHook`. Deliver effortless transitions between light and dark palettes with a single interactive element.
Choose from multiple sizes and optionally surface accessibility labels to clarify mode changes.
<.theme_switch />
<.theme_switch size={:lg} />
<.theme_switch show_label={true} />
Integrate the switch with minimal wiring and customise visuals using Tailwind utilities to match your brand aura.
ThemeToggleHook
defined in assets/js/app.js.
show_label
for explicit light/dark annotations.
class
attribute for unique theming.
ARIA-friendly alert component for displaying important messages to users. Supports multiple variants and dismissible alerts so teams can deliver timely, accessible feedback.
Demonstrates each semantic variant and how it harmonises with the dark UI foundation.
<.alert variant={:info} title="Information" message="This is an info alert" />
<.alert variant={:success} title="Success" message="Operation successful" />
<.alert variant={:warning} title="Warning" message="Please review" />
<.alert variant={:error} title="Error" message="Something went wrong" />
Enable dismissible alerts for temporary feedback that users can clear once acknowledged.
<.alert variant={:info} dismissible={true} title="Alert" message="Message" />
Configure alerts with a small, expressive API. Defaults are optimised for accessibility and theme coherence.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | atom | :info | Alert variant (:info, :success, :warning, :error) |
| dismissible | boolean | false | Allow dismissing the alert |
| role | string | "alert" | ARIA role attribute |
Configurable theme system with color, spacing, and font tokens. Use the `Ashui.Theme` module to orchestrate consistent styling programmatically across your product.
Curated palette engineered for luminous contrast on dark surfaces.
# Get a color token
Ashui.Theme.color(:primary)
# => "oklch(70% 0.213 47.604)"
# Get all colors
Ashui.Theme.colors()
Modular scale spacing mapped to responsive rhythm.
# Get a spacing token
Ashui.Theme.spacing(:md)
# => "1rem"
# Get all spacing tokens
Ashui.Theme.spacing_tokens()
Typography stacks that deliver clarity across platforms.
# Get a font token
Ashui.Theme.font(:sans)
# => ["ui-sans-serif", "system-ui", "sans-serif"]
# Get all fonts
Ashui.Theme.fonts()
Leverage helper functions to access tokens inside LiveViews, components, and modules.
| Function | Description |
|---|---|
| Ashui.Theme.color/1 | Get a color token value |
| Ashui.Theme.spacing/1 | Get a spacing token value |
| Ashui.Theme.font/1 | Get a font token value |
| Ashui.Theme.current_theme/1 | Get current theme from assigns |
Diff component for side-by-side comparison of two items (text, code, etc.).
<.diff left="Old content" right="New content" />
| Prop | Type | Default | Description |
|---|---|---|---|
| left | string | - | Left side content |
| right | string | - | Right side content |
| type | atom | :text | Content type (:text or :code) |
Keyboard shortcut display component for showing keyboard combinations.
Press
Press
Press
Press <.kbd>Ctrl</.kbd> + <.kbd>S</.kbd> to save
Small
Medium
Large
| Prop | Type | Default | Description |
|---|---|---|---|
| size | atom | :md | Key size (:sm, :md, :lg) |
Styled link component with support for external links and LiveView `navigate`/`patch`.
<.link href="#">Default link</.link>
<.link href="#" variant={:primary}>Primary</.link>
<.link navigate={~p"/dashboard"}>Dashboard</.link>
<.link patch={~p"/docs?section=components"}>Components</.link>
<.link href="https://ashui.dev" external={true}>AshUI</.link>
Flexible hero banner with title, subtitle, CTA, and optional background image.
<.hero
title="Ship Better Products"
subtitle="Plan, collaborate, and deliver features."
cta="Get Started"
cta_href={~p"/signup"}
background="https://example.com/hero.jpg"
/>
Toggle between two states using animated swap transitions.
<.swap id="theme-swap" active={@dark_mode}>
<:on>Dark</:on>
<:off>Light</:off>
</.swap>
Dock / Bottom Navigation component for mobile-style bottom navigation bars.
<.dock id="main-dock">
<:item icon="hero-home" label="Home" active={true} />
<:item icon="hero-user" label="Profile" />
</.dock>
| Prop | Type | Default | Description |
|---|---|---|---|
| icon | string | - | Icon name for dock item |
| active | boolean | false | Active state |
Drawer sidebar component for off-canvas navigation.
<.drawer id="nav-drawer" open={@drawer_open}>
<:trigger>Open Menu</:trigger>
<:content>
<nav>Navigation items</nav>
</.drawer>
| Prop | Type | Default | Description |
|---|---|---|---|
| side | atom | :left | Drawer side (:left or :right) |
| overlay | boolean | true | Show overlay backdrop |
Mockup component for displaying phone, browser, or window mockups.
Phone Content
<.mockup type={:phone}>
<img src="/screenshot.png" />
</.mockup>
This is a browser mockup
Window content here
| Prop | Type | Default | Description |
|---|---|---|---|
| type | atom | - | Mockup type (:phone, :browser, :window) |
| url | string | nil | URL for browser mockup |
Timeline component for displaying chronological events or activities with support for nested/collapsible events.
<.timeline id="events-timeline" items={@events} />
<.timeline
id="nested-timeline"
items={@events}
collapsible={true}
default_expanded={true}
/>
Lightbox / Image Viewer component for full-screen image previews with navigation.
<.lightbox id="gallery-lightbox" images={@images} current_index={0} show={@show_lightbox} />
| Prop | Type | Default | Description |
|---|---|---|---|
| images | list | - | List of image URLs or maps with :src and optional :alt |
| current_index | integer | 0 | Currently displayed image index |
Video Player component with custom controls for embedded videos.
<.video_player src="/videos/example.mp4" poster="/images/poster.jpg" />
| Prop | Type | Default | Description |
|---|---|---|---|
| src | string | - | Video source URL |
| autoplay | boolean | false | Autoplay video |
Animated Chart component with smooth transitions when data changes.
<.animated_chart type={:line} data={@chart_data} labels={@labels} />
| Prop | Type | Default | Description |
|---|---|---|---|
| type | atom | - | Chart type (:line, :bar, :area) |
| duration | integer | 1000 | Animation duration in milliseconds |
Masonry Grid / Pinterest Layout component for dynamic card layouts.
<.masonry_grid items={@items} columns={3} gap={4}>
<:item :let={item}>
<div>{item.content}</div>
</:item>
</.masonry_grid>
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | integer | 3 | Number of columns |
| gap | integer | 4 | Gap between items (Tailwind spacing unit) |
This component documentation is being built. Check back soon!