@juanitte/inoui
v1.1.4
Published
A modern React UI component library
Maintainers
Readme
Ino-UI
A modern, lightweight React component library with built-in theming support.
Table of Contents
- Features
- Installation
- Quick Start
- Theme System
- Semantic DOM Styling
- Utils
- Components
- Affix
- Alert
- AutoComplete
- Avatar
- Anchor
- App
- Badge
- Breadcrumb
- Bubble
- Button
- Card
- Calendar
- Carousel
- Checkbox
- ColorPicker
- Collapse
- ConfigProvider
- DataDisplay
- DatePicker
- Divider
- Drawer
- Dropdown
- Empty
- Form
- Input
- Image
- InputNumber
- Flex
- Grid
- Layout
- Menu
- Mention
- Modal
- NestedSelect
- Pagination
- PopAlert
- PopConfirm
- Popover
- Progress
- QRCode
- Radio
- Rate
- Result
- Select
- Slider
- Space
- Spinner
- Splitter
- Steps
- Statistic
- Switch
- Table
- Tabs
- Tag
- Text
- TimePicker
- Timeline
- Toggle
- Tooltip
- Tour
- Transfer
- Tree
- TreeSelect
- Upload
- Waterfall
- Watermark
Features
- Fully typed with TypeScript
- Built-in dark/light mode support
- Automatic color palette generation
- CSS variables for easy customization
- Tree-shakeable and lightweight
- React 18 & 19 compatible
Installation
# npm
npm install @juanitte/inoui
# yarn
yarn add @juanitte/inoui
# pnpm
pnpm add @juanitte/inouiQuick Start
Import the stylesheet once in your app entry point, wrap your application with ThemeProvider, and start using components:
import '@juanitte/inoui/styles.css'
import { ThemeProvider, Button, Tooltip } from '@juanitte/inoui'
function App() {
return (
<ThemeProvider>
<Tooltip content="Click me!">
<Button>Hello Ino-UI</Button>
</Tooltip>
</ThemeProvider>
)
}Theme System
Ino-UI includes a powerful theming system that automatically generates color palettes and handles light/dark mode switching.
ThemeProvider
The ThemeProvider component must wrap your application to enable theming functionality.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | Application content |
| config | ThemeConfig | {} | Theme configuration object |
ThemeConfig
interface ThemeConfig {
colors?: ThemeColors // Custom color palette
defaultMode?: ThemeMode // 'light' | 'dark'
}
interface ThemeColors {
primary?: string // Default: '#8c75d1'
secondary?: string // Default: '#78808b'
success?: string // Default: '#79bc58'
warning?: string // Default: '#d5ac59'
error?: string // Default: '#d7595b'
info?: string // Default: '#6591c7'
}Usage
// Basic usage
<ThemeProvider>
<App />
</ThemeProvider>
// With custom configuration
<ThemeProvider
config={{
defaultMode: 'dark',
colors: {
primary: '#FF6B6B',
success: '#4ECDC4'
}
}}
>
<App />
</ThemeProvider>useTheme Hook
Access and control the current theme from any component within the ThemeProvider.
const { mode, setMode, toggleMode } = useTheme()Returns
| Property | Type | Description |
|----------|------|-------------|
| mode | 'light' \| 'dark' | Current theme mode |
| setMode | (mode: ThemeMode) => void | Set theme mode explicitly |
| toggleMode | () => void | Toggle between light and dark |
Example
import { useTheme, Button } from '@juanitte/inoui'
function ThemeToggle() {
const { mode, toggleMode } = useTheme()
return (
<Button onClick={toggleMode}>
Current: {mode}
</Button>
)
}Color System
Ino-UI automatically generates 10 shades (50-900) for each semantic color. These are available as CSS variables:
/* Base color */
var(--j-primary)
/* Color variants */
var(--j-primary-50) /* Lightest */
var(--j-primary-100)
var(--j-primary-200)
var(--j-primary-300)
var(--j-primary-400)
var(--j-primary-500)
var(--j-primary-600)
var(--j-primary-700)
var(--j-primary-800)
var(--j-primary-900) /* Darkest */
/* Semantic aliases */
var(--j-primary-light) /* Light variant */
var(--j-primary-dark) /* Dark variant */
var(--j-primary-hover) /* Hover state */
var(--j-primary-border) /* Border color */
var(--j-primary-contrast) /* Contrast text color */Neutral Colors
These automatically adapt based on the current theme mode:
| Variable | Description |
|----------|-------------|
| --j-bg | Background color |
| --j-bgSubtle | Subtle background |
| --j-bgMuted | Muted background |
| --j-border | Border color |
| --j-borderHover | Border hover color |
| --j-text | Primary text color |
| --j-textMuted | Muted text |
| --j-textSubtle | Subtle text |
| --j-shadowSm | Small shadow |
| --j-shadowMd | Medium shadow |
| --j-shadowLg | Large shadow |
Theme Tokens
Ino-UI provides a tokens object with typed references to all CSS variables. This enables autocomplete in your IDE and type-safe styling in JavaScript/TypeScript.
Import
import { tokens } from '@juanitte/inoui'Usage
// In inline styles
<div style={{
backgroundColor: tokens.colorPrimaryBg,
color: tokens.colorPrimary,
boxShadow: tokens.shadowMd
}}>
Styled with tokens
</div>
// In styled-components, emotion, etc.
const StyledCard = styled.div`
background: ${tokens.colorBgSubtle};
border: 1px solid ${tokens.colorBorder};
`Available Tokens
Semantic Colors (for each: primary, secondary, success, warning, error, info):
| Token | CSS Variable |
|-------|--------------|
| tokens.colorPrimary | var(--j-primary) |
| tokens.colorPrimaryHover | var(--j-primary-hover) |
| tokens.colorPrimaryBg | var(--j-primary-dark) |
| tokens.colorPrimaryBorder | var(--j-primary-border) |
| tokens.colorPrimaryContrast | var(--j-primary-contrast) |
| tokens.colorPrimary50 - tokens.colorPrimary900 | var(--j-primary-50) - var(--j-primary-900) |
Neutral Colors:
| Token | CSS Variable |
|-------|--------------|
| tokens.colorBg | var(--j-bg) |
| tokens.colorBgSubtle | var(--j-bgSubtle) |
| tokens.colorBgMuted | var(--j-bgMuted) |
| tokens.colorBorder | var(--j-border) |
| tokens.colorBorderHover | var(--j-borderHover) |
| tokens.colorText | var(--j-text) |
| tokens.colorTextMuted | var(--j-textMuted) |
| tokens.colorTextSubtle | var(--j-textSubtle) |
Shadows:
| Token | CSS Variable |
|-------|--------------|
| tokens.shadowSm | var(--j-shadowSm) |
| tokens.shadowMd | var(--j-shadowMd) |
| tokens.shadowLg | var(--j-shadowLg) |
Semantic DOM Styling
Most Ino-UI components expose classNames and styles props that allow you to style their internal parts (semantic slots) without needing to override CSS or inspect the DOM structure.
How it works
Each component defines named slots representing its internal parts. You can target these slots using:
classNames— apply CSS classes to specific internal partsstyles— apply inline styles to specific internal parts
// Apply a CSS class to the popup part of a Tooltip
<Tooltip
content="Hello"
classNames={{ popup: 'my-custom-popup' }}
>
<Button>Hover me</Button>
</Tooltip>
// Apply inline styles to the icon and content parts of a Button
<Button
icon={<SearchIcon />}
styles={{
icon: { color: 'red' },
content: { fontWeight: 'bold' },
}}
>
Search
</Button>Priority order
Styles are merged with the following priority (highest wins):
- Component base styles — internal defaults
styles.slot— semantic styles for each slotstyleprop — direct style prop (only applies toroot)
For className, the component-level className prop and classNames.root are merged together.
Available slots per component
| Component | Slots |
|-----------|-------|
| Affix | root, affix |
| AutoComplete | root, input, dropdown, option |
| Anchor | root, track, indicator, link |
| Badge | root, indicator |
| Badge.Ribbon | wrapper, ribbon, content, corner |
| Breadcrumb | root, list, item, separator, link, overlay |
| Bubble | root, icon, badge, tooltip, tooltipArrow |
| Bubble.Menu | root, trigger, menu |
| Button | root, icon, spinner, content |
| Checkbox | root, checkbox, indicator, label |
| Checkbox.Group | root |
| ColorPicker | root, trigger, panel, presets |
| DatePicker | root, input, popup, header, body, cell, footer |
| Divider | root, line, text |
| Dropdown | root, overlay, item, arrow |
| Form | root |
| Form.Item | root, label, control, help, extra |
| Layout.Sider | root, content, trigger |
| Menu | root, item, submenu, group, groupTitle, divider |
| NestedSelect | root, selector, dropdown, menu, option |
| Pagination | root, item, options |
| Space | root, item, separator |
| Splitter | root, panel, bar, collapseButton |
| Steps | root, step, icon, content, tail |
| Tabs | root, tabBar, tab, content, inkBar |
| Text | root, content, copyButton, expandButton |
| Tooltip | root, popup, arrow |
| Waterfall | root, column, item |
CSS Class Customization
All components use BEM class names with the ino- prefix, which you can target directly in your stylesheets.
Naming convention:
| Pattern | Example | Description |
|---------|---------|-------------|
| ino-{block} | ino-btn | Component root |
| ino-{block}--{modifier} | ino-btn--primary, ino-btn--sm | Variant, size, or state |
| ino-{block}__{element} | ino-btn__icon | Internal part |
Example — custom button styles:
/* Your stylesheet — loaded after ino-ui */
.ino-btn--primary {
border-radius: 999px;
}
.ino-tag--sm {
font-size: 0.7rem;
}You can combine this with the classNames prop for scoped overrides:
<Button classNames={{ root: 'my-btn' }}>Click</Button>.my-btn {
border-radius: 999px;
}Utils
Ino-UI exports a set of utility functions and hooks that can be used independently in your application.
import {
getScrollBarSize, scrollTo, canUseDom,
omit, classNames,
BREAKPOINT_VALUES, BREAKPOINT_ORDER, getResponsiveValue,
useEvent, useMergedState, useWindowWidth, useBreakpoint,
} from '@juanitte/inoui'
import type {
ScrollToOptions, ClassValue,
Breakpoint, ResponsiveValue,
UseMergedStateOptions,
} from '@juanitte/inoui'DOM Utilities
getScrollBarSize(fresh?)
Returns the browser's native scrollbar width in pixels. The result is cached after the first call.
import { getScrollBarSize } from '@juanitte/inoui'
const width = getScrollBarSize() // e.g. 17 (cached)
const fresh = getScrollBarSize(true) // re-measures even if cachedReturns 0 in SSR / non-DOM environments.
scrollTo(top, options?)
Smoothly scrolls a container to a target vertical position using an easeInOutCubic easing curve.
import { scrollTo } from '@juanitte/inoui'
import type { ScrollToOptions } from '@juanitte/inoui'
scrollTo(500) // scroll window to y=500
scrollTo(0, {
container: myRef.current, // scroll a custom element
duration: 600, // animation duration in ms (default: 450)
callback: () => console.log('done'),
})| Option | Type | Default | Description |
|--------|------|---------|-------------|
| container | HTMLElement \| Window | window | Scroll target |
| duration | number | 450 | Animation duration in ms |
| callback | () => void | — | Called when animation completes |
canUseDom()
Returns true when running in a real browser environment (DOM is available). SSR-safe alternative to checking typeof window !== 'undefined'.
import { canUseDom } from '@juanitte/inoui'
if (canUseDom()) {
// safe to access document, window, etc.
}Object Utilities
omit(obj, keys)
Returns a shallow copy of obj with the specified keys removed. Fully typed.
import { omit } from '@juanitte/inoui'
const clean = omit(props, ['className', 'style'])
// clean is typed as Omit<typeof props, 'className' | 'style'>classNames(...args)
Builds a class name string from any mix of strings, numbers, objects, and arrays — falsy values are automatically ignored.
import { classNames } from '@juanitte/inoui'
import type { ClassValue } from '@juanitte/inoui'
classNames('foo', undefined, 'bar') // 'foo bar'
classNames({ active: true, disabled: false }) // 'active'
classNames(['a', ['b', 'c']], 'z') // 'a b c z'Breakpoint Utilities
These utilities implement a shared responsive breakpoint system used by Avatar, DataDisplay, Grid, and Waterfall.
import { BREAKPOINT_VALUES, BREAKPOINT_ORDER, getResponsiveValue } from '@juanitte/inoui'
import type { Breakpoint, ResponsiveValue } from '@juanitte/inoui'Breakpoint type
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'BREAKPOINT_VALUES
const BREAKPOINT_VALUES: Record<Breakpoint, number> = {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
xxl: 1600,
}BREAKPOINT_ORDER
Breakpoints sorted from largest to smallest, used when resolving ResponsiveValue:
const BREAKPOINT_ORDER: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs']ResponsiveValue<T>
A value that can either be a scalar or a per-breakpoint map:
type ResponsiveValue<T> = T | Partial<Record<Breakpoint, T>>getResponsiveValue(value, windowWidth)
Resolves a ResponsiveValue<T> to a concrete value given the current window width. If value is a scalar it is returned as-is; if it is an object, the largest active breakpoint wins.
import { getResponsiveValue } from '@juanitte/inoui'
getResponsiveValue(3, 1024) // 3
getResponsiveValue({ xs: 1, md: 2, xl: 4 }, 1024) // 2 (lg ≥ md, xl not yet active)Hooks
useEvent(fn)
Returns a stable function reference that always calls the latest version of fn. The returned reference never changes between renders, so it is safe to pass to memoized children without causing re-renders.
import { useEvent } from '@juanitte/inoui'
const handleClick = useEvent((e: MouseEvent) => {
console.log(count) // always reads the latest `count`
})useMergedState(defaultValue, options?)
Merges controlled and uncontrolled state into a single interface. Behaves like useState but also accepts an optional value / onChange pair for controlled use.
import { useMergedState } from '@juanitte/inoui'
import type { UseMergedStateOptions } from '@juanitte/inoui'
// Uncontrolled
const [value, setValue] = useMergedState('hello')
// Controlled
const [value, setValue] = useMergedState('hello', {
value: controlledValue,
onChange: (next, prev) => setControlledValue(next),
postState: (v) => v.trim(), // optional transform before returning
})| Option | Type | Description |
|--------|------|-------------|
| value | T \| undefined | External controlled value. When defined, the hook is in controlled mode. |
| onChange | (next: T, prev: T) => void | Called on every state change (controlled and uncontrolled). |
| postState | (value: T) => T | Optional transform applied before the value is returned. |
useWindowWidth()
Returns the current window.innerWidth, updating on every resize event. SSR-safe — returns 1200 on the server.
import { useWindowWidth } from '@juanitte/inoui'
function MyComponent() {
const width = useWindowWidth()
return <p>Window is {width}px wide</p>
}useBreakpoint()
Returns a map of active breakpoints based on the current window width.
import { useBreakpoint } from '@juanitte/inoui'
function MyComponent() {
const screens = useBreakpoint()
// { xs: true, sm: true, md: true, lg: false, xl: false, xxl: false } at 800px
return <p>{screens.md ? 'Medium or larger' : 'Smaller than md'}</p>
}Returns Record<Breakpoint, boolean> where each key is true if the window width meets or exceeds that breakpoint's minimum width.
Components
Affix
Affix keeps its children pinned to the top or bottom of the viewport (or a custom scroll container) once the user scrolls past a defined threshold. A placeholder div of equal size replaces the content in the normal flow so the page layout does not jump when the element becomes fixed.
import { Affix } from '@juanitte/inoui'Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| offsetTop | number | 0* | Distance in px from the top of the container at which to pin the element. *Defaults to 0 when neither offsetTop nor offsetBottom is set |
| offsetBottom | number | — | Distance in px from the bottom of the container at which to pin the element. offsetTop takes priority if both are set |
| target | () => HTMLElement \| Window | window | Returns the scrollable container to observe. Defaults to window |
| onChange | (affixed: boolean) => void | — | Called when the affixed state changes |
| children | ReactNode | — | Content to affix |
| className | string | — | CSS class on the root placeholder |
| style | CSSProperties | — | Inline style on the root placeholder |
| classNames | AffixClassNames | — | Semantic class names per slot |
| styles | AffixStyles | — | Semantic inline styles per slot |
Types
type AffixSemanticSlot = 'root' | 'affix'
type AffixClassNames = SemanticClassNames<AffixSemanticSlot>
type AffixStyles = SemanticStyles<AffixSemanticSlot>Behaviour
- Two divs:
rootis a normal-flow placeholder that holds the reserved height when affixed;affixis the inner element that switches toposition: fixed. - Affixed styles:
position: fixed; top | bottom: <computed>; left: <measured>; width: <measured>; z-index: 10. - Scroll listeners: passive listener on the target container +
windowresize. Whentargetis a custom element, an additional passivewindowscroll listener keeps the affixed element tracking the container as it moves within the page. - onChange: fires with
trueon the first tick that triggers pinning, andfalseon the first tick that releases it.
Examples
1. Stick to top of window
<Affix>
<Button>Fixed at top</Button>
</Affix>2. Offset from top
<Affix offsetTop={64}>
<nav>Site navigation</nav>
</Affix>3. Stick to bottom
<Affix offsetBottom={24}>
<div>Footer actions</div>
</Affix>4. onChange callback
<Affix offsetTop={0} onChange={(affixed) => console.log('affixed:', affixed)}>
<Button>Track me</Button>
</Affix>5. Custom scroll container
const containerRef = useRef<HTMLDivElement>(null)
<div ref={containerRef} style={{ height: 400, overflowY: 'auto' }}>
<Affix offsetTop={8} target={() => containerRef.current!}>
<Button>Sticky inside scroll area</Button>
</Affix>
<div style={{ height: 1200 }}>Long content…</div>
</div>6. Sticky toolbar
<Affix offsetTop={0}>
<div style={{ background: tokens.colorBg, padding: '0.5rem 1rem', borderBottom: `1px solid ${tokens.colorBorder}` }}>
<Button>Save</Button>
<Button variant="ghost">Cancel</Button>
</div>
</Affix>7. Semantic styles
<Affix
offsetTop={0}
styles={{
affix: { boxShadow: '0 2px 8px rgba(0,0,0,0.15)', background: tokens.colorBg },
}}
>
<nav>Navigation</nav>
</Affix>Alert
Alert is a feedback component that displays contextual messages to the user. It supports four severity types (success, info, warning, error) with automatic color theming (light/dark aware via color-mix), an optional icon, a close button with slide-out animation, a custom action slot, and a full-width banner mode. The compound Alert.ErrorBoundary wraps children and renders an error alert when a React error is caught.
import { Alert } from '@juanitte/inoui'Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| type | 'success' \| 'info' \| 'warning' \| 'error' | 'info' (or 'warning' in banner mode) | Severity level — controls background, border, and icon color |
| title | ReactNode | — | Main message text (bold when description is present) |
| description | ReactNode | — | Secondary description below the title |
| showIcon | boolean | false (or true in banner mode) | Show the type icon |
| icon | ReactNode | — | Custom icon replacing the default type icon |
| closable | boolean \| AlertClosable | false | Show close button; pass an object for closeIcon, onClose, and afterClose callbacks |
| action | ReactNode | — | Extra content (e.g. a button) placed on the right side |
| banner | boolean | false | Full-width banner mode: no border-radius, bottom border only, showIcon and type='warning' by default |
| className | string | — | CSS class on the root element |
| style | CSSProperties | — | Inline style on the root element |
| classNames | AlertClassNames | — | Semantic class names per slot |
| styles | AlertStyles | — | Semantic inline styles per slot |
AlertClosable
| Field | Type | Description |
|-------|------|-------------|
| closeIcon | ReactNode | Custom close icon |
| onClose | (e: React.MouseEvent) => void | Called when the close button is clicked |
| afterClose | () => void | Called after the close animation completes |
Alert.ErrorBoundary
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | Content to wrap |
| title | ReactNode | 'Something went wrong' | Error alert title |
| description | ReactNode | error.message | Error alert description |
Types
type AlertType = 'success' | 'info' | 'warning' | 'error'
type AlertSemanticSlot = 'root' | 'icon' | 'content' | 'message' | 'description' | 'action' | 'closeBtn'
type AlertClassNames = SemanticClassNames<AlertSemanticSlot>
type AlertStyles = SemanticStyles<AlertSemanticSlot>Close animation
When the alert is dismissed via the close button, the component:
- Fades out the alert card (opacity → 0, 250 ms ease).
- Collapses the wrapper height to 0 using
grid-template-rows: 0fr(300 ms ease). - After the collapse transition ends, calls
afterCloseand unmounts.
Examples
1. Basic types
<Alert type="success" title="Operation completed successfully" />
<Alert type="info" title="A new version is available" />
<Alert type="warning" title="Disk space running low" />
<Alert type="error" title="Failed to save changes" />2. With description
<Alert
type="success"
title="File uploaded"
description="document.pdf has been uploaded and is being processed. You will receive a notification once it's ready."
showIcon
/>3. Show icon
<Alert type="info" title="Tip: press Ctrl+K for quick search" showIcon />4. Custom icon
<Alert type="info" title="Scheduled maintenance tonight" icon={<span>🔧</span>} showIcon />5. Closable
<Alert
type="warning"
title="Your session will expire in 5 minutes"
closable
showIcon
/>6. Closable with callbacks
<Alert
type="error"
title="Connection lost"
closable={{
onClose: () => console.log('Alert closed'),
afterClose: () => console.log('Close animation finished'),
}}
showIcon
/>7. Custom close icon
<Alert
type="info"
title="Notification"
closable={{ closeIcon: <span>✕</span> }}
/>8. Action slot
<Alert
type="warning"
title="Unsaved changes"
description="You have unsaved changes that will be lost."
showIcon
action={<button onClick={() => save()}>Save now</button>}
closable
/>9. Banner mode
// Full-width banner — defaults to type="warning" and showIcon=true
<Alert banner title="The site will be under maintenance from 2:00 AM to 4:00 AM." />10. Banner with custom type
<Alert banner type="error" title="Service outage detected. Our team has been notified." />11. Title only (no description)
<Alert type="info" title="Press Enter to confirm" showIcon />12. Description only (no title)
<Alert type="warning" description="Some features may not work as expected in this browser." showIcon />13. Error boundary
import { Alert } from '@juanitte/inoui'
function RiskyWidget() {
// This might throw during render
return <div>{JSON.parse('invalid')}</div>
}
<Alert.ErrorBoundary title="Widget crashed" description="Please refresh the page.">
<RiskyWidget />
</Alert.ErrorBoundary>14. Semantic styles
<Alert
type="success"
title="Payment received"
description="Your invoice has been updated."
showIcon
styles={{
root: { borderRadius: 12 },
message: { fontSize: '1rem' },
description: { fontStyle: 'italic' },
}}
/>15. Multiple stacked alerts
import { useState } from 'react'
import { Alert, Button } from '@juanitte/inoui'
function Notifications() {
const [alerts, setAlerts] = useState([
{ id: 1, type: 'info' as const, title: 'Welcome back!' },
{ id: 2, type: 'warning' as const, title: 'Your trial expires tomorrow' },
{ id: 3, type: 'success' as const, title: 'Profile updated' },
])
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{alerts.map((a) => (
<Alert
key={a.id}
type={a.type}
title={a.title}
showIcon
closable={{
afterClose: () => setAlerts(prev => prev.filter(x => x.id !== a.id)),
}}
/>
))}
</div>
)
}AutoComplete
An input component with auto-complete suggestions. Supports filtering, grouped options, keyboard navigation, backfill, controlled/uncontrolled state, multiple variants, validation status, clear button, and auto-flip dropdown positioning.
Import
import { AutoComplete } from '@juanitte/inoui'Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| options | AutoCompleteOption[] | [] | Available options |
| value | string | — | Controlled input value |
| defaultValue | string | '' | Default input value (uncontrolled) |
| placeholder | string | — | Input placeholder text |
| open | boolean | — | Controlled dropdown visibility |
| defaultOpen | boolean | false | Default dropdown visibility |
| disabled | boolean | false | Disable the component |
| allowClear | boolean | false | Show clear button |
| autoFocus | boolean | false | Focus input on mount |
| backfill | boolean | false | Fill input with highlighted option value on keyboard navigation |
| defaultActiveFirstOption | boolean | true | Automatically highlight the first option |
| variant | 'outlined' \| 'filled' \| 'borderless' | 'outlined' | Input visual variant |
| status | 'error' \| 'warning' | — | Validation status |
| filterOption | boolean \| (inputValue, option) => boolean | true | Filter function. true = case-insensitive includes, false = no filtering |
| notFoundContent | ReactNode | null | Content when no matches. null = hide dropdown |
| popupMatchSelectWidth | boolean \| number | true | true = match input width, number = fixed width |
| onChange | (value: string) => void | — | Called when value changes |
| onSearch | (value: string) => void | — | Called when user types |
| onSelect | (value, option) => void | — | Called when an option is selected |
| onFocus | (e) => void | — | Called on focus |
| onBlur | (e) => void | — | Called on blur |
| onDropdownVisibleChange | (open: boolean) => void | — | Called when dropdown visibility changes |
| onClear | () => void | — | Called when input is cleared |
| prefix | ReactNode | — | Prefix content in the input (e.g. icon on the left) |
| suffix | ReactNode | — | Suffix content in the input (e.g. icon on the right) |
| className | string | — | CSS class for the root element |
| style | CSSProperties | — | Inline styles for the root element |
| classNames | AutoCompleteClassNames | — | CSS classes for internal parts |
| styles | AutoCompleteStyles | — | Inline styles for internal parts |
Types
type AutoCompleteVariant = 'outlined' | 'filled' | 'borderless'
type AutoCompleteStatus = 'error' | 'warning'
interface AutoCompleteOption {
value: string
label?: ReactNode
disabled?: boolean
options?: AutoCompleteOption[] // for grouped options
}Semantic DOM
| Slot | Description |
|------|-------------|
| root | Outer wrapper container |
| input | Input element |
| dropdown | Dropdown suggestions container |
| option | Individual option item |
Examples
Basic usage
<AutoComplete
options={[
{ value: 'React' },
{ value: 'Vue' },
{ value: 'Angular' },
]}
placeholder="Search framework..."
/>Controlled value
const [value, setValue] = useState('')
<AutoComplete
value={value}
onChange={setValue}
options={[
{ value: 'React' },
{ value: 'Vue' },
{ value: 'Angular' },
]}
/>Custom labels
<AutoComplete
options={[
{ value: 'react', label: <span><strong>React</strong> - A JavaScript library</span> },
{ value: 'vue', label: <span><strong>Vue</strong> - The Progressive Framework</span> },
]}
/>Grouped options
<AutoComplete
options={[
{
value: 'frameworks',
label: 'Frameworks',
options: [
{ value: 'React' },
{ value: 'Vue' },
],
},
{
value: 'languages',
label: 'Languages',
options: [
{ value: 'TypeScript' },
{ value: 'JavaScript' },
],
},
]}
/>Custom filter
<AutoComplete
filterOption={(inputValue, option) =>
option.value.toUpperCase().startsWith(inputValue.toUpperCase())
}
options={[
{ value: 'React' },
{ value: 'Redux' },
{ value: 'Vue' },
]}
/>No filtering (async search)
const [options, setOptions] = useState([])
<AutoComplete
filterOption={false}
options={options}
onSearch={async (text) => {
const results = await fetchSuggestions(text)
setOptions(results.map((r) => ({ value: r })))
}}
/>Backfill mode
<AutoComplete
backfill
options={[
{ value: 'apple' },
{ value: 'banana' },
{ value: 'cherry' },
]}
/>Prefix & Suffix
// Prefix only
<AutoComplete
prefix={<MailIcon />}
options={[{ value: 'Option 1' }, { value: 'Option 2' }]}
placeholder="With prefix"
/>
// Suffix only
<AutoComplete
suffix={<SearchIcon />}
options={[{ value: 'Option 1' }, { value: 'Option 2' }]}
placeholder="With suffix"
/>
// Prefix + Suffix + Clear
<AutoComplete
prefix={<GlobeIcon />}
suffix={<SearchIcon />}
allowClear
options={[{ value: 'Option 1' }, { value: 'Option 2' }]}
placeholder="With both"
/>Variants
<AutoComplete variant="outlined" options={[...]} placeholder="Outlined" />
<AutoComplete variant="filled" options={[...]} placeholder="Filled" />
<AutoComplete variant="borderless" options={[...]} placeholder="Borderless" />Validation status
<AutoComplete status="error" options={[...]} placeholder="Error" />
<AutoComplete status="warning" options={[...]} placeholder="Warning" />Not found content
<AutoComplete
options={[]}
notFoundContent="No results found"
placeholder="Search..."
/>Disabled
<AutoComplete disabled options={[{ value: 'React' }]} value="React" />Avatar
Avatar displays a user's profile picture, icon, or initials. It supports images, custom icons, text fallbacks, and badge indicators. Includes Avatar.Group for displaying overlapping avatar lists.
Import:
import { Avatar } from '@juanitte/inoui';Avatar Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| src | string | undefined | Image source URL |
| srcSet | string | undefined | Responsive image sources |
| alt | string | undefined | Alternative text for image |
| icon | ReactNode | <UserIcon /> | Custom icon (shown when no image) |
| shape | 'circle' \| 'square' | 'circle' | Avatar shape |
| size | 'small' \| 'default' \| 'large' \| number \| AvatarResponsiveSize | 'default' | Avatar size |
| gap | number | 4 | Gap between text and container edge (for auto-scaling) |
| count | number | undefined | Badge count (shows as "99+" if > 99) |
| dot | boolean | false | Show dot indicator |
| draggable | boolean \| 'true' \| 'false' | true | Whether image is draggable |
| crossOrigin | '' \| 'anonymous' \| 'use-credentials' | undefined | CORS setting for image |
| onError | () => boolean | undefined | Callback when image fails to load (return false to prevent fallback) |
| children | ReactNode | undefined | Text content (usually initials) |
| className | string | undefined | Root element class name |
| style | CSSProperties | undefined | Root element inline styles |
| classNames | SemanticClassNames<AvatarSemanticSlot> | undefined | Semantic class names |
| styles | SemanticStyles<AvatarSemanticSlot> | undefined | Semantic inline styles |
Avatar.Group Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| max | { count?: number; style?: CSSProperties } | undefined | Maximum avatars to show (rest shown as "+N") |
| size | AvatarSize | 'default' | Size applied to all child avatars |
| shape | 'circle' \| 'square' | 'circle' | Shape applied to all child avatars |
| children | ReactNode | undefined | Avatar components |
| className | string | undefined | Container class name |
| style | CSSProperties | undefined | Container inline styles |
AvatarResponsiveSize Type
AvatarResponsiveSize is an alias for ResponsiveValue<number> from the shared Utils breakpoint system:
// Equivalent to:
type AvatarResponsiveSize = Partial<Record<Breakpoint, number>>
// where Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'See Breakpoint Utilities for the full type definitions and breakpoint values.
Size Configuration
| Size | Dimension |
|------|-----------|
| small | 24px (1.5rem) |
| default | 32px (2rem) |
| large | 40px (2.5rem) |
| Custom number | Specified pixels |
| Responsive object | Size based on viewport breakpoint |
Semantic DOM Slots
| Slot | Description |
|------|-------------|
| root | The root container element |
| image | The image element (when src is provided) |
| icon | The icon container (when icon is provided) |
| text | The text container (when children text is provided) |
Examples
Basic usage with image:
import { Avatar } from '@juanitte/inoui';
<Avatar src="https://i.pravatar.cc/150?img=1" alt="User" />With initials:
<Avatar>JD</Avatar>With custom icon:
<Avatar icon={<UserIcon />} />Different shapes:
<Avatar src="user.jpg" shape="circle" />
<Avatar src="user.jpg" shape="square" />Different sizes:
<Avatar src="user.jpg" size="small" />
<Avatar src="user.jpg" size="default" />
<Avatar src="user.jpg" size="large" />
<Avatar src="user.jpg" size={64} />Responsive size:
<Avatar
src="user.jpg"
size={{
xs: 24,
sm: 32,
md: 40,
lg: 48,
xl: 64,
}}
/>With badge count:
<Avatar src="user.jpg" count={5} />
<Avatar src="user.jpg" count={99} />
<Avatar src="user.jpg" count={100} /> {/* Shows "99+" */}With dot indicator:
<Avatar src="user.jpg" dot />Error handling:
<Avatar
src="invalid-url.jpg"
onError={() => {
console.log('Image failed to load');
return true; // Allow fallback to icon/text
}}
>
FB
</Avatar>Avatar Group:
<Avatar.Group>
<Avatar src="https://i.pravatar.cc/150?img=1" />
<Avatar src="https://i.pravatar.cc/150?img=2" />
<Avatar src="https://i.pravatar.cc/150?img=3" />
<Avatar src="https://i.pravatar.cc/150?img=4" />
</Avatar.Group>Group with max count:
<Avatar.Group max={{ count: 3 }}>
<Avatar src="https://i.pravatar.cc/150?img=1" />
<Avatar src="https://i.pravatar.cc/150?img=2" />
<Avatar src="https://i.pravatar.cc/150?img=3" />
<Avatar src="https://i.pravatar.cc/150?img=4" />
<Avatar src="https://i.pravatar.cc/150?img=5" />
</Avatar.Group>
{/* Shows first 3 avatars + "+2" */}Group with custom size and shape:
<Avatar.Group size="large" shape="square">
<Avatar src="user1.jpg" />
<Avatar src="user2.jpg" />
<Avatar>AB</Avatar>
<Avatar icon={<UserIcon />} />
</Avatar.Group>Custom styling for overflow avatar:
<Avatar.Group
max={{
count: 2,
style: { backgroundColor: '#f56a00', color: '#fff' }
}}
>
<Avatar src="user1.jpg" />
<Avatar src="user2.jpg" />
<Avatar src="user3.jpg" />
</Avatar.Group>Anchor
A navigation component that renders a list of anchor links and highlights the currently active one based on scroll position. Supports vertical and horizontal layouts with nested links.
Import
import { Anchor } from '@juanitte/inoui'Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| items | AnchorLinkItemProps[] | [] | Declarative list of anchor links |
| direction | 'vertical' \| 'horizontal' | 'vertical' | Navigation direction |
| offsetTop | number | 0 | Offset in px from the top when calculating scroll position |
| targetOffset | number | offsetTop | Offset for scroll destination when clicking a link |
| bounds | number | 5 | Tolerance distance in px for detecting the active section |
| getContainer | () => HTMLElement \| Window | () => window | Scrollable container |
| getCurrentAnchor | (activeLink: string) => string | — | Custom function to determine the active link |
| onChange | (currentActiveLink: string) => void | — | Callback when the active link changes |
| onClick | (e: MouseEvent, link: { title: ReactNode; href: string }) => void | — | Callback when a link is clicked |
| replace | boolean | false | Use replaceState instead of pushState for URL updates |
| className | string | — | Additional CSS class |
| style | CSSProperties | — | Additional inline styles |
AnchorLinkItemProps
interface AnchorLinkItemProps {
key: string // Unique key for the link
href: string // Link destination (must start with #)
title: ReactNode // Link content
children?: AnchorLinkItemProps[] // Nested links (vertical only)
}Semantic DOM
| Slot | Description |
|------|-------------|
| root | Outer wrapper element |
| track | Background track line (vertical mode) |
| indicator | Active link indicator |
| link | Individual anchor link |
Examples
// Basic vertical anchor
<Anchor
items={[
{ key: 'section1', href: '#section1', title: 'Section 1' },
{ key: 'section2', href: '#section2', title: 'Section 2' },
{ key: 'section3', href: '#section3', title: 'Section 3' },
]}
/>
// Horizontal anchor
<Anchor
direction="horizontal"
items={[
{ key: 'overview', href: '#overview', title: 'Overview' },
{ key: 'features', href: '#features', title: 'Features' },
{ key: 'api', href: '#api', title: 'API' },
]}
/>
// Nested links (vertical only)
<Anchor
items={[
{
key: 'components',
href: '#components',
title: 'Components',
children: [
{ key: 'button', href: '#button', title: 'Button' },
{ key: 'input', href: '#input', title: 'Input' },
],
},
{ key: 'hooks', href: '#hooks', title: 'Hooks' },
]}
/>
// With offset (e.g., for fixed header)
<Anchor
offsetTop={64}
items={[
{ key: 'intro', href: '#intro', title: 'Introduction' },
{ key: 'guide', href: '#guide', title: 'Guide' },
]}
/>
// Custom scroll container
<Anchor
getContainer={() => document.getElementById('my-container')!}
items={[
{ key: 'part1', href: '#part1', title: 'Part 1' },
{ key: 'part2', href: '#part2', title: 'Part 2' },
]}
/>
// With onChange callback
<Anchor
onChange={(activeLink) => console.log('Active:', activeLink)}
items={[
{ key: 'a', href: '#a', title: 'Section A' },
{ key: 'b', href: '#b', title: 'Section B' },
]}
/>
// Replace URL instead of push
<Anchor
replace
items={[
{ key: 'tab1', href: '#tab1', title: 'Tab 1' },
{ key: 'tab2', href: '#tab2', title: 'Tab 2' },
]}
/>App
App is a thin root-level provider that internally calls useModal() and usePopAlert(), mounts their contextHolder nodes, and exposes both APIs through React context. Any descendant component can call App.useApp() to access modal and notification without managing hooks or contextHolder placement itself.
import { App } from '@juanitte/inoui'Setup
// main.tsx (or your app entry point)
import { App } from '@juanitte/inoui'
createRoot(document.getElementById('root')!).render(
<App>
<MyApp />
</App>
)Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| notification | PopAlertHookConfig | — | Configuration forwarded to the internal usePopAlert() call (placement, size, duration, maxCount, offset) |
| children | ReactNode | — | Application content |
| className | string | — | CSS class on the root <div> |
| style | CSSProperties | — | Inline style on the root <div> |
App.useApp()
const { modal, notification } = App.useApp()Returns the AppContextValue from the nearest <App> ancestor. Throws if called outside an <App>.
| Field | Type | Description |
|-------|------|-------------|
| modal | ModalHookApi | Programmatic modal API — same as the modal returned by useModal() |
| notification | PopAlertApi | Toast notification API — same as the api returned by usePopAlert() |
See Modal → useModal hook and PopAlert → PopAlertApi for the full method reference.
Context value types
interface AppContextValue {
modal: ModalHookApi // confirm, info, success, warning, error, destroyAll
notification: PopAlertApi // open, success, error, info, warning, loading, destroy
}Examples
1. Basic setup
import { App } from '@juanitte/inoui'
createRoot(document.getElementById('root')!).render(
<App>
<MyApp />
</App>
)2. With notification defaults
<App notification={{ placement: 'topRight', duration: 4, maxCount: 5 }}>
<MyApp />
</App>3. Using App.useApp() in a child component
import { App, Button } from '@juanitte/inoui'
function DeleteButton({ id }: { id: number }) {
const { modal, notification } = App.useApp()
const handleClick = () => {
modal.confirm({
title: 'Delete this item?',
content: 'This action cannot be undone.',
onOk: async () => {
await deleteItem(id)
notification.success('Item deleted.')
},
})
}
return <Button variant="danger" onClick={handleClick}>Delete</Button>
}4. Notifications from anywhere in the tree
function SaveButton() {
const { notification } = App.useApp()
return (
<Button onClick={async () => {
await save()
notification.success('Saved!')
}}>
Save
</Button>
)
}5. Programmatic confirm dialog
function LogoutButton() {
const { modal } = App.useApp()
return (
<Button onClick={() => modal.confirm({
title: 'Log out?',
onOk: logout,
})}>
Log out
</Button>
)
}Badge
A small numerical value or status indicator for UI elements. Wraps any content with a floating count badge, dot indicator, or displays standalone status dots.
Import
import { Badge } from '@juanitte/inoui'Badge Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | Content to wrap (badge floats at top-right) |
| count | ReactNode | — | Number or custom content shown in the badge |
| overflowCount | number | 99 | Max count before showing {overflowCount}+ |
| dot | boolean | false | Show a dot indicator instead of a count |
| showZero | boolean | false | Show the badge when count is zero |
| size | 'default' \| 'small' | 'default' | Badge size |
| status | 'success' \| 'processing' \| 'default' \| 'error' \| 'warning' | — | Status indicator (standalone or with child) |
| text | ReactNode | — | Text shown next to the status dot (standalone mode only) |
| color | string | — | Custom color (preset name or any CSS color) |
| offset | [number, number] | — | [right, top] offset in pixels for the badge position |
| title | string | — | Tooltip title for the badge indicator |
| className | string | — | Class for the root element |
| style | CSSProperties | — | Style for the root element |
| classNames | BadgeClassNames | — | Semantic class names |
| styles | BadgeStyles | — | Semantic styles |
Preset Colors
pink · red · yellow · orange · cyan · green · blue · purple · geekblue · magenta · volcano · gold · lime
Badge.Ribbon Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | Content to wrap |
| text | ReactNode | — | Text displayed inside the ribbon |
| color | string | tokens.colorPrimary | Ribbon color (preset name or CSS color) |
| placement | 'start' \| 'end' | 'end' | Which corner the ribbon appears on |
| className | string | — | Class for the wrapper element |
| style | CSSProperties | — | Style for the wrapper element |
| classNames | RibbonClassNames | — | Semantic class names |
| styles | RibbonStyles | — | Semantic styles |
Semantic DOM – Badge
| Slot | Description |
|------|-------------|
| root | Outer wrapper element |
| indicator | The count badge / dot element (<sup>) |
Semantic DOM – Badge.Ribbon
| Slot | Description |
|------|-------------|
| wrapper | Outer wrapper element |
| ribbon | The ribbon strip |
| content | Text content inside the ribbon |
| corner | The folded corner triangle |
Examples
// Count badge wrapping content
<Badge count={5}>
<Avatar shape="square" />
</Badge>
// Overflow count (shows "99+")
<Badge count={120}>
<Avatar shape="square" />
</Badge>
// Show zero
<Badge count={0} showZero>
<Avatar shape="square" />
</Badge>
// Dot indicator
<Badge dot>
<NotificationIcon />
</Badge>
// Small size
<Badge count={5} size="small">
<Avatar shape="square" />
</Badge>
// Custom offset
<Badge count={5} offset={[10, 10]}>
<Avatar shape="square" />
</Badge>
// Status dots (standalone, no children)
<Badge status="success" text="Completed" />
<Badge status="processing" text="In Progress" />
<Badge status="error" text="Failed" />
<Badge status="warning" text="Warning" />
<Badge status="default" text="Inactive" />
// Preset colors
<Badge color="blue" count={8}>
<Avatar shape="square" />
</Badge>
<Badge color="volcano" count={3}>
<Avatar shape="square" />
</Badge>
// Custom CSS color
<Badge color="#faad14" count={10}>
<Avatar shape="square" />
</Badge>
// Standalone count (no children)
<Badge count={25} />
// Badge.Ribbon
<Badge.Ribbon text="Featured">
<Card>Card content</Card>
</Badge.Ribbon>
<Badge.Ribbon text="Sale" color="red" placement="start">
<Card>Card content</Card>
</Badge.Ribbon>Breadcrumb
A navigation component that displays the current location within a hierarchical structure. Supports custom separators, icons, dropdown menus, custom item rendering, and route parameters.
Import
import { Breadcrumb } from '@juanitte/inoui'Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| items | BreadcrumbItemType[] | [] | Breadcrumb items |
| separator | ReactNode | '/' | Separator between items |
| itemRender | (item, params, items, paths) => ReactNode | — | Custom render function for each item |
| params | Record<string, string> | — | Route parameters |
| className | string | — | Additional CSS class |
| style | CSSProperties | — | Additional inline styles |
| classNames | BreadcrumbClassNames | — | CSS classes for internal parts |
| styles | BreadcrumbStyles | — | Styles for internal parts |
BreadcrumbItemType
interface BreadcrumbItemType {
title?: ReactNode // Text/content of the item
href?: string // URL (renders as <a>)
path?: string // Path segment (accumulated for itemRender)
icon?: ReactNode // Icon before the title
onClick?: (e: MouseEvent) => void // Click handler
className?: string // Individual CSS class
style?: CSSProperties // Individual inline styles
menu?: { items: BreadcrumbMenuItemType[] } // Dropdown menu
}BreadcrumbMenuItemType
interface BreadcrumbMenuItemType {
key: string // Unique key
title: ReactNode // Item text
href?: string // Item URL
icon?: ReactNode // Item icon
onClick?: (e: MouseEvent) => void // Click handler
}Semantic DOM
| Slot | Description |
|------|-------------|
| root | Outer <nav> element |
| list | <ol> list container |
| item | Individual <li> breadcrumb item |
| separator | Separator <li> element between items |
| link | Link or text element inside each item |
| overlay | Dropdown menu container |
Examples
// Basic breadcrumb
<Breadcrumb
items={[
{ title: 'Home', href: '/' },
{ title: 'Products', href: '/products' },
{ title: 'Detail' },
]}
/>
// With icons
<Breadcrumb
items={[
{ title: 'Home', href: '/', icon: <HomeIcon /> },
{ title: 'Settings', href: '/settings', icon: <SettingsIcon /> },
{ title: 'Profile' },
]}
/>
// Custom separator
<Breadcrumb
separator=">"
items={[
{ title: 'Home', href: '/' },
{ title: 'Category', href: '/category' },
{ title: 'Item' },
]}
/>
// ReactNode separator
<Breadcrumb
separator={<span style={{ color: 'red' }}>→</span>}
items={[
{ title: 'Step 1', href: '#' },
{ title: 'Step 2', href: '#' },
{ title: 'Step 3' },
]}
/>
// With dropdown menu
<Breadcrumb
items={[
{ title: 'Home', href: '/' },
{
title: 'Category',
href: '/category',
menu: {
items: [
{ key: 'electronics', title: 'Electronics', href: '/electronics' },
{ key: 'clothing', title: 'Clothing', href: '/clothing' },
{ key: 'books', title: 'Books', href: '/books' },
],
},
},
{ title: 'Product' },
]}
/>
// With onClick handlers
<Breadcrumb
items={[
{ title: 'Home', onClick: () => navigate('/') },
{ title: 'Users', onClick: () => navigate('/users') },
{ title: 'John Doe' },
]}
/>
// Custom item render with paths
<Breadcrumb
items={[
{ title: 'Home', path: '' },
{ title: 'Users', path: 'users' },
{ title: ':id', path: ':id' },
]}
params={{ id: '42' }}
itemRender={(item, params, items, paths) => {
const last = items.indexOf(item) === items.length - 1
let path = paths[items.indexOf(item)]
// Replace params
if (params) {
Object.entries(params).forEach(([key, value]) => {
path = path.replace(`:${key}`, value)
})
}
return last ? (
<span>{item.title}</span>
) : (
<a href={`/${path}`}>{item.title}</a>
)
}}
/>
// With semantic DOM styling
<Breadcrumb
items={[
{ title: 'Home', href: '/' },
{ title: 'Products', href: '/products' },
{ title: 'Detail' },
]}
classNames={{ link: 'custom-link' }}
styles={{
separator: { color: '#999', margin: '0 12px' },
overlay: { borderRadius: 8 },
}}
/>Bubble
A floating action button (FAB) component for quick actions, with support for badges, tooltips, compact groups, and expandable menus.
Import
import { Bubble, BackToTopIcon, ChatIcon, BellIcon, CloseIcon } from '@juanitte/inoui'Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| icon | ReactNode | — | Icon to display |
| description | string | — | Text to display (only if no icon) |
| position | 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' | 'bottom-right' | Fixed position on screen |
| shape | 'circle' \| 'square' | 'circle' | Bubble shape |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Bubble size |
| color | 'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info' | 'primary' | Semantic color |
| badge | number \| boolean | — | Show badge with number or dot |
| badgeColor | 'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info' | 'error' | Badge color |
| tooltip | string | — | Tooltip text |
| tooltipPosition | 'left' \| 'right' \| 'top' \| 'bottom' | auto | Tooltip position |
| offsetX | number | 24 | Horizontal offset from edge (px) |
| offsetY | number | 24 | Vertical offset from edge (px) |
| shadow | boolean \| 'sm' \| 'md' \| 'lg' | 'lg' | Apply shadow |
| bordered | boolean | true | Show border |
| onBackToTop | () => void | — | Callback when scrolling to top |
| visibleOnScroll | number | — | Show only after scrolling (px) |
| disabled | boolean | false | Disable bubble |
Also accepts all standard <button> HTML attributes.
Sizes
| Size | Dimensions | Icon Size |
|------|------------|-----------|
| sm | 40px | 16px |
| md | 48px | 20px |
| lg | 56px | 24px |
Built-in Icons
Ino-UI provides utility icons for common FAB use cases:
| Icon | Description |
|------|-------------|
| BackToTopIcon | Arrow pointing up |
| ChatIcon | Chat/message bubble |
| BellIcon | Notification bell |
| CloseIcon | X close icon |
Semantic DOM
| Slot | Description |
|------|-------------|
| root | Outer wrapper element |
| icon | Icon element |
| badge | Badge counter element |
| tooltip | Tooltip popup element |
| tooltipArrow | Tooltip arrow element |
Bubble.Menu:
| Slot | Description |
|------|-------------|
| root | Menu wrapper element |
| trigger | Menu trigger element |
| menu | Menu container element |
Examples
// Basic
<Bubble icon={<ChatIcon />} />
// With tooltip
<Bubble icon={<ChatIcon />} tooltip="Open chat" />
// Different positions
<Bubble icon={<BellIcon />} position="top-right" />
<Bubble icon={<ChatIcon />} position="bottom-left" />
// With badge (number)
<Bubble icon={<BellIcon />} badge={5} />
// With badge (dot)
<Bubble icon={<ChatIcon />} badge={true} />
// Badge with custom color
<Bubble icon={<BellIcon />} badge={3} badgeColor="warning" />
// Different colors
<Bubble icon={<ChatIcon />} color="success" />
<Bubble icon={<BellIcon />} color="info" />
// Different sizes
<Bubble icon={<ChatIcon />} size="sm" />
<Bubble icon={<ChatIcon />} size="lg" />
// Square shape
<Bubble icon={<ChatIcon />} shape="square" />
// Without border
<Bubble icon={<ChatIcon />} bordered={false} />
// Back to top button (appears after scrolling 200px)
<Bubble
icon={<BackToTopIcon />}
tooltip="Back to top"
visibleOnScroll={200}
onBackToTop={() => console.log('Scrolled to top')}
/>
// Custom offset
<Bubble icon={<ChatIcon />} offsetX={40} offsetY={40} />
// With text instead of icon
<Bubble description="?" tooltip="Help" />Bubble.Group
A compact button bar that joins multiple Bubbles together with unified styling. The bubbles are rendered as a seamless group with shared shadow and automatic border-radius handling.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | Child Bubble components |
| position | 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' | 'bottom-right' | Fixed position on screen |
| direction | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' | Layout direction |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size for all children |
| offsetX | number | 24 | Horizontal offset from edge (px) |
| offsetY | number | 24 | Vertical offset from edge (px) |
| shadow | boolean \| 'sm' \| 'md' \| 'lg' | 'lg' | Shadow for the group container |
Direction
| Direction | Description |
|-----------|-------------|
| top | Bubbles stack upward from position |
| bottom | Bubbles stack downward from position |
| left | Bubbles stack leftward from position |
| right | Bubbles stack rightward from position |
Examples
// Vertical compact group (stacks upward)
<Bubble.Group>
<Bubble icon={<ChatIcon />} color="info" />
<Bubble icon={<BellIcon />} color="warning" />
<Bubble icon={<BackToTopIcon />} color="success" />
</Bubble.Group>
// Horizontal compact group
<Bubble.Group direction="left">
<Bubble icon={<ChatIcon />} color="primary" />
<Bubble icon={<BellIcon />} color="secondary" />
</Bubble.Group>
// Different position
<Bubble.Group position="top-right" direction="bottom">
<Bubble icon={<ChatIcon />} />
<Bubble icon={<BellIcon />} />
</Bubble.Group>
// With badges (badges extend outside the group)
<Bubble.Group>
<Bubble icon={<ChatIcon />} badge={3} />
<Bubble icon={<BellIcon />} badge={true} badgeColor="warning" />
</Bubble.Group>
// Custom size
<Bubble.Group size="lg">
<Bubble icon={<ChatIcon />} />
<Bubble icon={<BellIcon />} />
</Bubble.Group>Bubble.Menu
An expandable floating menu that shows/hides child Bubbles with animation. Supports click or hover activation.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | Child Bubble components |
| position | 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' | 'bottom-right' | Fixed position on screen |
| direction | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' | Expansion direction |
| trigger | 'click' \| 'hover' | 'click' | Activation mode |
| icon | ReactNode | + | Trigger icon when closed |
| openIcon | ReactNode | — | Trigger icon when open (defaults to rotating the icon 45deg) |
| shape | 'circle' \| 'square' | 'circle' | Shape for trigger and children |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size for trigger and children |
| color | 'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info' | 'primary' | Trigger color |
| offsetX | number | 24 | Horizontal offset from edge (px) |
| offsetY | number | 24 | Vertical offset from edge (px) |
| shadow | boolean \| 'sm' \| 'md' \| 'lg' | 'lg' | Apply shadow |
| tooltip | string | — | Tooltip for trigger (shown when closed) |
| defaultOpen | boolean | false | Initially open (uncontrolled) |
| open | boolean | — | Controlled open state |
| onOpenChange | (open: boolean) => void | — | Callback when state changes |
| gap | number | 12 | Space between bubbles (px) |
Examples
// Basic expandable menu (click to open)
<Bubble.Menu>
<Bubble icon={<ChatIcon />} tooltip="Chat" color="info" />
<Bubble icon={<BellIcon />} tooltip="Notifications" color="warning" />
</Bubble.Menu>
// Hover to open
<Bubble.Menu trigger="hover">
<Bubble icon={<ChatIcon />} tooltip="Chat" />
<Bubble icon={<BellIcon />} tooltip="Alerts" />
</Bubble.Menu>
// Custom trigger icons
<Bubble.Menu
icon={<ChatIcon />}
openIcon={<CloseIcon />}
color="success"
>
<Bubble icon={<BellIcon />} tooltip="Notifications" />
<Bubble icon={<BackToTopIcon />} tooltip="Back to top" />
</Bubble.Menu>
// Horizontal expansion
<Bubble.Menu direction="left">
<Bubble icon={<ChatIcon />} />
<Bubble icon={<BellIcon />} />
</Bubble.Menu>
// Controlled state
function MyComponent() {
const [open, setOpen] = useState(false)
return (
<Bubble.Menu
open={open}
onOpenChange={setOpen}
tooltip="Actions"
>
<Bubble icon={<ChatIcon />} onClick={() => openChat()} />
<Bubble icon={<BellIcon />} onClick={() => openNotifications()} />
</Bubble.Menu>
)
}
// Different position
<Bubble.Menu position="top-left" direction="bottom">
<Bubble icon={<ChatIcon />} />
<Bubble icon={<BellIcon />} />
</Bubble.Menu>
// Inline mode (not fixed, flows with content)
<Bubble.Menu style={{ position: 'relative' }}>
<Bubble icon={<ChatIcon />} />
<Bubble icon={<BellIcon />} />
</Bubble.Menu>
// Custom gap
<Bubble.Menu gap={20}>
<Bubble icon={<ChatIcon />} />
<Bubble icon={<BellIcon />} />
</Bubble.Menu>Button
A versatile button component with multiple variants, sizes, and states.
Import
import { Button } from '@juanitte/inoui'Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'primary' \| 'secondary' \| 'outline' \| 'dashed' \| 'ghost' \| 'link' | 'primary' | Button style variant |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Button size |
| color | 'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info' | 'primary' | Semantic color |
| icon | ReactNode | — | Icon element |
| iconPlacement | 'start' \| 'end' | 'start' | Icon position relative to text |
| loading | boolean | false | Show loading spinner |
| shadow | boolean \| 'sm' \| 'md' \| 'lg' | false | Apply shadow |
| clickAnimation | 'pulse' \| 'ripple' \| 'shake' \| 'firecracker' \| 'confetti' | — | Animation on click |
| hoverAnimation | 'pulse' \| 'ripple' \| 'shake' \| 'firecracker' \| 'confetti' | — | Animation on hover |
| gradient | 'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info' | — | Preset gradient using theme color |
| `gradi
