nicklabs-ui
v1.0.46
Published
A Vue 3 component library with glassmorphism design, built for modern web applications.
Readme
nicklabs-ui
A Vue 3 component library with glassmorphism design, built for modern web applications.
Version: 1.0.39 | Framework: Vue 3.5+
Table of Contents
Installation
npm install nicklabs-ui
# or
pnpm add nicklabs-uiSetup
1. Import Styles
In your main entry file (e.g., main.ts):
import { createApp } from 'vue'
import App from './App.vue'
// Required: reset and CSS variables
import 'nicklabs-ui/reset.css'
import 'nicklabs-ui/variables.css'
// Required: component styles
import 'nicklabs-ui/nicklabs-ui.css'
createApp(App).mount('#app')2. Import Components
Import components individually as needed:
import { NButton, NInput, NModal, useToast } from 'nicklabs-ui'3. Register All Components (Optional)
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { NickLabsUI } from 'nicklabs-ui'
const app = createApp(App)
app.use(router)
app.use(NickLabsUI, { router })
app.mount('#app')Passing
routerenablesuseBreadcrumbanduseRouteModalwithout any additional setup.
Components
Form Components
NButton
A versatile button supporting multiple visual variants and semantic intents.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | "none" \| "solid" \| "outline" \| "ghost" \| "mute" | "solid" | Visual style |
| intent | "none" \| "primary" \| "error" \| "success" \| "warning" \| "info" | "none" | Semantic color intent |
| size | "sm" \| "md" \| "lg" | "md" | Button size |
| radiusSize | "sm" \| "md" \| "lg" \| "xl" \| "full" | "md" | Border radius |
| disabled | boolean | false | Disable interaction |
| type | "button" \| "submit" \| "reset" | "button" | HTML button type |
| square | boolean | false | Equal width/height (icon button) |
| padding | string | — | Custom padding override |
| width | string | — | Custom width override |
| height | string | — | Custom height override |
Usage
<template>
<!-- Basic -->
<NButton>Click me</NButton>
<!-- Variants -->
<NButton variant="solid" intent="primary">Primary</NButton>
<NButton variant="outline" intent="success">Success</NButton>
<NButton variant="ghost" intent="warning">Warning</NButton>
<NButton variant="mute" intent="error">Danger</NButton>
<!-- Sizes -->
<NButton size="sm">Small</NButton>
<NButton size="md">Medium</NButton>
<NButton size="lg">Large</NButton>
<!-- Submit button -->
<NButton type="submit" intent="primary">Submit</NButton>
<!-- Disabled -->
<NButton disabled>Disabled</NButton>
<!-- Icon button (square) -->
<NButton square size="sm">
<svg>...</svg>
</NButton>
</template>
<script setup>
import { NButton } from 'nicklabs-ui'
</script>NInput
A full-featured text input with support for password visibility, number controls, and clearing.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| modelValue | string \| number | — | Bound value (v-model) |
| type | string | "text" | HTML input type |
| placeholder | string | — | Placeholder text |
| disabled | boolean | false | Disable input |
| readonly | boolean | false | Read-only mode |
| inline | boolean | false | Display title and input on the same line |
| clearable | boolean | false | Show clear button |
| maxlength | number | — | Maximum character length |
| min | number | — | Minimum value (for type="number") |
| max | number | — | Maximum value (for type="number") |
| title | string | — | Label above the input |
| autocomplete | string | — | HTML autocomplete attribute |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| update:modelValue | string \| number | Value changed |
| focus | FocusEvent | Input focused |
| blur | FocusEvent | Input blurred |
| input | Event | Input event |
| change | Event | Change event |
| keydown | KeyboardEvent | Key pressed |
| clear | — | Clear button clicked |
Usage
<template>
<NInput v-model="username" placeholder="Enter username" title="Username" />
<!-- Password with visibility toggle -->
<NInput v-model="password" type="password" placeholder="Enter password" />
<!-- Clearable -->
<NInput v-model="search" placeholder="Search..." clearable />
<!-- Number with min/max -->
<NInput v-model="age" type="number" :min="0" :max="120" title="Age" />
<!-- Inline title -->
<NInput v-model="username" title="Username" placeholder="Enter username" inline />
</template>
<script setup>
import { ref } from 'vue'
import { NInput } from 'nicklabs-ui'
const username = ref('')
const password = ref('')
const search = ref('')
const age = ref(0)
</script>NTextarea
Multi-line text input with optional character count display.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| modelValue | string | — | Bound value (v-model) |
| placeholder | string | — | Placeholder text |
| disabled | boolean | false | Disable textarea |
| readonly | boolean | false | Read-only mode |
| rows | number | 4 | Visible row count |
| maxLength | number | — | Maximum character count |
| title | string | — | Label above the textarea |
| showCount | boolean | false | Show character counter |
| wrap | "soft" \| "off" | "soft" | Text wrapping behavior |
| autofocus | boolean | false | Auto-focus on mount |
| autocomplete | string | — | HTML autocomplete attribute |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| update:modelValue | string | Value changed |
| focus | FocusEvent | Textarea focused |
| blur | FocusEvent | Textarea blurred |
| input | Event | Input event |
| change | Event | Change event |
| keydown | KeyboardEvent | Key pressed |
| paste | ClipboardEvent | Content pasted |
Usage
<template>
<NTextarea
v-model="description"
title="Description"
placeholder="Enter description..."
:rows="5"
:maxLength="500"
showCount
/>
</template>
<script setup>
import { ref } from 'vue'
import { NTextarea } from 'nicklabs-ui'
const description = ref('')
</script>NCheckbox
Checkbox input supporting both single and multi-option modes.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| modelValue | boolean \| any[] | — | Bound value (v-model) |
| multiple | boolean | false | Enable multi-option mode |
| options | Record<string, any>[] | — | Options for multi mode |
| disabled | boolean | false | Disable input |
| title | string | — | Group label displayed above options |
| label | string | — | Text beside checkbox in single mode (overridable by default slot) |
| inline | boolean | false | Display title and options on the same line |
| direction | "row" \| "column" | "row" | Layout direction for options |
| autofocus | boolean | false | Auto-focus on mount |
| allowDeselect | boolean | true | Allow deselecting in single mode; set to false to prevent unchecking once checked |
| formatLabel | (option: any) => string | (opt) => opt.label | Function to extract display text from an option |
| formatValue | (option: any) => any | (opt) => opt.value | Function to extract the value from an option |
Events
| Event | Description |
|-------|-------------|
| update:modelValue | Value changed |
| change | Generic change event |
| change:item | Selected OptionItem changed |
| change:value | Selected value changed |
| change:values | Selected values array changed (multiple mode) |
Usage
<template>
<!-- Single checkbox -->
<NCheckbox v-model="agreed" title="I agree to terms" />
<!-- Multiple checkboxes -->
<NCheckbox
v-model="selected"
multiple
:options="options"
direction="column"
title="Select interests"
/>
<!-- Inline title -->
<NCheckbox v-model="selected" multiple :options="options" title="Interests" inline />
<!-- Custom field names -->
<NCheckbox
v-model="selected"
multiple
:options="[{ name: 'Apple', id: 'apple' }, { name: 'Banana', id: 'banana' }]"
:format-label="(opt) => opt.name"
:format-value="(opt) => opt.id"
/>
</template>
<script setup>
import { ref } from 'vue'
import { NCheckbox } from 'nicklabs-ui'
const agreed = ref(false)
const selected = ref([])
const options = [
{ label: 'Vue', value: 'vue' },
{ label: 'React', value: 'react' },
{ label: 'Angular', value: 'angular' },
]
</script>NSelect
Dropdown select with search, multi-select, and clear support.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| modelValue | any \| any[] | — | Bound value (v-model) |
| options | OptionItem[] | [] | Dropdown options |
| multiple | boolean | false | Allow multiple selection |
| searchable | boolean | false | Enable search/filter |
| clearable | boolean | false | Show clear button |
| placeholder | string | — | Placeholder text |
| disabled | boolean | false | Disable input |
| title | string | — | Label above dropdown |
| multipleDisplay | "count" \| "tags" | "tags" | How selected items display |
| formatLabel | (option: any) => string | (opt) => opt.label | Function to extract display text from an option |
| formatValue | (option: any) => any | (opt) => opt.value | Function to extract the value from an option |
Events
| Event | Description |
|-------|-------------|
| update:modelValue | Value changed |
| change | Generic change event |
| change:item | Selected OptionItem |
| change:value | Selected value |
| change:values | Selected values array (multiple mode) |
Usage
<template>
<!-- Basic select -->
<NSelect v-model="city" :options="cities" placeholder="Select city" title="City" />
<!-- Searchable + clearable -->
<NSelect v-model="country" :options="countries" searchable clearable />
<!-- Multiple selection -->
<NSelect
v-model="tags"
:options="tagOptions"
multiple
multipleDisplay="tags"
title="Tags"
/>
<!-- Custom keys (API data with non-standard field names) -->
<NSelect
v-model="city"
:options="[{ name: 'Taipei', id: 'taipei' }, { name: 'Tokyo', id: 'tokyo' }]"
:format-label="(opt) => opt.name"
:format-value="(opt) => opt.id"
placeholder="Select city"
/>
<!-- Combine multiple fields into display text -->
<NSelect
v-model="city"
:options="apiData"
:format-label="(opt) => `${opt.name} (${opt.code})`"
:format-value="(opt) => opt.id"
/>
</template>
<script setup>
import { ref } from 'vue'
import { NSelect } from 'nicklabs-ui'
const city = ref('')
const country = ref(null)
const tags = ref([])
const cities = [
{ label: 'Taipei', value: 'taipei' },
{ label: 'Tokyo', value: 'tokyo' },
{ label: 'Seoul', value: 'seoul' },
]
const tagOptions = [
{ label: 'Frontend', value: 'frontend' },
{ label: 'Backend', value: 'backend' },
{ label: 'DevOps', value: 'devops' },
]
</script>NFileSelect
Drag-and-drop file selector.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| multiple | boolean | false | Allow multiple file selection |
| disabled | boolean | false | Disable input |
| label | string | — | Button/drop zone label |
| hint | string | — | Helper text below |
| accept | string | — | Accepted MIME types (e.g. "image/*") |
| title | string | — | Label above component |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| change:file | File | Single file selected |
| change:files | File[] | Files selected (multiple mode) |
Usage
<template>
<NFileSelect
title="Upload Avatar"
label="Drop image here or click to browse"
hint="Accepts PNG, JPG up to 2MB"
accept="image/*"
@change:file="handleFile"
/>
<!-- Multiple files -->
<NFileSelect multiple accept=".pdf,.doc" @change:files="handleFiles" />
</template>
<script setup>
import { NFileSelect } from 'nicklabs-ui'
function handleFile(file: File) {
console.log('Selected:', file.name)
}
function handleFiles(files: File[]) {
console.log('Selected:', files.length, 'files')
}
</script>NSwitch
Animated toggle switch.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| modelValue | boolean | false | Bound value (v-model) |
| disabled | boolean | false | Disable switch |
| size | "sm" \| "md" \| "lg" | "md" | Switch size |
| title | string | — | Label above the switch |
| inline | boolean | false | Display title and switch on the same line |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| update:modelValue | boolean | Value changed |
| change | boolean | Value changed |
Usage
<template>
<!-- Basic -->
<NSwitch v-model="enabled" />
<!-- With title (stacked) -->
<NSwitch v-model="enabled" title="Enable notifications" />
<!-- Inline title -->
<NSwitch v-model="darkMode" title="Dark mode" size="lg" inline />
</template>
<script setup>
import { ref } from 'vue'
import { NSwitch } from 'nicklabs-ui'
const enabled = ref(false)
const darkMode = ref(false)
</script>NDatePicker
Date picker with range selection and custom format support.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| modelValue | string \| Date | — | Bound value (v-model) for single date |
| start | string | — | Range start (v-model:start) |
| end | string | — | Range end (v-model:end) |
| placeholder | string | "請選擇日期" | Placeholder text |
| disabled | boolean | false | Disable picker |
| clearable | boolean | false | Show clear button |
| title | string | — | Label above picker |
| format | string | "YYYY-MM-DD" | Date format string (supports YYYY, MM, DD, HH, mm) |
| range | boolean | false | Enable range selection mode |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| update:modelValue | string | Single date value changed |
| change | string | Date changed |
| clear | — | Clear button clicked |
| update:start | string | Range start changed |
| update:end | string | Range end changed |
Usage
<template>
<!-- Single date -->
<NDatePicker v-model="date" title="Select Date" clearable />
<!-- With time format -->
<NDatePicker v-model="datetime" format="YYYY-MM-DD HH:mm" title="Date & Time" />
<!-- Date range -->
<NDatePicker
range
v-model:start="startDate"
v-model:end="endDate"
title="Date Range"
/>
</template>
<script setup>
import { ref } from 'vue'
import { NDatePicker } from 'nicklabs-ui'
const date = ref('')
const datetime = ref('')
const startDate = ref('')
const endDate = ref('')
</script>Data Display
NTable
A type-safe, sortable data table with support for batch selection and action slots.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| columns | NTableColumn[] | [] | Column definitions |
| items | T[] | [] | Row data |
| bordered | boolean | false | Show borders |
| hoverable | boolean | true | Highlight rows on hover |
| loading | boolean | false | Show loading state |
| emptyTitle | string | "目前沒有資料" | Title when no data |
| emptyDescription | string | — | Description when no data |
| itemKey | string | "id" | Unique key field |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| sort | NTableSortState | Column sort changed |
| click | T | Row was clicked |
Slots
| Slot | Description |
|------|-------------|
| batch | Content in the batch selection header column |
| actions-header | Custom header text for the actions column (default: "操作") |
| actions | Custom action buttons per row (scoped: { item, column, index }) |
| empty | Custom empty state content |
NTableColumn Interface
interface NTableColumn {
key: string // Data field key
label: string // Column header text
sortable?: boolean
}Usage
<template>
<NTable
:columns="columns"
:items="users"
hoverable
@sort="handleSort"
@click="handleItemClick"
>
<template #actions="{ item }">
<NButton size="sm" variant="ghost" intent="primary" @click="edit(item)">Edit</NButton>
<NButton size="sm" variant="ghost" intent="error" @click="remove(item)">Delete</NButton>
</template>
</NTable>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { NTable, NButton } from 'nicklabs-ui'
import type { NTableColumn, NTableSortState } from 'nicklabs-ui'
interface User {
id: number
name: string
email: string
role: string
}
const columns: NTableColumn[] = [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role' },
]
const users = ref<User[]>([
{ id: 1, name: 'Alice', email: '[email protected]', role: 'Admin' },
{ id: 2, name: 'Bob', email: '[email protected]', role: 'User' },
])
function handleSort(state: NTableSortState) {
console.log('Sort by:', state.key, state.order)
}
function handleItemClick(item: User) {
console.log('Clicked:', item)
}
</script>NList
A full-featured data management component combining table, pagination, filtering, and CRUD operations.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | string | — | Section title displayed in hero header |
| description | string | — | Section description displayed in hero header |
| items | T[] | [] | Data items (T must extend { id: number }) |
| columns | NTableColumn[] | [] | Column definitions |
| itemKey | string | "id" | Unique key field |
| totalItems | number | 0 | Total item count for pagination calculation |
| pageSize | number | 10 | Items per page |
| maxPageButtons | number | 7 | Visible page buttons |
| creatable | boolean | false | Show create button |
| createLabel | string | "新增" | Create button label |
| updatable | boolean | false | Show row edit button |
| updateLabel | string | "編輯" | Edit button label |
| deletable | boolean | false | Show row delete button |
| deleteLabel | string | "刪除" | Delete button label |
| batchDeletable | boolean | false | Enable batch delete with checkboxes |
| batchDeleteLabel | string | "批量刪除" | Batch delete button label |
| filterable | boolean | false | Show filter button |
| filterLabel | string | "篩選" | Filter button label |
| refreshable | boolean | false | Show refresh button |
| refreshLabel | string | "重新整理" | Refresh button label |
| emptyTitle | string | "目前沒有資料" | Empty state title |
| emptyDescription | string | "可以點擊上方的按鈕來新增資料或重新整理" | Empty state description |
| emptyIcon | string | — | Custom SVG string for empty state |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| update | T | Row edit clicked |
| delete | T | Row delete clicked |
| pageChange | number | Page changed |
| create | — | Create clicked |
| batchDelete | number[], clearSelected: () => void | Batch delete with selected IDs; call clearSelected() after deletion to reset checkboxes |
| refresh | — | Refresh clicked |
| filter | — | Filter clicked |
| click | T | Row clicked |
| sort | NTableSortState | Sort changed |
Slots
| Slot | Description |
|------|-------------|
| toolbar | Extra toolbar content (alongside create/refresh buttons) |
| item | Custom cell renderer (scoped: { item, column, index }) |
| actions | Custom action buttons per row (scoped: { item, index }) |
| actions-header | Custom header text for the actions column |
Usage
<template>
<NList
title="User Management"
:items="users"
:columns="columns"
:page-size="20"
filterable
updatable
deletable
creatable
refreshable
@update="handleEdit"
@delete="handleDelete"
@create="handleCreate"
@refresh="loadUsers"
@filter="openFilter"
/>
</template>
<script setup lang="ts">
import { NList } from 'nicklabs-ui'
// T must extend { id: number }
interface User {
id: number
name: string
email: string
}
</script>NTag
Semantic tag/badge component.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| intent | "none" \| "primary" \| "success" \| "warning" \| "error" \| "info" | "none" | Color intent |
| variant | "solid" \| "light" \| "outline" | "light" | Visual style |
| size | "sm" \| "md" \| "lg" | "md" | Tag size |
| closable | boolean | false | Show close button |
| round | boolean | false | Fully rounded (pill shape) |
Events
| Event | Description |
|-------|-------------|
| close | Close button clicked |
Usage
<template>
<NTag intent="success">Active</NTag>
<NTag intent="warning" variant="outline">Pending</NTag>
<NTag intent="error" variant="solid" round>Blocked</NTag>
<NTag intent="info" closable @close="removeTag">Vue 3</NTag>
</template>
<script setup>
import { NTag } from 'nicklabs-ui'
</script>NEmpty
Empty state placeholder component.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | string | — | Main message |
| description | string | — | Sub-message |
| size | "sm" \| "md" \| "lg" | "md" | Component size |
| icon | string | — | Custom SVG string |
Usage
<template>
<NEmpty
title="No results found"
description="Try adjusting your search filters"
size="lg"
/>
</template>
<script setup>
import { NEmpty } from 'nicklabs-ui'
</script>NCode
Syntax-highlighted code display with copy-to-clipboard.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| code | string | — | Code content to display |
| language | string | — | Language for highlighting |
| showLineNumbers | boolean | false | Show line numbers |
Usage
<template>
<NCode
:code="snippet"
language="typescript"
showLineNumbers
/>
</template>
<script setup>
import { NCode } from 'nicklabs-ui'
const snippet = `const greeting = (name: string) => {
return \`Hello, \${name}!\`
}`
</script>Modals & Overlays
NModal
A teleported modal dialog.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| show | boolean | false | Visibility (v-model:show) |
| title | string | — | Modal title |
| width | string | "500px" | Modal width |
| closeOnClickOverlay | boolean | true | Close when backdrop clicked |
| showClose | boolean | true | Show close button |
| zIndex | number | — | Custom z-index |
Events
| Event | Description |
|-------|-------------|
| update:show | Visibility changed |
| close | Modal closed |
| open | Modal opened |
Slots
| Slot | Description |
|------|-------------|
| default | Modal body content |
| footer | Footer actions area |
Usage
<template>
<NButton @click="open">Open Modal</NButton>
<NModal v-model:show="isOpen" title="Confirm Action" width="400px">
<p>Are you sure you want to proceed?</p>
<template #footer>
<NButton variant="ghost" @click="isOpen = false">Cancel</NButton>
<NButton intent="primary" @click="confirm">Confirm</NButton>
</template>
</NModal>
</template>
<script setup>
import { ref } from 'vue'
import { NModal, NButton } from 'nicklabs-ui'
const isOpen = ref(false)
function open() { isOpen.value = true }
function confirm() {
isOpen.value = false
}
</script>NDrawer
A teleported side drawer that slides in from the right or left edge of the viewport.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| show | boolean | false | Visibility (v-model:show) |
| title | string | — | Drawer title |
| width | string | "380px" | Drawer width |
| placement | "right" \| "left" | "right" | Which side to slide in from |
| closeOnClickOverlay | boolean | true | Close when backdrop clicked |
| showClose | boolean | true | Show close button |
| zIndex | number | 1000 | Custom z-index |
Events
| Event | Description |
|-------|-------------|
| update:show | Visibility changed |
| close | Drawer closed |
| open | Drawer opened |
Slots
| Slot | Description |
|------|-------------|
| default | Drawer body content |
| title | Override the title area |
| footer | Footer actions area (renders footer only when provided) |
Usage
<template>
<NButton @click="isOpen = true">Open Drawer</NButton>
<NDrawer v-model:show="isOpen" title="Edit Item">
<NInput v-model="name" title="Name" placeholder="Enter name" />
<template #footer>
<NButton variant="outline" @click="isOpen = false">Cancel</NButton>
<NButton variant="solid" intent="primary" @click="save">Save</NButton>
</template>
</NDrawer>
</template>
<script setup>
import { ref } from 'vue'
import { NDrawer, NButton, NInput } from 'nicklabs-ui'
const isOpen = ref(false)
const name = ref('')
function save() {
isOpen.value = false
}
</script>NAlert
Programmatic alert and confirm dialogs via the useAlert composable.
NAlertmust be mounted once at the app root level.
Setup
<!-- App.vue -->
<template>
<RouterView />
<NAlert />
</template>
<script setup>
import { NAlert } from 'nicklabs-ui'
</script>Usage via useAlert
See useAlert composable below.
NToast
Notification toasts via the useToast composable.
NToastmust be mounted once at the app root level.
Setup
<!-- App.vue -->
<template>
<RouterView />
<NToast />
</template>
<script setup>
import { NToast } from 'nicklabs-ui'
</script>Usage via useToast
See useToast composable below.
NTooltip
Hover/focus tooltip with directional positioning.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| content | string | — | Tooltip text |
| position | "top" \| "right" \| "bottom" \| "left" | "top" | Tooltip position |
| disabled | boolean | false | Disable tooltip |
Usage
<template>
<NTooltip content="Delete this item" position="top">
<NButton variant="ghost" intent="error" square>
<svg>...</svg>
</NButton>
</NTooltip>
</template>
<script setup>
import { NTooltip, NButton } from 'nicklabs-ui'
</script>NLoading
Loading indicator supporting inline, overlay, and fullscreen modes.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| loading | boolean | true | Show loading state |
| title | string | — | Loading message |
| variant | "spinner" \| "dots" | "spinner" | Animation style |
| overlay | boolean | false | Overlay mode — covers slot content with a backdrop |
| fullscreen | boolean | false | Fullscreen mode — covers the entire viewport and blocks interaction |
Usage
<template>
<!-- Inline: shows spinner while loading, slot content when done -->
<NLoading :loading="isFetching" title="Loading data...">
<p>Content loaded!</p>
</NLoading>
<!-- Overlay: spinner overlaid on top of slot content -->
<NLoading :loading="isSubmitting" overlay title="Saving..." variant="dots">
<div>Form content here</div>
</NLoading>
<!-- Fullscreen: blocks entire viewport, no slot needed -->
<NLoading :loading="isProcessing" fullscreen title="處理中..." />
</template>
<script setup>
import { ref } from 'vue'
import { NLoading } from 'nicklabs-ui'
const isFetching = ref(true)
const isSubmitting = ref(false)
const isProcessing = ref(false)
</script>Layout
NLayout
Main application shell integrating sidebar and main content area.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| menus | Menu[] | — | Sidebar menu items |
| isShowSidebar | boolean | true | Show/hide sidebar |
| copyright | string | — | Footer copyright text |
| currentPath | string | "" | Active route path |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| logout | — | Logout triggered |
| navigate | MenuChild | Menu item clicked |
Slots
| Slot | Description |
|------|-------------|
| default | Main page content |
Usage
<template>
<NLayout
:menus="menus"
:current-path="$route.path"
copyright="© 2025 MyApp"
@logout="handleLogout"
@navigate="handleNavigate"
>
<RouterView />
</NLayout>
</template>
<script setup>
import { NLayout } from 'nicklabs-ui'
const menus = [
{
icon: '<svg>...</svg>',
title: 'Dashboard',
children: [
{ icon: '<svg>...</svg>', title: 'Overview', route: '/dashboard' },
],
},
]
</script>NNavigation
Top navigation bar with sidebar toggle, fullscreen, and user controls.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| isShowSidebar | boolean | true | Show sidebar toggle button |
| isShowFullscreen | boolean | true | Show fullscreen button |
| isShowUser | boolean | true | Show user pill |
| isShowLogoutButton | boolean | true | Show logout button |
Events
| Event | Description |
|-------|-------------|
| toggleSidebar | Sidebar toggle clicked |
| logout | Logout clicked |
NSidebar
Collapsible side navigation menu with hover-expand behavior.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| isOpen | boolean | false | Sidebar open state |
| menus | Menu[] | — | Menu items |
| currentPath | string | "" | Active route path |
| userName | string | — | Display name shown in user area |
| userAvatarUrl | string | — | Avatar image URL |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| update:isOpen | boolean | Open state changed |
| logout | — | Logout triggered |
| navigate | MenuChild | Menu item clicked |
NCard
Content card with glassmorphism styling.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | "none" \| "sm" \| "md" \| "lg" | "md" | Padding size |
| radius | "none" \| "sm" \| "md" \| "lg" \| "xl" | "md" | Border radius |
Usage
<template>
<NCard size="lg" radius="xl">
<h2>Card Title</h2>
<p>Card content goes here.</p>
</NCard>
</template>
<script setup>
import { NCard } from 'nicklabs-ui'
</script>NForm
Form wrapper with optional tab navigation and hero section header.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| model | object | {} | Form data object |
| disabled | boolean | false | Disable all inputs |
| icon | string | — | Header icon (SVG string) |
| title | string | — | Form header title |
| description | string | — | Form header description |
| tabs | string[] | [] | Tab labels; active tab persisted to localStorage |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| submit | Record<string, any> | Form submitted with model data |
| reset | — | Form reset |
Slots
| Slot | Description |
|------|-------------|
| heroSection | Replace the entire hero header (rarely needed) |
| toolbar | Toolbar area in the hero header (buttons, etc.) |
| description | Custom description content in hero header |
| tab0 | Content for first tab (or only content when no tabs) |
| tab1, tab2, ... | Content for subsequent tabs |
| footer | Form footer (submit/cancel buttons) |
Usage
<template>
<NForm
:model="formData"
title="User Profile"
description="Manage your account details"
:tabs="['Basic Info', 'Security', 'Preferences']"
@submit="handleSubmit"
>
<template #toolbar>
<NButton variant="ghost" @click="cancel">Cancel</NButton>
</template>
<template #tab0>
<NInput v-model="formData.name" title="Name" />
<NInput v-model="formData.email" title="Email" />
</template>
<template #tab1>
<NInput v-model="formData.password" type="password" title="Password" />
</template>
<template #footer>
<NButton type="submit" intent="primary">Save</NButton>
</template>
</NForm>
</template>
<script setup>
import { reactive } from 'vue'
import { NForm, NInput, NButton } from 'nicklabs-ui'
const formData = reactive({ name: '', email: '', password: '' })
function handleSubmit(model) {
console.log('Submitted:', model)
}
</script>NLoginLayout
Full-screen login page wrapper with animated card.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| backgroundImage | string | — | Background image URL (falls back to --bg-gradient) |
| logo | string | — | Logo image URL or SVG string |
| title | string | — | Application name |
| description | string | — | Subtitle/tagline |
Usage
<template>
<NLoginLayout
title="MyApp"
description="Sign in to your account"
background-image="/images/bg.jpg"
logo="/images/logo.png"
>
<NInput v-model="email" type="email" placeholder="Email" />
<NInput v-model="password" type="password" placeholder="Password" />
<NButton type="submit" intent="primary" style="width: 100%">Sign In</NButton>
</NLoginLayout>
</template>
<script setup>
import { ref } from 'vue'
import { NLoginLayout, NInput, NButton } from 'nicklabs-ui'
const email = ref('')
const password = ref('')
</script>NBreadcrumb
Breadcrumb navigation driven by useBreadcrumb. No props — reads from the composable's state automatically.
Configure breadcrumbs via useBreadcrumb.
Usage
<template>
<NBreadcrumb />
</template>
<script setup>
import { NBreadcrumb } from 'nicklabs-ui'
</script>NPaginate
Pagination control with smart ellipsis.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| totalItems | number | — | Total number of items |
| pageSize | number | — | Items per page |
| maxPageButtons | number | 7 | Max visible page buttons (min: 5) |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| onPageChange | number | New page number |
Usage
<template>
<NPaginate
:total-items="totalCount"
:page-size="20"
:max-page-buttons="7"
@on-page-change="loadPage"
/>
</template>
<script setup>
import { ref } from 'vue'
import { NPaginate } from 'nicklabs-ui'
const totalCount = ref(350)
function loadPage(page: number) {
// fetch page data
}
</script>NHeroSection
Page header with icon, title, description, breadcrumb, and toolbar slot.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | string | — | Section title |
| description | string | — | Section description |
| icon | string | — | Icon (SVG string) |
Slots
| Slot | Description |
|------|-------------|
| toolbar | Toolbar content (buttons, filters, etc.) |
| description | Custom description content |
Usage
<template>
<NHeroSection title="User Management" description="Manage system users" :icon="userIcon">
<template #toolbar>
<NButton intent="primary" @click="create">New User</NButton>
</template>
</NHeroSection>
</template>
<script setup>
import { NHeroSection, NButton } from 'nicklabs-ui'
const userIcon = '<svg>...</svg>'
</script>NSideFilter
A slide-out side drawer for filter interfaces.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | — | Open state (v-model:open) |
| title | string | — | Drawer title |
Events
| Event | Description |
|-------|-------------|
| update:open | Open state changed |
| close | Drawer closed |
Usage
<template>
<NButton @click="filterOpen = true">Filters</NButton>
<NSideFilter v-model:open="filterOpen" title="Filter Results">
<NSelect v-model="status" :options="statusOptions" title="Status" />
<NInput v-model="search" placeholder="Search name..." title="Name" />
</NSideFilter>
</template>
<script setup>
import { ref } from 'vue'
import { NSideFilter, NButton, NSelect, NInput } from 'nicklabs-ui'
const filterOpen = ref(false)
const status = ref(null)
const search = ref('')
</script>Composables
useToast
Display notification toasts programmatically.
import { useToast } from 'nicklabs-ui'
const { toasts, toast, removeToast } = useToast()Methods
| Method | Signature | Description |
|--------|-----------|-------------|
| toast | (message: string, options?: ToastOptions) => void | Show a toast |
| toast.success | (message: string, options?) => void | Success toast |
| toast.danger | (message: string, options?) => void | Error toast |
| toast.warning | (message: string, options?) => void | Warning toast |
| toast.info | (message: string, options?) => void | Info toast |
| removeToast | (id: string) => void | Remove a specific toast |
ToastOptions
interface ToastOptions {
title?: string // Optional heading
duration?: number // Auto-dismiss ms (default: 4000)
}Usage
<script setup>
import { useToast } from 'nicklabs-ui'
const { toast } = useToast()
function save() {
toast.success('Saved successfully!', { title: 'Done' })
}
function handleError() {
toast.danger('Something went wrong.', { duration: 6000 })
}
</script>useAlert
Display programmatic alert and confirm dialogs.
import { useAlert } from 'nicklabs-ui'
const { alert, confirm, clearAlerts } = useAlert()Methods
| Method | Returns | Description |
|--------|---------|-------------|
| alert(message, options?) | Promise<void> | Show an alert dialog |
| alert.success(message, options?) | Promise<void> | Success alert |
| alert.warning(message, options?) | Promise<void> | Warning alert |
| alert.danger(message, options?) | Promise<void> | Danger alert |
| alert.info(message, options?) | Promise<void> | Info alert |
| confirm(message, options?) | Promise<boolean> | Show a confirm dialog |
| clearAlerts() | void | Dismiss all alerts |
AlertOptions
interface AlertOptions {
title?: string
confirmText?: string
cancelText?: string // confirm() only
}Usage
<script setup>
import { useAlert } from 'nicklabs-ui'
const { alert, confirm } = useAlert()
async function deleteUser(id: number) {
const confirmed = await confirm(
'This action cannot be undone.',
{ title: 'Delete user?', confirmText: 'Delete', cancelText: 'Cancel' }
)
if (confirmed) {
await api.deleteUser(id)
await alert.success('User deleted.')
}
}
</script>useDisclosure
Simple boolean state management for modals and drawers.
import { useDisclosure } from 'nicklabs-ui'
// Array destructuring
const [isOpen, open, close] = useDisclosure()Usage
<template>
<NButton @click="open">Open Modal</NButton>
<NModal v-model:show="isOpen" title="My Modal">
<p>Content</p>
<template #footer>
<NButton @click="close">Close</NButton>
</template>
</NModal>
</template>
<script setup>
import { NModal, NButton, useDisclosure } from 'nicklabs-ui'
const [isOpen, open, close] = useDisclosure()
</script>useBreadcrumb
Manage breadcrumb navigation state. NBreadcrumb reads from this composable automatically.
import { useBreadcrumb } from 'nicklabs-ui'
const {
breadcrumbs,
setLabelResolver,
setBreadcrumbSuffix,
clearSuffix,
navigate,
} = useBreadcrumb()Methods
| Method | Signature | Description |
|--------|-----------|-------------|
| setLabelResolver | (fn: (route) => string) => void | Custom function to extract label from a route record |
| setBreadcrumbSuffix | (fn: (label: string) => string) => void | Transform the last breadcrumb label (e.g. append a record name) |
| clearSuffix | () => void | Reset the suffix transform |
| navigate | (path: string) => void | Navigate to a breadcrumb path |
BreadcrumbItem
interface BreadcrumbItem {
label: string
path?: string // undefined for the last (active) crumb
}Setup
Pass router when installing the plugin (see Setup). The label is read from route.meta.breadcrumb by default — no additional configuration needed.
Route meta
const routes = [
{
path: '/users',
component: UserList,
meta: { breadcrumb: 'Users' },
children: [
{
path: ':id',
component: UserDetail,
meta: { breadcrumb: 'Detail' },
},
],
},
]Dynamic suffix example
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { useBreadcrumb } from 'nicklabs-ui'
const { setBreadcrumbSuffix, clearSuffix } = useBreadcrumb()
// Show user name as the last breadcrumb label
onMounted(() => setBreadcrumbSuffix(() => user.value.name))
onUnmounted(() => clearSuffix())
</script>useSidebarManager
Manage sidebar open/close state, expandable menu groups (persisted to localStorage), and active state logic.
import { useSidebarManager } from 'nicklabs-ui'
const {
isMenuExpanded,
toggleMenu,
isSidebarExpanded,
toggleSidebar,
setMenuActiveResolver,
setItemActiveResolver,
} = useSidebarManager(sidebarId?)Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| sidebarId | string | "default" | Unique ID for localStorage key |
Returns
| Property | Type | Description |
|----------|------|-------------|
| isMenuExpanded | (menuTitle: string) => boolean | Check if a menu group is expanded |
| toggleMenu | (menuTitle: string) => void | Toggle a menu group |
| isSidebarExpanded | () => boolean | Check if sidebar is open |
| toggleSidebar | () => void | Toggle sidebar open/close |
| setMenuActiveResolver | (fn: (menu: Menu, currentPath: string) => boolean) => void | Override the active logic for a top-level menu group |
| setItemActiveResolver | (fn: (childMenu: MenuChild, currentPath: string) => boolean) => void | Override the active logic for individual menu items |
Custom active state
By default, NSidebar marks a menu item active when its route exactly matches currentPath. Use the resolver setters to provide custom logic — for example, prefix matching for nested routes:
import { useSidebarManager } from 'nicklabs-ui'
import type { MenuChild } from 'nicklabs-ui'
const { setItemActiveResolver } = useSidebarManager()
// Highlight the item whenever the current path starts with its route
setItemActiveResolver((childMenu: MenuChild, currentPath: string) => {
return currentPath.startsWith(childMenu.route)
})Call the setters once at app initialisation (e.g. main.ts or App.vue) — the setting is global and persists for the lifetime of the page.
useRouteModal
Open and close route-based modals using Vue Router's nested routes.
import { useRouteModal } from 'nicklabs-ui'
const { isOpen, open, close, params } = useRouteModal({
routeName: 'user-detail',
parentRouteName: 'users', // optional fallback when no history
})Options
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| routeName | string | Yes | Named route that represents the open modal state |
| parentRouteName | string | No | Fallback route to navigate to on close when there is no browser history |
Returns
| Property | Type | Description |
|----------|------|-------------|
| isOpen | ComputedRef<boolean> | true when the named route is in the matched stack |
| open | (params?) => void | Navigate to the modal route |
| close | () => void | Navigate back (uses router.back() or parent route) |
| params | ComputedRef<RouteParams> | Current route params |
Usage
// router/index.ts
const routes = [
{
path: '/users',
name: 'users',
component: UserList,
children: [
{
path: ':id',
name: 'user-detail',
component: UserDetail,
},
],
},
]<template>
<NButton @click="open({ id: user.id })">View</NButton>
<NModal v-model:show="isOpen" title="User Detail" @close="close">
<p>ID: {{ params.id }}</p>
</NModal>
</template>
<script setup>
import { useRouteModal } from 'nicklabs-ui'
import { NModal, NButton } from 'nicklabs-ui'
const { isOpen, open, close, params } = useRouteModal({
routeName: 'user-detail',
parentRouteName: 'users',
})
</script>Type Reference
// Common
interface OptionItem {
label: string
value: any
}
type NSize = 'sm' | 'md' | 'lg'
// Table
interface NTableColumn {
key: string
label: string
sortable?: boolean
}
type NTableSortOrder = 'asc' | 'desc' | null
interface NTableSortState {
key: string
order: NTableSortOrder
}
// Layout
type MenuChild = {
icon: string
title: string
route: string
}
type Menu = {
icon: string
title: string
children?: MenuChild[]
}
// Breadcrumb
interface BreadcrumbItem {
label: string
path?: string
}
// Sizes / Variants
type NButtonVariant = 'none' | 'solid' | 'outline' | 'ghost' | 'mute'
type NButtonIntent = 'none' | 'primary' | 'error' | 'success' | 'warning' | 'info'
type NButtonRadiusSize = 'sm' | 'md' | 'lg' | 'xl' | 'full'
type NButtonSize = 'sm' | 'md' | 'lg'
type NTagIntent = 'none' | 'primary' | 'success' | 'warning' | 'error' | 'info'
type NTagVariant = 'solid' | 'light' | 'outline'
type PaddingSize = 'none' | 'sm' | 'md' | 'lg'
type RadiusSize = 'none' | 'sm' | 'md' | 'lg' | 'xl'CSS Variables
The library reads these CSS custom properties from the host application. Override them to theme the components.
:root {
/* Colors */
--primary-color: #4f6ef7;
--primary-light: #7b93fa;
--primary-dark: #3554e1;
--success-color: #22c55e;
--error-color: #ef4444;
--warning-color: #f59e0b;
--info-color: #3b82f6;
/* Text */
--text-main: #1e293b;
--text-secondary: #475569;
--text-muted: #94a3b8;
--text-inverse: #ffffff;
/* Backgrounds */
--bg-body: #f1f5f9;
--bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
/* Surfaces (glassmorphism) */
--surface-solid: #ffffff;
--surface-glass: rgba(255, 255, 255, 0.7);
--surface-glass-border: rgba(255, 255, 255, 0.3);
--surface-glass-hover: rgba(255, 255, 255, 0.85);
/* Border */
--border-color: #e2e8f0;
/* Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
/* Layout */
--header-height: 56px;
--sidebar-width: 60px;
--sidebar-width-open: 240px;
--sidebar-logo-height: 48px;
/* Inputs */
--input-bg: #ffffff;
--input-disabled-bg: #f8fafc;
--input-disabled-text: #94a3b8;
/* Transitions */
--transition-fast: 0.15s ease;
--transition-normal: 0.25s ease;
}The
variables.cssexport fromnicklabs-uiprovides a default set of these variables. Import it as a starting point and override as needed in your own stylesheet.
