npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@juanitte/inoui

v1.1.4

Published

A modern React UI component library

Readme

Ino-UI

License: MIT

🇪🇸 Leer en Español

A modern, lightweight React component library with built-in theming support.

Table of Contents

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/inoui

Quick 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 parts
  • styles — 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):

  1. Component base styles — internal defaults
  2. styles.slot — semantic styles for each slot
  3. style prop — direct style prop (only applies to root)

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 cached

Returns 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: root is a normal-flow placeholder that holds the reserved height when affixed; affix is the inner element that switches to position: fixed.
  • Affixed styles: position: fixed; top | bottom: <computed>; left: <measured>; width: <measured>; z-index: 10.
  • Scroll listeners: passive listener on the target container + window resize. When target is a custom element, an additional passive window scroll listener keeps the affixed element tracking the container as it moves within the page.
  • onChange: fires with true on the first tick that triggers pinning, and false on 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:

  1. Fades out the alert card (opacity → 0, 250 ms ease).
  2. Collapses the wrapper height to 0 using grid-template-rows: 0fr (300 ms ease).
  3. After the collapse transition ends, calls afterClose and 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 ModaluseModal hook and PopAlertPopAlertApi 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