cisse-vue-ui

Vue 3 + TypeScript + Tailwind CSS v4 component library
10,723
Install
npm install cisse-vue-ui
Latest Version:1.0.0
Vue:3
License:MIT
Last Updated:Jan 27, 2026
Links: GitHub  ·  npm
Maintainer: moulyae

cisse-vue-ui

A Vue 3 component library built with TypeScript and Tailwind CSS v4.

View Storybook Documentation

Installation

npm install cisse-vue-ui
# or
bun add cisse-vue-ui

Peer Dependencies

npm install vue@^3.4 tailwindcss@^4 @iconify/vue@^4

Setup

1. Import Styles

Add the pre-compiled CSS to your main CSS file:

@import 'cisse-vue-ui/style.css';
@import 'tailwindcss';

2. Configure Primary Color (Optional)

Override the default primary color in your CSS:

@theme {
  --color-primary-50: oklch(97% 0.02 142);
  --color-primary-100: oklch(94% 0.05 142);
  --color-primary-200: oklch(88% 0.10 142);
  --color-primary-300: oklch(78% 0.15 142);
  --color-primary-400: oklch(65% 0.20 142);
  --color-primary-500: oklch(55% 0.22 142);
  --color-primary-600: oklch(48% 0.20 142);
  --color-primary-700: oklch(40% 0.17 142);
  --color-primary-800: oklch(32% 0.14 142);
  --color-primary-900: oklch(25% 0.10 142);
  --color-primary-950: oklch(18% 0.08 142);
}

Usage

<script setup lang="ts">
import { Button, CardComponent, FormInput } from 'cisse-vue-ui'
</script>

Category Imports

import { Button, Tabs, TabPanel } from 'cisse-vue-ui/components/core'
import { FormInput, FormSelect, Switch } from 'cisse-vue-ui/components/form'
import { Modal, Alert, LoadingSpinner } from 'cisse-vue-ui/components/feedback'
import { BaseLayout, PageLayout } from 'cisse-vue-ui/components/layout'

Global Registration (Vue Plugin)

import { createApp } from 'vue'
import { VueTailwindUI } from 'cisse-vue-ui'

const app = createApp(App)

// Register all components
app.use(VueTailwindUI)

// Or with a prefix
app.use(VueTailwindUI, { prefix: 'Ui' }) // <UiButton>, <UiCard>, etc.

// Or specific components only
app.use(VueTailwindUI, { components: ['Button', 'CardComponent'] })

Components

Core

Component Description
Button Button with variants (primary, secondary, outline, ghost, danger, success), sizes, icons, loading state
CardComponent Card container with header, content, and footer slots
CardWrapper Advanced card with shadow/border/padding variants, image positions, clickable/selected states, loading skeleton
DataTable Full-featured data table with sorting, selection, pagination, error states, striped/bordered/compact options
Table Atomic table wrapper with context provider for styling (striped, bordered, hover, compact, stickyHeader)
Colgroup / Col Column grouping and column styling
Thead / Tbody / Tfoot Table section wrappers (support multiple sections)
Tr Table row with selected, clickable, disabled states
Th Header cell with sortable, colspan/rowspan, scope, alignment, and sticky column support
Td Data cell with colspan/rowspan, alignment, main column styling, truncate, and custom classes
TableHeader Composed header row with sort and select-all functionality
TableRow Composed data row with selection and type rendering
TableFooter Composed footer row for summary/pagination
TableComponent Alias for DataTable (backwards compatibility)
MobileList Mobile-optimized card-based list with selection support
ResponsiveList Combines MobileList (mobile) and TableComponent (desktop) with automatic breakpoint switching
Tabs Tab navigation with variants (underline, pills, boxed)
TabPanel Tab content panel (use with Tabs)
Dropdown Dropdown menu with items, icons, and dividers
Avatar User avatar with image, initials, or icon fallback
AutocompleteComponent Searchable select with keyboard navigation
MenuItem Navigation menu item with icon, active state detection, flyout submenus, and route support
StatusBadge Colored status indicator badge
TableAction Icon button for table row actions
Stepper Multi-step progress indicator with horizontal/vertical orientation
CollapsibleCard Card that can expand/collapse its content
Accordion Expandable content sections with single/multiple mode
AccordionItem Individual accordion panel (use with Accordion)
Breadcrumb Navigation breadcrumb trail
Drawer Slide-out panel from any edge (left, right, top, bottom)
Popover Floating content panel triggered by click or hover
Timeline Vertical timeline for events/history display
Tooltip Hover tooltip with customizable position
DarkModeToggle Dark mode toggle button with icon variants
StatsCard Statistics display card with icon, value, and trend
StatsGrid Grid layout for multiple StatsCard components
FilterTabs Tabbed filter buttons with counts

Form

Component Description
FormInput Text input with validation states, sizes (sm/md/lg), and ARIA support
FormSelect Select dropdown with search, multi-select, and validation
FormGroup Form field wrapper with label, help text, and error states
FormLabel Styled form label with required indicator
FormHelp Help/error text for form fields
FormSection Grouped form section with title and description
FormActions Form button container with alignment options
InputWrapper Wrapper for consistent input styling
SearchInput Search input with icon and clear button
Switch Toggle switch with label and description
Checkbox Checkbox with label, description, and indeterminate state
CheckboxGroup Group of related checkboxes with select all
Combobox Multi-select combobox with search and tags
TagsInput Tag input with add/remove functionality
TextArea Multi-line text input with auto-resize
DatePicker Calendar date picker with min/max dates
ColorPicker Color selection with swatches and custom input
IconPicker Icon selection from Iconify with search
FileUpload Drag-and-drop file upload with preview
Rating Star rating input with half-star support
Slider Single value slider input
RangeSlider Dual-handle range slider
EmailInput Email input with validation
PasswordInput Password input with visibility toggle
PhoneInput Phone number input with formatting
NumberInput Numeric input with increment/decrement
MoneyInput Currency input with formatting
PercentInput Percentage input with formatting
QuantityInput Quantity input with +/- buttons
URLInput URL input with validation
OTPInput One-time password input with multiple digits

Feedback

Component Description
Modal Modal dialog with focus trap, ARIA support, and slots
ConfirmDialog Confirmation modal with customizable actions
Alert Alert banner with variants (info, success, warning, error)
Toast Individual toast notification with auto-dismiss
ToastContainer Toast notification container with positioning
LoadingSpinner Loading indicator with size variants
Progress Progress bar with percentage display
Skeleton Loading placeholder with animation
CardSkeleton Card loading skeleton
ListSkeleton List loading skeleton
TableSkeleton Table loading skeleton
PaginationControls Pagination with page numbers and navigation
NotificationList Notification list container
NotificationComponent Individual notification item
EmptyState Placeholder for empty content with icon and action slot

Layout

Component Description
AuthLayout Split-panel authentication layout with branding and form sections
BaseLayout App shell with sidebar, header, main content area, and route-aware menu
PageLayout Page wrapper with breadcrumbs
PageHero Hero section with title, description, and action slots

Type Display

Component Description
TextType Text value display
NumberType Formatted number display
DateType Formatted date display
BooleanType Boolean value display (check/cross icons)
BadgeType Badge value display with colors

Atomic Table Usage

Build custom tables with granular control using atomic components:

<script setup lang="ts">
import { Table, Thead, Tbody, Tfoot, Tr, Th, Td, Colgroup, Col } from 'cisse-vue-ui'

const data = [
  { id: 1, name: 'John Doe', amount: 1250.00 },
  { id: 2, name: 'Jane Smith', amount: 890.50 },
]
const total = data.reduce((sum, row) => sum + row.amount, 0)
</script>

<template>
  <Table striped bordered>
    <Colgroup>
      <Col width="200px" />
      <Col width="150px" />
    </Colgroup>
    <Thead>
      <Tr>
        <Th sortable>Name</Th>
        <Th align="right">Amount</Th>
      </Tr>
    </Thead>
    <Tbody>
      <Tr v-for="row in data" :key="row.id" clickable @click="select(row)">
        <Td main>{{ row.name }}</Td>
        <Td align="right">{{ row.amount.toFixed(2) }}</Td>
      </Tr>
    </Tbody>
    <Tfoot>
      <Tr>
        <Td>Total</Td>
        <Td align="right">{{ total.toFixed(2) }}</Td>
      </Tr>
    </Tfoot>
  </Table>
</template>

Advanced: Colspan, Rowspan & Row Grouping

<Table bordered>
  <Thead>
    <Tr>
      <Th rowspan="2">Product</Th>
      <Th colspan="2" align="center">Sales</Th>
    </Tr>
    <Tr>
      <Th align="right">Q1</Th>
      <Th align="right">Q2</Th>
    </Tr>
  </Thead>
  <Tbody>
    <!-- Row grouping with scope="rowgroup" -->
    <Tr>
      <Th rowspan="2" scope="rowgroup">Electronics</Th>
      <Td align="right">$45,000</Td>
      <Td align="right">$52,000</Td>
    </Tr>
    <Tr>
      <Td align="right">$38,000</Td>
      <Td align="right">$41,000</Td>
    </Tr>
  </Tbody>
</Table>

All atomic components support v-bind="$attrs" for full customization.

Or use the full-featured DataTable for common use cases:

<script setup lang="ts">
import { DataTable } from 'cisse-vue-ui'
import type { Property } from 'cisse-vue-ui/types'

const properties: Property[] = [
  { name: 'name', label: 'Name', main: true, sortable: true },
  { name: 'email', label: 'Email' },
  { name: 'status', label: 'Active', type: 'boolean' },
]
</script>

<template>
  <DataTable
    :items="users"
    :properties="properties"
    selectable
    striped
    bordered
    @select="handleSelect"
    @sort="handleSort"
  >
    <template #action="{ item }">
      <Button variant="ghost" size="sm" icon="lucide:edit" />
    </template>
  </DataTable>
</template>

Composables

import {
  useNotifications,
  useDarkMode,
  useExportCSV,
  useDropdown,
  useModal,
  useToast,
  useFocusTrap,
  useId,
  useInputStyles
} from 'cisse-vue-ui/composables'

useModal

Manage modal state with data support:

import { useModal } from 'cisse-vue-ui/composables'

// Simple modal
const createModal = useModal()
createModal.open()
createModal.close()

// Modal with data (e.g., for editing)
const editModal = useModal<User>()
editModal.open(selectedUser)
// Access editModal.data.value in template

// With callbacks
const deleteModal = useModal<Item>({
  onOpen: (data) => console.log('Opening with:', data),
  onClose: () => refetchData()
})
<template>
  <!-- Use isOpen for v-model binding -->
  <Modal v-model="editModal.isOpen.value" title="Edit User">
    <FormInput v-model="editModal.data.value.name" label="Name" />
    <template #footer>
      <Button @click="editModal.close()">Cancel</Button>
      <Button variant="primary" @click="save">Save</Button>
    </template>
  </Modal>
</template>

useDropdown

Shared dropdown logic for custom dropdown components (used internally by Dropdown, FormSelect, AutocompleteComponent):

import { useDropdown } from 'cisse-vue-ui/composables'
import { ref } from 'vue'

const triggerRef = ref<HTMLElement>()
const dropdownRef = ref<HTMLElement>()

const {
  isOpen,
  highlightedIndex,
  dropdownStyle,
  open,
  close,
  toggle,
  handleKeydown,
  scrollToHighlighted,
} = useDropdown(triggerRef, dropdownRef, {
  teleport: true,
  align: 'left',
  gap: 8,
  onOpen: () => console.log('Opened'),
  onClose: () => console.log('Closed'),
})

useNotifications

const { notifications, addNotification, removeNotification } = useNotifications()

addNotification({
  type: 'success',
  title: 'Saved',
  message: 'Your changes have been saved.'
})

useDarkMode

const { isDark, toggle, enable, disable } = useDarkMode({
  selector: 'html',      // Element to add .dark class
  storageKey: 'theme',   // localStorage key
  defaultDark: false     // Default state
})

useExportCSV

const { exportToCSV } = useExportCSV()

exportToCSV(data, columns, 'export.csv')

useToast

Toast notification system with positioning and auto-dismiss:

import { useToast } from 'cisse-vue-ui/composables'

const { toasts, addToast, removeToast, clearToasts } = useToast()

// Add a toast
addToast({
  type: 'success',
  title: 'Success!',
  message: 'Your changes have been saved.',
  duration: 5000  // auto-dismiss after 5s
})

// Different toast types
addToast({ type: 'error', title: 'Error', message: 'Something went wrong' })
addToast({ type: 'warning', title: 'Warning', message: 'Please review' })
addToast({ type: 'info', title: 'Info', message: 'New update available' })
<template>
  <!-- Add ToastContainer to your app root -->
  <ToastContainer position="top-right" />
</template>

useFocusTrap

Trap focus within a container (used internally by Modal):

import { useFocusTrap } from 'cisse-vue-ui/composables'
import { ref } from 'vue'

const isActive = ref(true)
const { containerRef } = useFocusTrap({
  active: isActive,
  focusFirst: true,      // Focus first focusable element on activate
  restoreFocus: true     // Restore focus on deactivate
})

useId

Generate unique IDs for accessibility (ARIA relationships):

import { useId } from 'cisse-vue-ui/composables'

const { id, related } = useId({ prefix: 'modal' })
// id.value = 'cisse-modal-1'
// related('title') = 'cisse-modal-1-title'
// related('description') = 'cisse-modal-1-description'
<template>
  <div :id="id" role="dialog" :aria-labelledby="related('title')">
    <h2 :id="related('title')">Dialog Title</h2>
  </div>
</template>

useInputStyles

Centralized input styling for form components with consistent sizing, states, and dark mode:

import { useInputStyles } from 'cisse-vue-ui/composables'
import { ref, computed } from 'vue'

const size = ref<'sm' | 'md' | 'lg'>('md')
const disabled = ref(false)
const invalid = ref(false)

const {
  inputClasses,      // Classes for input elements
  triggerClasses,    // Classes for dropdown triggers
  wrapperClasses,    // Classes for input wrappers
  iconClasses,       // Classes for input icons
} = useInputStyles({
  size,
  disabled,
  invalid,
  focused: computed(() => false)
})

Size variants:

  • sm: Smaller text (text-sm), reduced padding (px-2.5 py-1.5)
  • md: Default size (text-sm), standard padding (px-3 py-2)
  • lg: Larger text (text-base), increased padding (px-4 py-2.5)

Table Composables

Powerful composables for building advanced data tables:

import {
  usePagination,
  useTableKeyboardNavigation,
  useColumnVisibility,
  useColumnResize,
  usePinnedRows,
  useEditableCell,
  useVirtualScroll
} from 'cisse-vue-ui/composables'

usePagination

Client-side pagination with page size selector support:

import { usePagination } from 'cisse-vue-ui/composables'
import { ref } from 'vue'

const items = ref([/* your data */])

const {
  currentPage,      // Current page (1-indexed)
  pageSize,         // Items per page
  totalPages,       // Total number of pages
  paginatedItems,   // Items for current page
  startIndex,       // First item index (0-indexed)
  endIndex,         // Last item index (0-indexed)
  hasNextPage,      // Can go forward?
  hasPreviousPage,  // Can go back?
  goToPage,         // Navigate to specific page
  nextPage,         // Go to next page
  previousPage,     // Go to previous page
  setPageSize,      // Change items per page
} = usePagination(items, {
  initialPage: 1,
  initialPageSize: 10,
  pageSizes: [10, 25, 50, 100]
})
<template>
  <DataTable
    :items="items"
    :properties="properties"
    paginated
    :page-size="25"
    :page-sizes="[10, 25, 50, 100]"
    @page-change="handlePageChange"
  />
</template>

useTableKeyboardNavigation

Keyboard navigation for accessible tables:

import { useTableKeyboardNavigation } from 'cisse-vue-ui/composables'
import { ref } from 'vue'

const tableRef = ref<HTMLElement>()
const items = ref([/* your data */])

const {
  focusedRowIndex,  // Currently focused row
  focusedCellIndex, // Currently focused cell
  handleKeydown,    // Keyboard event handler
  focusRow,         // Focus specific row
  focusCell,        // Focus specific cell
} = useTableKeyboardNavigation({
  tableRef,
  items,
  onSelect: (index) => console.log('Selected row:', index),
  onActivate: (index) => console.log('Activated row:', index),
})
<template>
  <Table ref="tableRef" @keydown="handleKeydown">
    <Tbody>
      <Tr
        v-for="(item, index) in items"
        :key="item.id"
        :tabindex="focusedRowIndex === index ? 0 : -1"
        :class="{ 'ring-2 ring-primary-500': focusedRowIndex === index }"
      >
        <Td>{{ item.name }}</Td>
      </Tr>
    </Tbody>
  </Table>
</template>

useColumnVisibility

Toggle column visibility with persistence:

import { useColumnVisibility, type Column } from 'cisse-vue-ui/composables'

const columns: Column[] = [
  { key: 'name', label: 'Name', visible: true, required: true },
  { key: 'email', label: 'Email', visible: true },
  { key: 'phone', label: 'Phone', visible: false },
  { key: 'address', label: 'Address', visible: false },
]

const {
  visibleColumns,     // Columns with visible: true
  hiddenColumns,      // Columns with visible: false
  toggleColumn,       // Toggle single column visibility
  showColumn,         // Show a column
  hideColumn,         // Hide a column
  showAllColumns,     // Show all columns
  hideAllColumns,     // Hide all except required
  resetColumns,       // Reset to initial state
  isColumnVisible,    // Check if column is visible
} = useColumnVisibility({
  columns,
  storageKey: 'my-table-columns' // Optional: persist to localStorage
})
<template>
  <div class="mb-4 flex gap-2">
    <Checkbox
      v-for="col in columns"
      :key="col.key"
      :model-value="isColumnVisible(col.key)"
      :disabled="col.required"
      :label="col.label"
      @update:model-value="toggleColumn(col.key)"
    />
  </div>

  <Table>
    <Thead>
      <Tr>
        <Th v-for="col in visibleColumns" :key="col.key">{{ col.label }}</Th>
      </Tr>
    </Thead>
    <Tbody>
      <Tr v-for="item in items" :key="item.id">
        <Td v-for="col in visibleColumns" :key="col.key">
          {{ item[col.key] }}
        </Td>
      </Tr>
    </Tbody>
  </Table>
</template>

useColumnResize

Drag-to-resize columns with constraints:

import { useColumnResize } from 'cisse-vue-ui/composables'

const {
  columnWidths,     // Map of column key to width
  isResizing,       // Currently resizing?
  resizingColumn,   // Which column is being resized
  startResize,      // Begin resize operation
  getColumnWidth,   // Get width for column
  setColumnWidth,   // Set width for column
  resetColumnWidth, // Reset column to default
  resetAllWidths,   // Reset all columns
} = useColumnResize({
  defaultWidth: 150,
  minWidth: 80,
  maxWidth: 500,
  storageKey: 'my-table-widths' // Optional: persist to localStorage
})
<template>
  <Table>
    <Thead>
      <Tr>
        <Th
          v-for="col in columns"
          :key="col.key"
          :style="{ width: getColumnWidth(col.key) + 'px' }"
          resizable
          :resizing="resizingColumn === col.key"
          @resize-start="(e) => startResize(col.key, e)"
        >
          {{ col.label }}
        </Th>
      </Tr>
    </Thead>
  </Table>
</template>

usePinnedRows

Pin rows to top or bottom of table:

import { usePinnedRows } from 'cisse-vue-ui/composables'

interface User {
  id: number
  name: string
  email: string
}

const items = ref<User[]>([/* your data */])

const {
  pinnedTop,        // Items pinned to top
  pinnedBottom,     // Items pinned to bottom
  unpinnedItems,    // Items not pinned
  isPinned,         // Check if item is pinned
  getPinPosition,   // Get pin position ('top' | 'bottom' | null)
  pinToTop,         // Pin item to top
  pinToBottom,      // Pin item to bottom
  unpin,            // Unpin item
  togglePin,        // Toggle pin state
  clearPinned,      // Clear all pinned items
} = usePinnedRows<User>({
  items,
  keyField: 'id',
  maxPinned: 5      // Optional: limit pinned rows
})
<template>
  <Table>
    <Tbody>
      <!-- Pinned top rows -->
      <Tr
        v-for="item in pinnedTop"
        :key="item.id"
        class="bg-yellow-50 dark:bg-yellow-900/20"
      >
        <Td>
          <Button size="sm" @click="unpin(String(item.id))">Unpin</Button>
        </Td>
        <Td>{{ item.name }}</Td>
      </Tr>

      <!-- Regular rows -->
      <Tr v-for="item in unpinnedItems" :key="item.id">
        <Td>
          <Button size="sm" @click="pinToTop(String(item.id), item)">Pin</Button>
        </Td>
        <Td>{{ item.name }}</Td>
      </Tr>

      <!-- Pinned bottom rows -->
      <Tr
        v-for="item in pinnedBottom"
        :key="item.id"
        class="bg-blue-50 dark:bg-blue-900/20"
      >
        <Td>
          <Button size="sm" @click="unpin(String(item.id))">Unpin</Button>
        </Td>
        <Td>{{ item.name }}</Td>
      </Tr>
    </Tbody>
  </Table>
</template>

useEditableCell

Inline cell editing with validation:

import { useEditableCell } from 'cisse-vue-ui/composables'

interface User {
  id: number
  name: string
  email: string
}

const {
  editingCell,      // Current cell being edited { rowKey, field }
  editingValue,     // Current edit value
  isEditing,        // Is any cell being edited?
  isSaving,         // Is save in progress?
  startEditing,     // Start editing a cell
  saveEdit,         // Save current edit
  cancelEdit,       // Cancel current edit
  getCellValue,     // Get original value for cell
} = useEditableCell<User>({
  onSave: async ({ rowKey, field, oldValue, newValue }) => {
    // Save to backend
    await api.updateUser(rowKey, { [field]: newValue })
  },
  validate: ({ field, newValue }) => {
    if (field === 'email' && !newValue.includes('@')) {
      return 'Invalid email address'
    }
    return true
  }
})
<template>
  <Table>
    <Tbody>
      <Tr v-for="item in items" :key="item.id">
        <Td @dblclick="startEditing(String(item.id), 'name', item.name)">
          <template v-if="editingCell?.rowKey === String(item.id) && editingCell?.field === 'name'">
            <input
              v-model="editingValue"
              @keydown.enter="saveEdit"
              @keydown.escape="cancelEdit"
              @blur="saveEdit"
              class="border rounded px-2 py-1"
            />
          </template>
          <template v-else>
            {{ item.name }}
          </template>
        </Td>
      </Tr>
    </Tbody>
  </Table>
</template>

useVirtualScroll

Virtual scrolling for large datasets:

import { useVirtualScroll } from 'cisse-vue-ui/composables'

const items = ref([/* 10,000+ items */])

const {
  visibleItems,     // Items to render
  totalHeight,      // Total scroll height
  offsetY,          // Top offset for positioning
  startIndex,       // First visible item index
  endIndex,         // Last visible item index
  scrollTop,        // Current scroll position
  onScroll,         // Scroll event handler
  scrollToIndex,    // Scroll to specific item
  containerRef,     // Ref for scroll container
} = useVirtualScroll({
  items,
  rowHeight: 48,         // Height of each row in px
  containerHeight: 500,  // Visible container height
  overscan: 5            // Extra rows to render (buffer)
})
<template>
  <div
    ref="containerRef"
    class="overflow-auto"
    :style="{ height: '500px' }"
    @scroll="onScroll"
  >
    <!-- Spacer for total height -->
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <!-- Positioned visible items -->
      <div :style="{ transform: `translateY(${offsetY}px)` }">
        <Table>
          <Tbody>
            <Tr
              v-for="(item, index) in visibleItems"
              :key="item.id"
              :style="{ height: '48px' }"
            >
              <Td>{{ startIndex + index + 1 }}</Td>
              <Td>{{ item.name }}</Td>
            </Tr>
          </Tbody>
        </Table>
      </div>
    </div>
  </div>

  <!-- Jump to row -->
  <Button @click="scrollToIndex(5000)">Jump to row 5000</Button>
</template>

Types

import type { Property, Notification, Breadcrumb } from 'cisse-vue-ui/types'

// Table column definition
const columns: Property[] = [
  { key: 'name', label: 'Name', sortable: true },
  { key: 'email', label: 'Email' },
  { key: 'status', label: 'Status', type: 'badge' }
]

// Breadcrumb navigation
const breadcrumbs: Breadcrumb[] = [
  { label: 'Home', to: '/' },
  { label: 'Users', to: '/users' },
  { label: 'Edit' }
]

Component Examples

Button

<Button variant="primary" size="md" :loading="isLoading">
  Save Changes
</Button>

<Button variant="outline" icon="lucide:plus">
  Add Item
</Button>

<Button variant="danger" icon="lucide:trash">
  Delete
</Button>

Tabs

<script setup>
import { ref } from 'vue'
import { Tabs, TabPanel } from 'cisse-vue-ui'

const activeTab = ref('profile')
const tabs = [
  { key: 'profile', label: 'Profile' },
  { key: 'settings', label: 'Settings' },
  { key: 'notifications', label: 'Notifications' }
]
</script>

<template>
  <Tabs v-model="activeTab" :tabs="tabs" variant="underline">
    <TabPanel value="profile">Profile content</TabPanel>
    <TabPanel value="settings">Settings content</TabPanel>
    <TabPanel value="notifications">Notifications content</TabPanel>
  </Tabs>
</template>

Switch

<Switch
  v-model="emailNotifications"
  label="Email notifications"
  description="Receive email updates about your account"
/>

Alert

<Alert variant="success" title="Success!" dismissible>
  Your changes have been saved successfully.
</Alert>

<Alert variant="error" title="Error">
  Something went wrong. Please try again.
</Alert>

Dropdown

<script setup>
import { Dropdown } from 'cisse-vue-ui'

const items = [
  { key: 'edit', label: 'Edit', icon: 'lucide:edit' },
  { key: 'duplicate', label: 'Duplicate', icon: 'lucide:copy' },
  { key: 'divider', divider: true },
  { key: 'delete', label: 'Delete', icon: 'lucide:trash', danger: true }
]

const handleSelect = (item) => {
  console.log('Selected:', item.key)
}
</script>

<template>
  <Dropdown :items="items" @select="handleSelect">
    <template #trigger-label>Actions</template>
  </Dropdown>
</template>

Stepper

<script setup>
import { ref } from 'vue'
import { Stepper } from 'cisse-vue-ui'

const currentStep = ref('step2')
const steps = [
  { key: 'step1', title: 'Account', description: 'Create account', icon: 'lucide:user' },
  { key: 'step2', title: 'Profile', description: 'Set up profile', icon: 'lucide:settings' },
  { key: 'step3', title: 'Complete', description: 'Ready to go!', icon: 'lucide:check' }
]
</script>

<template>
  <Stepper v-model="currentStep" :steps="steps" />
</template>

EmptyState

<EmptyState
  title="No results found"
  message="Try adjusting your search or filters"
  icon="lucide:search-x"
>
  <template #action>
    <Button variant="primary" size="sm">Clear filters</Button>
  </template>
</EmptyState>

Checkbox

<Checkbox
  v-model="accepted"
  label="Accept terms"
  description="I agree to the terms and conditions"
/>

CardWrapper

<script setup>
import { CardWrapper, Button } from 'cisse-vue-ui'
</script>

<template>
  <!-- Basic card with title and icon -->
  <CardWrapper
    title="Dashboard"
    description="Overview of your account"
    icon="lucide:layout-dashboard"
  >
    <div class="p-5">
      <p>Card content goes here.</p>
    </div>
  </CardWrapper>

  <!-- Card with image -->
  <CardWrapper
    title="Product"
    image="/product.jpg"
    image-position="top"
    image-height="200px"
  >
    <div class="p-5">Product description</div>
  </CardWrapper>

  <!-- Clickable card with accent -->
  <CardWrapper
    title="Clickable Card"
    accent="primary"
    clickable
    @click="handleClick"
  >
    <div class="p-5">Click me!</div>
  </CardWrapper>

  <!-- Card with actions -->
  <CardWrapper title="Settings" icon="lucide:settings">
    <template #actions>
      <Button size="sm" variant="outline">Cancel</Button>
      <Button size="sm">Save</Button>
    </template>
    <div class="p-5">Settings content</div>
    <template #footer>
      <p class="text-sm text-gray-500">Last updated: Today</p>
    </template>
  </CardWrapper>
</template>
CardWrapper Props
Prop Type Default Description
title string - Card title
description string - Card description
icon string - Header icon (Iconify format)
shadow 'none' | 'sm' | 'md' | 'lg' | 'xl' 'md' Shadow level
rounded 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full' 'lg' Border radius
padding 'none' | 'sm' | 'md' | 'lg' 'none' Content padding
border 'none' | 'default' | 'primary' | 'secondary' 'none' Border style
variant 'default' | 'glass' | 'outline' | 'flat' 'default' Visual variant
accent 'none' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' 'none' Colored accent border
clickable boolean false Make card clickable with hover effects
selected boolean false Selected state with ring indicator
disabled boolean false Disabled state
image string - Image URL
imagePosition 'top' | 'bottom' | 'left' | 'right' | 'background' 'top' Image position
loading boolean false Show loading skeleton

IconPicker

<script setup>
import { ref } from 'vue'
import { IconPicker } from 'cisse-vue-ui'

const selectedIcon = ref('mdi:heart')
</script>

<template>
  <IconPicker
    v-model="selectedIcon"
    label="Select Icon"
    help="Choose an icon for your item"
    :collections="['mdi', 'heroicons', 'lucide']"
  />
</template>

TableComponent

<script setup>
import { ref } from 'vue'
import { TableComponent } from 'cisse-vue-ui'

const properties = [
  { name: 'name', label: 'Name', main: true },
  { name: 'email', label: 'Email' },
  { name: 'role', label: 'Role', type: 'badge' }
]

const items = [
  { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' }
]

// Selection support
const selectedItems = ref(new Set())
const toggleSelect = (id) => {
  if (selectedItems.value.has(id)) {
    selectedItems.value.delete(id)
  } else {
    selectedItems.value.add(id)
  }
}
</script>

<template>
  <TableComponent
    :properties="properties"
    :items="items"
    selectable
    :selected-items="selectedItems"
    @select="toggleSelect"
    @select-all="toggleSelectAll"
  >
    <template #action="{ item }">
      <TableAction icon="lucide:edit" @click="edit(item)" />
      <TableAction icon="lucide:trash" variant="danger" @click="delete(item)" />
    </template>
  </TableComponent>
</template>

ResponsiveList

A component that automatically switches between a mobile card layout and a desktop table layout based on screen size.

<script setup>
import { ref } from 'vue'
import { ResponsiveList } from 'cisse-vue-ui'

const columns = [
  { key: 'name', label: 'Name' },
  { key: 'email', label: 'Email' },
  { key: 'status', label: 'Status' }
]

const items = [
  { id: 1, name: 'John Doe', email: 'john@example.com', status: 'active' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'inactive' }
]

const selectedItems = ref(new Set())

const toggleSelect = (id) => {
  if (selectedItems.value.has(id)) {
    selectedItems.value.delete(id)
  } else {
    selectedItems.value.add(id)
  }
}

const toggleSelectAll = () => {
  if (selectedItems.value.size === items.length) {
    selectedItems.value.clear()
  } else {
    items.forEach(item => selectedItems.value.add(String(item.id)))
  }
}
</script>

<template>
  <ResponsiveList
    :items="items"
    :columns="columns"
    key-field="id"
    selectable
    :selected-items="selectedItems"
    breakpoint="lg"
    @select="toggleSelect"
    @select-all="toggleSelectAll"
  >
    <!-- Mobile view: avatar -->
    <template #avatar="{ item }">
      <div class="w-10 h-10 rounded-full bg-primary-500 flex items-center justify-center text-white">
        {{ item.name[0] }}
      </div>
    </template>

    <!-- Mobile view: content -->
    <template #mobileContent="{ item }">
      <h3 class="font-semibold">{{ item.name }}</h3>
      <p class="text-sm text-gray-500">{{ item.email }}</p>
    </template>

    <!-- Mobile view: actions -->
    <template #mobileActions="{ item }">
      <button @click="viewItem(item)">View</button>
    </template>

    <!-- Desktop table: custom cell rendering -->
    <template #cell-name="{ item }">
      <span class="font-medium">{{ item.name }}</span>
    </template>

    <template #cell-status="{ item }">
      <span :class="item.status === 'active' ? 'text-green-600' : 'text-red-600'">
        {{ item.status }}
      </span>
    </template>

    <!-- Desktop table: actions column -->
    <template #actions="{ item }">
      <Button size="sm" variant="ghost" @click="edit(item)">Edit</Button>
    </template>

    <!-- Empty state -->
    <template #empty>
      <EmptyState title="No items" message="No items to display" />
    </template>
  </ResponsiveList>
</template>
ResponsiveList Props
Prop Type Default Description
items Array required Array of items to display
columns Array required Column definitions with key or name, label, and optional type
keyField string 'id' Field to use as unique key for items
selectable boolean false Enable selection mode
selectedItems Set<string> - Set of selected item keys
selectableFilter Function - Filter function to determine if an item is selectable
breakpoint string 'lg' Breakpoint for switching views: 'sm', 'md', 'lg', 'xl', '2xl'

MobileList

A mobile-optimized card-based list component with selection support.

<script setup>
import { MobileList } from 'cisse-vue-ui'
</script>

<template>
  <MobileList
    :items="items"
    key-field="id"
    selectable
    :selected-items="selectedItems"
    @select="toggleSelect"
    @select-all="toggleSelectAll"
  >
    <template #avatar="{ item }">
      <div class="w-12 h-12 rounded-full bg-blue-500" />
    </template>

    <template #content="{ item }">
      <h3>{{ item.name }}</h3>
      <p>{{ item.description }}</p>
    </template>

    <template #actions="{ item }">
      <button>View</button>
    </template>
  </MobileList>
</template>

MenuItem

<script setup>
import { useRoute } from 'vue-router'
import { MenuItem } from 'cisse-vue-ui'

const route = useRoute()

const menuItem = {
  label: 'Dashboard',
  link: '/dashboard',
  icon: 'lucide:layout-dashboard'
}
</script>

<template>
  <!-- Auto-detect active state from current route -->
  <MenuItem :menu-item="menuItem" :current-path="route.path" />

  <!-- Or manually control active state -->
  <MenuItem :menu-item="menuItem" :active="true" />
</template>

AuthLayout

Split-panel layout for authentication pages (login, register, password reset).

<script setup>
import { ref } from 'vue'
import { AuthLayout, type AuthFeature } from 'cisse-vue-ui'

const email = ref('')
const password = ref('')

const features: AuthFeature[] = [
  { icon: 'lucide:shield', text: 'Secure authentication' },
  { icon: 'lucide:zap', text: 'Fast login' },
  { icon: 'lucide:users', text: 'Team collaboration' },
]

function handleSubmit() {
  // Handle login
}
</script>

<template>
  <AuthLayout
    app-name="My App"
    app-icon="lucide:box"
    headline="Welcome to"
    sub-headline="My Application"
    description="Sign in to access your account."
    :features="features"
    form-title="Sign In"
    form-subtitle="Enter your credentials"
    home-link="/"
  >
    <form @submit.prevent="handleSubmit" class="space-y-4">
      <input v-model="email" type="email" placeholder="Email" class="..." />
      <input v-model="password" type="password" placeholder="Password" class="..." />
      <button type="submit">Sign In</button>
    </form>

    <template #form-footer>
      <p class="text-center mt-6">
        Don't have an account? <a href="https://github.com/moulayecisse/cisse-vue-ui/blob/HEAD/register">Sign up</a>
      </p>
    </template>
  </AuthLayout>
</template>
AuthLayout Props
Prop Type Default Description
appName string '' Application name displayed in logo
appIcon string 'lucide:box' Iconify icon for app logo
headline string '' First line of headline
subHeadline string '' Second line with decorative underline
description string '' Description paragraph
features AuthFeature[] [] List of features with icon and text
gradientFrom string 'from-primary-700' Tailwind gradient start class
gradientVia string '' Tailwind gradient middle class
gradientTo string 'to-primary-800' Tailwind gradient end class
showDecorations boolean true Show floating decorative shapes
showPattern boolean true Show dot pattern overlay
underlineColor string 'rgba(165, 180, 252, 0.5)' Sub-headline underline color
formTitle string '' Form panel title
formSubtitle string '' Form panel subtitle
homeLink string '/' Mobile logo link URL
brandingAnimation string '' CSS class for branding animation
formAnimation string '' CSS class for form panel animation
AuthLayout Slots
Slot Description
default Form content (inside the white card)
branding-panel Complete override of the branding panel
branding-logo Custom logo in branding panel
branding-headline Custom headline content
branding-features Custom features list
branding-content Additional content after features
mobile-logo Custom mobile logo
form-header Content above the form card
form-footer Content below the form card

BaseLayout

<script setup>
import { useRoute } from 'vue-router'
import { BaseLayout } from 'cisse-vue-ui'

const route = useRoute()

const menuItems = [
  { label: 'Dashboard', link: '/', icon: 'lucide:home' },
  { label: 'Users', link: '/users', icon: 'lucide:users' },
  { label: 'Settings', link: '/settings', icon: 'lucide:settings' }
]
</script>

<template>
  <BaseLayout
    :menu-items="menuItems"
    :current-path="route.path"
    :show-dark-toggle="true"
    menu-position="top"
  >
    <template #logo>
      <img src="https://raw.githubusercontent.com/moulayecisse/cisse-vue-ui/HEAD/logo.svg" alt="Logo" class="h-8" />
    </template>

    <RouterView />
  </BaseLayout>
</template>
BaseLayout Props
Prop Type Default Description
menuItems MenuItemProps[] [] Menu items for the sidebar
appName string 'App' App/brand name displayed in sidebar
appIcon string 'lucide:box' App icon (Iconify icon name)
sidebarOpen boolean true Whether sidebar is open (v-model:sidebarOpen)
dark boolean false Whether dark mode is enabled (v-model:dark)
showDarkToggle boolean true Show dark mode toggle in header
sidebarClass string 'bg-[#172b4c]...' CSS classes for sidebar background
currentPath string - Current route path for menu active state
userName string - User display name
userAvatar string - User avatar (initials or image URL)
userMenuItems UserMenuItem[] [] User menu dropdown items
menuPosition 'top' | 'center' | 'bottom' 'top' Menu vertical position in sidebar
BaseLayout Slots
Slot Description
default Main content area (or renders RouterView if available)
logo Custom logo in sidebar header
menu Custom menu content (receives currentPath)
sidebar-footer Content at bottom of sidebar
header-center Center content in header
header-actions Action buttons in header (before dark toggle)

Dark Mode

Components support dark mode via the .dark class on a parent element:

<html class="dark">
  <!-- Components will use dark theme -->
</html>

Use the useDarkMode composable or implement your own toggle:

const { isDark, toggle } = useDarkMode()

License

MIT

Related Packages

ketekny-ui-kit

A Vue 3 UI component library with Tailwind CSS styling

25,243
@caneara/varnish

A library of UI components built using Vue.js and TailwindCSS.

2,792 78
@cssninja/nuxt-ui

🖌️ A beautiful, customizable, and simple to use component library with Tailwind...

2,651

Version History

Version Released Vue License
1.0.0 >=3.4.0 MIT
0.11.0 >=3.4.0 MIT
0.10.1 >=3.4.0 MIT
0.10.0 >=3.4.0 MIT
0.9.0 >=3.4.0 MIT