Appearance
UI Component Library — Usage Guide
Welcome to the Design System UI Component Library. Every component in this directory is auto-imported globally by Nuxt 4 and utilizes design tokens defined in our CSS custom properties (no hardcoded styling values).
Table of Contents
- UiSpinner
- UiButton
- UiInput
- UiBadge
- UiCard
- UiAlert
- UiSelect
- UiTextarea
- UiTable
- UiPagination
- UiModal
- UiDrawer
- UiPageState
UiSpinner
A flexible loading spinner used as the default indicator throughout the application.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'md' | The size of the spinner (sm: 16px, md: 24px, lg: 40px) |
class | string | undefined | Custom CSS classes to apply |
Example
vue
<template>
<div class="flex items-center gap-2">
<UiSpinner size="sm" />
<span>Loading...</span>
</div>
</template>UiButton
A robust button component utilizing reka-ui Primitive. Supports variant styling, size options, and a native loading spinner state.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'default' | Visual styling variant |
size | 'default' | 'sm' | 'lg' | 'icon' | 'default' | Physical size options |
loading | boolean | false | Sets loading state, disables action, displays spinner, auto disable and spinner during loading |
as | string | 'button' | Underlying HTML element to render |
asChild | boolean | false | Pass-through rendering pattern |
Example
vue
<template>
<!-- Standard Button -->
<UiButton variant="primary" @click="handleClick">
Submit Entry
</UiButton>
<!-- Loading State Button -->
<UiButton variant="secondary" :loading="isLoading">
Save Changes
</UiButton>
</template>UiInput
Fully accessible form input field component featuring built-in state styling for errors, custom clearable actions, labels, and support text.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | number | undefined | Binding value |
defaultValue | string | number | undefined | Initial default value |
type | string | 'text' | HTML input type attribute |
placeholder | string | undefined | Input placeholder value |
disabled | boolean | false | Disables user interaction |
error | string | undefined | Displays error message and triggers invalid borders |
clearable | boolean | false | Displays a clear button when text exists |
id | string | undefined | Accessibility ID |
Slots
label: Custom label nodemessage: Custom description message block
Emits
update:modelValue: Fired on value updatesclear: Fired when the clear button is clicked
Example
vue
<script setup lang="ts">
const name = ref('')
const inputError = ref('')
</script>
<template>
<UiInput
v-model="name"
id="username"
placeholder="Enter Username"
:error="inputError"
clearable
>
<template #label>Username</template>
<template #message>Choose a unique identifier.</template>
</UiInput>
</template>UiBadge
A chip status element mapped cleanly to Tailwind status color tokens.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
status | 'active' | 'pending' | 'cancelled' | 'overdue' | 'paid' | 'rejected' | 'non_payer' | 'active' | Maps the badge to the corresponding design token palette |
Example
vue
<template>
<UiBadge status="paid">
Transaction Approved
</UiBadge>
</template>UiCard
A container component for grouping content with optional header and footer panels.
Slots
default: Core card body layoutheader: Header title panelfooter: Action buttons/footer panel
Example
vue
<template>
<UiCard class="max-w-md">
<template #header>
<h3 class="text-lg font-bold">Profile Card</h3>
</template>
<p>User account configuration and preferences go here.</p>
<template #footer>
<UiButton variant="outline">Cancel</UiButton>
</template>
</UiCard>
</template>UiAlert
Status warning blocks for context alerts, available in multiple status levels.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'info' | 'success' | 'warning' | 'danger' | 'info' | Alert severity configuration |
dismissible | boolean | false | Renders close button if true |
Emits
dismiss: Emitted when the user closes the alert
Example
vue
<template>
<UiAlert variant="success" dismissible @dismiss="onAlertClose">
Profile successfully updated.
</UiAlert>
</template>UiSelect
Accessible custom selector component based on reka-ui.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | undefined | Current chosen value |
options | SelectOption[] | [] | List of items: { label: string, value: string, disabled?: boolean } |
placeholder | string | 'Select an option' | Placeholder content |
error | string | undefined | Custom validation error string |
disabled | boolean | false | Disables selector interaction |
id | string | undefined | Identifier attribute |
Example
vue
<script setup lang="ts">
const selection = ref('')
const items = [
{ label: 'Role Developer', value: 'developer' },
{ label: 'Role Admin', value: 'admin' }
]
</script>
<template>
<UiSelect
v-model="selection"
:options="items"
placeholder="Choose your workspace role"
>
<template #label>User Role</template>
</UiSelect>
</template>UiTextarea
A responsive multi-line input field, supporting standard features alongside optional auto-height adjustments.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | undefined | Binding value |
defaultValue | string | undefined | Baseline text |
error | string | undefined | Error feedback string |
autoResize | boolean | false | Auto-sizes height to prevent scroll bars if true |
rows | number | 3 | Base height (in rows) |
Example
vue
<script setup lang="ts">
const bio = ref('')
</script>
<template>
<UiTextarea
v-model="bio"
placeholder="Write your bio..."
auto-resize
>
<template #label>Biography</template>
</UiTextarea>
</template>UiTable
A complete visual grid component featuring integrated dynamic cells, responsive loading feedback, empty status layout, and column sorting flags.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | TableColumn[] | (required) | Array of structure elements: { key: string, label: string, sortable?: boolean, class?: string } |
rows | Record<string, unknown>[] | (required) | Array of data structures |
sortKey | string | undefined | Column key representing active sorting |
sortDir | 'asc' | 'desc' | undefined | Sorting direction |
loading | boolean | false | Shows a overlay loader when true |
Slots
cell-[key]: Custom formatter cell for the column matching the specified key. Injected with{ row, value }.loading: Custom loader spinner structure.empty: Custom empty grid feedback node.
Emits
sort: Emitted when a sortable column header cell is triggered. Emits the column key.
Example
vue
<script setup lang="ts">
const columns = [
{ key: 'title', label: 'Title', sortable: true },
{ key: 'status', label: 'Status' }
]
const rows = [
{ title: 'Project Alfa', status: 'active' },
{ title: 'Project Beta', status: 'pending' }
]
</script>
<template>
<UiTable :columns="columns" :rows="rows">
<template #cell-status="{ value }">
<UiBadge :status="value === 'active' ? 'active' : 'pending'">
{{ value.toUpperCase() }}
</UiBadge>
</template>
</UiTable>
</template>Server-Side (API) Sorting Example
For production applications with paginated backend databases, you should handle sorting via API requests. When a user clicks a sortable column header, UiTable emits the @sort event with the clicked column key. You can then trigger an asynchronous network request (or simulate it with a fake API fetch) as shown in the example below:
vue
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { TableColumn } from '~/components/ui/table'
const columns: TableColumn[] = [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'role', label: 'Role' },
{ key: 'amount', label: 'Amount', sortable: true, class: 'text-right' },
]
// Bindings for active sorting state
const sortKey = ref('name')
const sortDir = ref<'asc' | 'desc'>('asc')
const isLoading = ref(false)
const items = ref<any[]>([])
// Simulated backend API response with network latency
async function fetchItemsFromApi(key: string, dir: 'asc' | 'desc') {
isLoading.value = true
try {
// 1. Simulate 500ms network round-trip time
await new Promise(resolve => setTimeout(resolve, 500))
// 2. Mock calling your Nuxt repository / fetch client
// e.g. const res = await $kfFetch('/api/users', { query: { sortBy: key, order: dir } })
const mockDb = [
{ name: 'Htin Linn', role: 'Admin', amount: 250 },
{ name: 'Yimon Khaing', role: 'Member', amount: 120 },
{ name: 'Kyaw Zin', role: 'Viewer', amount: 75 },
]
// Simulate database order handling on the mock "server"
mockDb.sort((a, b) => {
const valA = a[key as keyof typeof a]
const valB = b[key as keyof typeof b]
if (typeof valA === 'number' && typeof valB === 'number') {
return dir === 'asc' ? valA - valB : valB - valA
}
return dir === 'asc'
? String(valA).localeCompare(String(valB))
: String(valB).localeCompare(String(valA))
})
items.value = mockDb
} catch (err) {
console.error('Error fetching data from API:', err)
} finally {
isLoading.value = false
}
}
// 3. Trigger sort direction swap or switch sortKey on @sort emit
function onSort(key: string) {
if (sortKey.value === key) {
sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
} else {
sortKey.value = key
sortDir.value = 'asc'
}
}
// 4. Watch sorting criteria changes to fetch fresh data from the server automatically
watch([sortKey, sortDir], () => {
fetchItemsFromApi(sortKey.value, sortDir.value)
}, { immediate: true })
</script>
<template>
<div class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-slate-500 uppercase">
Server-Side Sorted Table
</h3>
<span v-if="isLoading" class="text-xs text-indigo-600 animate-pulse">
Fetching from API...
</span>
</div>
<UiTable
:columns="columns"
:rows="items"
:sort-key="sortKey"
:sort-dir="sortDir"
:loading="isLoading"
@sort="onSort"
>
<!-- Format cell contents -->
<template #cell-amount="{ value }">
<span class="font-semibold font-mono block text-right">
${{ value.toFixed(2) }}
</span>
</template>
</UiTable>
</div>
</template>UiPagination
A cursor-based navigation component for records lists, showing standard next and prev hooks.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
prevCursor | string | null | null | Previous pagination cursor value. Renders button disabled if null. |
nextCursor | string | null | null | Next pagination cursor value. Renders button disabled if null. |
Emits
change: Emits target cursor value string on user click action.
Example
vue
<template>
<UiPagination
:prev-cursor="prevVal"
:next-cursor="nextVal"
@change="fetchPage"
/>
</template>UiModal
A clean accessible popup container built around reka-ui Dialog, utilizing subtle backdrops and zoom transitions.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Visibility state control |
persistent | boolean | false | Disables close on Escape / outside click |
title | string | undefined | Standard header title |
description | string | undefined | Standard support description |
Slots
default: Core modal contentsheader: Custom title / subtitle panel overridesfooter: Custom control buttons row
Emits
close: Emitted when the modal closes (via close click, Escape, or backdrop click)
Example
vue
<template>
<UiModal :open="isOpen" title="Confirm Action" @close="isOpen = false">
<p>Are you sure you want to proceed with deletion?</p>
<template #footer>
<UiButton variant="ghost" @click="isOpen = false">Cancel</UiButton>
<UiButton variant="destructive">Confirm</UiButton>
</template>
</UiModal>
</template>UiDrawer
A sliding drawer panel component suitable for secondary configuration blocks or details sheets.
Props
Shares identical prop list and attributes as UiModal.
Example
vue
<template>
<UiDrawer :open="isConfigOpen" title="Settings Panel" @close="isConfigOpen = false">
<div class="space-y-4">
<h4 class="text-sm font-semibold">User details</h4>
<p>Modify specific elements inside this sidebar.</p>
</div>
</UiDrawer>
</template>UiPageState
A page loading state orchestrator designed to wrap full viewport sections, responding gracefully with customized spinners, error message displays, or empty notifications.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
status | 'pending' | 'error' | 'empty' | 'success' | (required) | Current lifecycle status |
errorMessage | string | 'Something went wrong. Please try again.' | Message displayed under error status |
Slots
default: Content to mount when status is'success'empty: Custom empty status panel nodes
Emits
retry: Emitted when the error state 'Try again' button is clicked
Example
vue
<script setup lang="ts">
const status = ref<'pending' | 'error' | 'empty' | 'success'>('pending')
</script>
<template>
<UiPageState :status="status" @retry="loadData">
<div>
<h3>Page Title Loaded</h3>
<p>Data loads and displays successfully.</p>
</div>
</UiPageState>
</template>