dosieungon-ui
v0.1.8
Published
A lightweight, beautiful React UI component library
Maintainers
Readme
dosieungon-ui
Lightweight, beautiful React UI component library. Install and use — no extra config needed.
📖 Full documentation: dosieungon.com/ui
npm install dosieungon-ui- Zero runtime dependencies
- CSS auto-injected — no import needed
- TypeScript ready (full type support)
- Dark mode via built-in
DSNProvider— auto follows system, persists to localStorage - Works with React 17+, Next.js (App Router & Pages Router)
Quick Start
import { Button, Modal, Select } from 'dosieungon-ui';
export default function App() {
return <Button>Click me</Button>;
}That's it. No CSS import. No provider wrapper.
Table of Contents
- DSNProvider & Dark Mode
- Button
- Input
- InputNumber
- Select
- Checkbox
- Radio
- Switch
- Segmented
- Modal
- Drawer
- Card
- Chip
- Table
- Breadcrumb
- Tooltip
- Popover
- Popconfirm
- Notification
- DatePicker
- TimePicker
- DateTimePicker
- ColorPicker
- Skeleton
- Customization
Button
import { Button } from 'dosieungon-ui';Variants
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button variant="warning">Warning</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>Outline with color
<Button variant="outline" color="danger">Delete</Button>
<Button variant="outline" color="warning">Warning</Button>
<Button variant="outline" color="secondary">Cancel</Button>Sizes
<Button size="sm">Small</Button>
<Button size="md">Medium</Button> {/* default */}
<Button size="lg">Large</Button>Loading
<Button loading>Saving...</Button>
<Button variant="danger" loading>Deleting</Button>With icons
import { Button } from 'dosieungon-ui';
// Left icon
<Button leftIcon={<SearchIcon />}>Search</Button>
// Right icon
<Button rightIcon={<ArrowIcon />}>Next</Button>
// Icon only (square button)
<Button iconOnly leftIcon={<PlusIcon />} aria-label="Add" />Rounded
<Button rounded="sm">Slightly rounded</Button>
<Button rounded="lg">More rounded</Button>
<Button rounded="full">Pill button</Button>
<Button rounded="none">Sharp corners</Button>All Button props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'primary' \| 'secondary' \| 'danger' \| 'warning' \| 'outline' \| 'ghost' | 'primary' | Visual style |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| loading | boolean | false | Show spinner, disable interaction |
| iconOnly | boolean | false | Square button, hides text |
| leftIcon | ReactNode | — | Icon before label |
| rightIcon | ReactNode | — | Icon after label |
| color | 'danger' \| 'warning' \| 'secondary' | — | Color for outline variant |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius |
| className | string | — | Extra CSS class |
| ... | ButtonHTMLAttributes | — | All native <button> props |
Input
import { Input } from 'dosieungon-ui';Basic
<Input placeholder="Enter your name" />With label and helper text
<Input
label="Email"
placeholder="[email protected]"
hint="We'll never share your email."
/>Error state
<Input
label="Password"
type="password"
error="Password must be at least 8 characters."
/>Required field
<Input label="Full Name" required placeholder="John Doe" />With icons
<Input
leftIcon={<SearchIcon />}
placeholder="Search..."
/>
<Input
rightIcon={<LockIcon />}
type="password"
placeholder="Password"
/>Sizes
<Input size="sm" placeholder="Small" />
<Input size="md" placeholder="Medium" /> {/* default */}
<Input size="lg" placeholder="Large" />Textarea
<Input
multiline
rows={4}
label="Message"
placeholder="Write something..."
/>Controlled
const [value, setValue] = useState('');
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
label="Username"
/>All Input props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| label | string | — | Label above input |
| required | boolean | false | Adds * to label |
| error | string | — | Error message (red border + text) |
| hint | string | — | Helper text below input |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| leftIcon | ReactNode | — | Icon on the left |
| rightIcon | ReactNode | — | Icon on the right |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius |
| multiline | true | — | Render as <textarea> |
| rows | number | — | Rows (textarea only) |
| wrapperClassName | string | — | Class on the input wrapper div |
| className | string | — | Class on the root element |
| ... | InputHTMLAttributes | — | All native <input> props |
Select
Custom dropdown — single select, multi-select, search, option groups.
import { Select } from 'dosieungon-ui';Basic
const [value, setValue] = useState('');
<Select
options={[
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'svelte', label: 'Svelte' },
]}
value={value}
onChange={(val) => setValue(val as string)}
placeholder="Choose a framework"
/>With label and error
<Select
label="Country"
required
options={countries}
value={country}
onChange={(val) => setCountry(val as string)}
error={!country ? 'Please select a country.' : undefined}
placeholder="Select country"
/>Searchable
<Select
options={largeList}
searchable
placeholder="Search and select..."
/>Clearable
<Select
options={options}
value={value}
onChange={setValue}
clearable
/>Multi-select
const [tags, setTags] = useState<string[]>([]);
<Select
options={[
{ value: 'js', label: 'JavaScript' },
{ value: 'ts', label: 'TypeScript' },
{ value: 'py', label: 'Python' },
]}
value={tags}
onChange={(val) => setTags(val as string[])}
multiple
searchable
clearable
placeholder="Select languages"
/>Option groups
<Select
options={[
{
group: 'Frontend',
options: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
],
},
{
group: 'Backend',
options: [
{ value: 'node', label: 'Node.js' },
{ value: 'go', label: 'Go' },
],
},
]}
placeholder="Select tech"
/>Disabled options
<Select
options={[
{ value: 'free', label: 'Free Plan' },
{ value: 'pro', label: 'Pro Plan' },
{ value: 'enterprise', label: 'Enterprise', disabled: true },
]}
/>Uncontrolled
<Select
options={options}
defaultValue="react"
onChange={(val) => console.log(val)}
/>All Select props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| options | (SelectOption \| SelectGroup)[] | — | Required. Option list |
| value | string \| number \| (string\|number)[] | — | Controlled value |
| defaultValue | same as value | — | Uncontrolled initial value |
| onChange | (value) => void | — | Change handler |
| multiple | boolean | false | Multi-select mode |
| searchable | boolean | false | Show search input |
| clearable | boolean | false | Show clear button |
| placeholder | string | 'Select...' | Placeholder text |
| disabled | boolean | false | Disable the select |
| label | string | — | Label above the select |
| required | boolean | false | Adds * to label |
| error | string | — | Error message |
| hint | string | — | Helper text |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius |
| emptyText | string | 'No options found' | Empty search result text |
| className | string | — | Class on root element |
SelectOption shape
interface SelectOption {
value: string | number;
label: string;
disabled?: boolean;
}SelectGroup shape
interface SelectGroup {
group: string; // group header label
options: SelectOption[];
}Checkbox
import { Checkbox } from 'dosieungon-ui';Basic (uncontrolled)
<Checkbox label="Accept terms and conditions" />Controlled
const [checked, setChecked] = useState(false);
<Checkbox
label="Subscribe to newsletter"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>Indeterminate
Useful for "select all" patterns.
const allChecked = items.every(i => i.checked);
const someChecked = items.some(i => i.checked);
<Checkbox
label="Select all"
checked={allChecked}
indeterminate={!allChecked && someChecked}
onChange={(e) => toggleAll(e.target.checked)}
/>Sizes
<Checkbox size="sm" label="Small" />
<Checkbox size="md" label="Medium" /> {/* default */}
<Checkbox size="lg" label="Large" />All Checkbox props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| checked | boolean | — | Controlled checked state |
| indeterminate | boolean | false | Shows dash (partially checked) |
| label | ReactNode | — | Label text |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius of the indicator |
| disabled | boolean | false | Disable interaction |
| className | string | — | Class on root element |
| ... | InputHTMLAttributes | — | All native <input> props (except type, size) |
Radio
import { Radio } from 'dosieungon-ui';Basic
const [plan, setPlan] = useState('free');
<Radio.Group value={plan} onChange={(val) => setPlan(val as string)}>
<Radio value="free" label="Free" />
<Radio value="pro" label="Pro" />
<Radio value="enterprise" label="Enterprise" />
</Radio.Group>Horizontal layout
<Radio.Group
value={value}
onChange={setValue}
direction="horizontal"
>
<Radio value="yes" label="Yes" />
<Radio value="no" label="No" />
</Radio.Group>Disabled
{/* Disable entire group */}
<Radio.Group disabled value={value} onChange={setValue}>
<Radio value="a" label="Option A" />
<Radio value="b" label="Option B" />
</Radio.Group>
{/* Disable single option */}
<Radio.Group value={value} onChange={setValue}>
<Radio value="free" label="Free" />
<Radio value="pro" label="Pro (unavailable)" disabled />
</Radio.Group>Uncontrolled
<Radio.Group defaultValue="free" onChange={(val) => console.log(val)}>
<Radio value="free" label="Free" />
<Radio value="pro" label="Pro" />
</Radio.Group>Radio.Group props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string \| number | — | Controlled value |
| defaultValue | string \| number | — | Uncontrolled initial value |
| onChange | (value) => void | — | Change handler |
| direction | 'vertical' \| 'horizontal' | 'vertical' | Layout direction |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius of the indicator |
| disabled | boolean | false | Disable all radios |
| name | string | auto-generated | HTML name attribute |
| className | string | — | Class on group wrapper |
Radio props
| Prop | Type | Description |
|------|------|-------------|
| value | string \| number | Required. Option value |
| label | ReactNode | Label text |
| disabled | boolean | Disable this option |
Switch
import { Switch } from 'dosieungon-ui';Basic
const [enabled, setEnabled] = useState(false);
<Switch
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
label="Enable notifications"
/>Label on the left
<Switch
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
label="Dark mode"
labelPosition="left"
/>Sizes
<Switch size="sm" checked={v} onChange={...} label="Small" />
<Switch size="md" checked={v} onChange={...} label="Medium" /> {/* default */}
<Switch size="lg" checked={v} onChange={...} label="Large" />All Switch props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| checked | boolean | false | Controlled state |
| onChange | ChangeEventHandler | — | Change handler |
| label | ReactNode | — | Label text |
| labelPosition | 'left' \| 'right' | 'right' | Label side |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius of the track |
| disabled | boolean | false | Disable interaction |
| className | string | — | Class on root element |
| ... | InputHTMLAttributes | — | All native <input> props (except type, size) |
Modal
import { Modal, Button } from 'dosieungon-ui';Basic
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
title="Confirm Action"
>
<p>Are you sure you want to continue?</p>
</Modal>With footer actions
<Modal
open={open}
onClose={() => setOpen(false)}
title="Delete Item"
footer={
<>
<Button variant="outline" color="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button variant="danger" onClick={handleDelete}>
Delete
</Button>
</>
}
>
<p>This action cannot be undone.</p>
</Modal>Sizes
<Modal open={open} onClose={onClose} size="sm">...</Modal> {/* 380px */}
<Modal open={open} onClose={onClose} size="md">...</Modal> {/* 520px — default */}
<Modal open={open} onClose={onClose} size="lg">...</Modal> {/* 720px */}
<Modal open={open} onClose={onClose} size="xl">...</Modal> {/* 960px */}
<Modal open={open} onClose={onClose} size="full">...</Modal> {/* full viewport */}Prevent accidental close
<Modal
open={open}
onClose={() => setOpen(false)}
closeOnBackdrop={false}
closeOnEscape={false}
title="Required Action"
>
You must complete this form.
</Modal>All Modal props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | — | Required. Controls visibility |
| onClose | () => void | — | Required. Called on close |
| title | ReactNode | — | Header title |
| size | 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full' | 'md' | Width preset |
| footer | ReactNode | — | Footer content (actions) |
| closeOnBackdrop | boolean | true | Close on backdrop click |
| closeOnEscape | boolean | true | Close on Escape key |
| hideCloseButton | boolean | false | Hide the × button |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius of the panel |
| className | string | — | Class on modal panel |
| bodyClassName | string | — | Class on body area |
Drawer
import { Drawer, Button } from 'dosieungon-ui';Basic
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open Drawer</Button>
<Drawer
open={open}
onClose={() => setOpen(false)}
title="Settings"
>
<p>Drawer content goes here.</p>
</Drawer>Placements
<Drawer placement="right" ...> {/* default — slides from right */}
<Drawer placement="left" ...> {/* slides from left */}
<Drawer placement="top" ...> {/* slides from top */}
<Drawer placement="bottom" ...> {/* slides from bottom — sheet style */}Sizes
For left/right: controls width.
For top/bottom: controls max-height.
<Drawer size="sm" ...> {/* left/right: 280px | top/bottom: 30vh */}
<Drawer size="md" ...> {/* left/right: 400px | top/bottom: 50vh — default */}
<Drawer size="lg" ...> {/* left/right: 560px | top/bottom: 70vh */}
<Drawer size="full" ...> {/* left/right: 100vw | top/bottom: 100vh */}With footer
<Drawer
open={open}
onClose={() => setOpen(false)}
title="Edit Profile"
footer={
<>
<Button variant="outline" color="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button onClick={handleSave}>Save</Button>
</>
}
>
<form>...</form>
</Drawer>All Drawer props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | — | Required. Controls visibility |
| onClose | () => void | — | Required. Called on close |
| placement | 'left' \| 'right' \| 'top' \| 'bottom' | 'right' | Slide direction |
| size | 'sm' \| 'md' \| 'lg' \| 'full' | 'md' | Size preset |
| title | ReactNode | — | Header title |
| footer | ReactNode | — | Footer content |
| closeOnBackdrop | boolean | true | Close on backdrop click |
| closeOnEscape | boolean | true | Close on Escape key |
| hideCloseButton | boolean | false | Hide the × button |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius of the panel |
| className | string | — | Class on drawer panel |
| bodyClassName | string | — | Class on body area |
Card
import { Card } from 'dosieungon-ui';Basic
<Card>
<p>Simple card content.</p>
</Card>With header and footer
<Card
header="User Profile"
footer={<Button size="sm">Edit</Button>}
>
<p>John Doe — Software Engineer</p>
</Card>Hoverable
<Card hoverable>
Hover me for a lift effect.
</Card>Clickable
<Card clickable onClick={() => router.push('/details')}>
Click anywhere on this card.
</Card>Shadow variant
<Card shadow>
No border, floating shadow.
</Card>All Card props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| header | ReactNode | — | Header content |
| footer | ReactNode | — | Footer content |
| hoverable | boolean | false | Lift animation on hover |
| clickable | boolean | false | Renders as <button>, full click area |
| shadow | boolean | false | Shadow instead of border |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius |
| bodyClassName | string | — | Class on body section |
| className | string | — | Class on root element |
| ... | HTMLAttributes<HTMLDivElement> | — | All native div/button props |
Chip
import { Chip } from 'dosieungon-ui';Basic
<Chip>Label</Chip>
<Chip color="danger">Error</Chip>
<Chip color="success">Active</Chip>
<Chip color="warning">Pending</Chip>
<Chip color="secondary">Draft</Chip>Outlined
<Chip variant="outlined">Default</Chip>
<Chip variant="outlined" color="danger">Danger</Chip>
<Chip variant="outlined" color="success">Success</Chip>Closable
const [tags, setTags] = useState(['React', 'TypeScript', 'Vite']);
{tags.map(tag => (
<Chip
key={tag}
onClose={() => setTags(tags.filter(t => t !== tag))}
>
{tag}
</Chip>
))}Clickable
<Chip onClick={() => setFilter('all')}>All</Chip>
<Chip onClick={() => setFilter('active')}>Active</Chip>Sizes
<Chip size="sm">Small</Chip>
<Chip size="md">Medium</Chip> {/* default */}
<Chip size="lg">Large</Chip>All Chip props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'filled' \| 'outlined' | 'filled' | Visual style |
| color | 'primary' \| 'secondary' \| 'danger' \| 'warning' \| 'success' | 'primary' | Color |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius |
| onClose | (e) => void | — | Show × button, called on click |
| onClick | (e) => void | — | Makes chip interactive (renders as <button>) |
| className | string | — | Extra CSS class |
Table
Full-featured data table with sorting, fixed columns, inner scroll, pagination, and custom colors.
import { Table } from 'dosieungon-ui';
import type { TableColumn } from 'dosieungon-ui';Basic
const columns: TableColumn<User>[] = [
{ key: 'id', title: '#', dataIndex: 'id', width: 56, align: 'center' },
{ key: 'name', title: 'Name', dataIndex: 'name', sortable: true },
{ key: 'dept', title: 'Dept', dataIndex: 'dept' },
{ key: 'score', title: 'Score',dataIndex: 'score', sortable: true, align: 'right' },
];
<Table columns={columns} data={users} rowKey="id" hoverable />Custom cell renderer
const columns: TableColumn<User>[] = [
{
key: 'status',
title: 'Status',
dataIndex: 'status',
render: (value, row) => (
<Chip color={value === 'Active' ? 'success' : 'secondary'} size="sm">
{value}
</Chip>
),
},
];Fixed columns (horizontal scroll)
const columns: TableColumn<User>[] = [
{ key: 'id', title: '#', dataIndex: 'id', width: 60, fixed: 'left' },
{ key: 'name', title: 'Name', dataIndex: 'name', width: 160, fixed: 'left' },
// ... scrollable columns ...
{ key: 'status', title: 'Status', dataIndex: 'status', width: 120, fixed: 'right' },
];
<Table columns={columns} data={data} rowKey="id" />Inner scroll (scrollY)
<Table
columns={columns}
data={data}
rowKey="id"
scrollY={300} // max body height; header sticks automatically
hoverable
/>Pagination
<Table
columns={columns}
data={data} // pass the full dataset
rowKey="id"
pagination={{
pageSize: 10,
showTotal: true,
pageSizeOptions: [10, 25, 50],
}}
/>Custom colors
<Table
columns={columns}
data={data}
rowKey="id"
hoverable
headerBg="#1e1b4b"
headerColor="#c7d2fe"
hoverBg="rgba(99, 102, 241, 0.08)"
/>All Table props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| columns | TableColumn<T>[] | — | Required. Column definitions |
| data | T[] | — | Required. Row data array |
| rowKey | string \| (row) => string \| number | — | Unique key per row |
| loading | boolean | false | Show skeleton rows |
| loadingRows | number | 5 | Skeleton row count |
| bordered | boolean | false | Border every cell |
| striped | boolean | false | Alternate even-row background |
| hoverable | boolean | false | Highlight rows on hover |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Cell padding preset |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius of the wrapper |
| scrollY | string \| number | — | Max body height — enables vertical scroll |
| fullWidth | boolean | true | Stretch table to fill container |
| stickyHeader | boolean | false | Pin header row (auto-on with scrollY) |
| headerBg | string | — | Custom header background color |
| headerColor | string | — | Custom header text color |
| bodyBg | string | — | Custom table body background |
| hoverBg | string | — | Custom row hover background |
| empty | ReactNode | 'Không có dữ liệu' | Empty-state content |
| caption | string | — | Caption below the table |
| pagination | TablePaginationConfig | — | Enable pagination bar |
| onSort | (key, dir) => void | — | Sort callback (server-side sort) |
| className | string | — | Extra class on the wrapper |
TableColumn shape
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| key | string | — | Required. Unique column id |
| title | ReactNode | — | Required. Header content |
| dataIndex | string | — | Row field to read |
| render | (value, row, index) => ReactNode | — | Custom cell renderer |
| width | string \| number | — | Column width (required for fixed columns) |
| align | 'left' \| 'center' \| 'right' | 'left' | Cell text alignment |
| sortable | boolean | false | Enable click-to-sort |
| fixed | 'left' \| 'right' | — | Pin column — requires width |
TablePaginationConfig shape
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| pageSize | number | 10 | Rows per page |
| total | number | — | Override total for server-side pagination |
| showTotal | boolean | true | Show "X–Y / Z" counter |
| pageSizeOptions | number[] | — | Render a page-size selector |
| onChange | (page, pageSize) => void | — | Called on page or size change |
Breadcrumb
import { Breadcrumb } from 'dosieungon-ui';Basic
<Breadcrumb
items={[
{ label: 'Home', href: '/' },
{ label: 'Settings', href: '/settings' },
{ label: 'Profile' },
]}
/>With onClick handler
<Breadcrumb
items={[
{ label: 'Home', onClick: () => router.push('/') },
{ label: 'Products', onClick: () => router.push('/products') },
{ label: 'Detail' },
]}
/>Custom separator
<Breadcrumb
items={items}
separator={<ChevronRightIcon />}
/>Sizes
<Breadcrumb items={items} size="sm" />
<Breadcrumb items={items} size="md" /> {/* default */}
<Breadcrumb items={items} size="lg" />All Breadcrumb props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| items | BreadcrumbItem[] | — | Required. Breadcrumb items — last item is the current page |
| separator | ReactNode | '/' | Separator between items |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| className | string | — | Extra CSS class |
| ... | HTMLAttributes<HTMLElement> | — | All native <nav> props |
BreadcrumbItem shape
interface BreadcrumbItem {
label: ReactNode; // display content
href?: string; // renders as <a> when provided
onClick?: (e) => void; // renders as <button> when provided (no href)
}InputNumber
Numeric input with formatting, min/max clamping, prefix/suffix, and optional spinner controls.
import { InputNumber } from 'dosieungon-ui';Basic
<InputNumber placeholder="Enter amount" />With min / max / step
<InputNumber
min={0}
max={100}
step={5}
defaultValue={50}
onChange={(value) => console.log(value)}
/>Currency formatting
<InputNumber
currency
prefix="$"
precision={2}
placeholder="0.00"
/>With spinner controls
<InputNumber
controls
min={1}
max={99}
defaultValue={1}
/>With label and error
<InputNumber
label="Quantity"
required
min={1}
error={qty < 1 ? 'Must be at least 1' : undefined}
/>All InputNumber props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | number | — | Controlled value |
| defaultValue | number | — | Uncontrolled initial value |
| onChange | (value: number \| null, text: string) => void | — | Called on every keystroke |
| min | number | — | Minimum value (enforced on blur and spinner) |
| max | number | — | Maximum value (enforced on blur and spinner) |
| step | number | 1 | Spinner increment |
| precision | number | — | Max decimal places |
| currency | boolean | false | Show thousand separators while typing |
| thousandSeparator | string | ',' | Thousand separator character |
| decimalSeparator | string | '.' | Decimal separator character |
| prefix | string | — | Text shown before the value (e.g. '$') |
| suffix | string | — | Text shown after the value (e.g. 'VND') |
| controls | boolean | false | Show ▲ ▼ spinner buttons |
| label | string | — | Label above input |
| required | boolean | false | Adds * to label |
| error | string | — | Error message |
| hint | string | — | Helper text |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius |
| wrapperClassName | string | — | Class on the input wrapper |
| className | string | — | Class on the root element |
| ... | InputHTMLAttributes | — | All native <input> props (except type, size, value, onChange) |
Segmented
Tab-style control for selecting one option from a set.
import { Segmented } from 'dosieungon-ui';Basic (uncontrolled)
<Segmented
options={['Daily', 'Weekly', 'Monthly']}
onChange={(value) => console.log(value)}
/>Controlled
const [view, setView] = useState('list');
<Segmented
options={['list', 'grid', 'table']}
value={view}
onChange={(v) => setView(v as string)}
/>With icons
<Segmented
options={[
{ value: 'list', label: 'List', icon: <ListIcon /> },
{ value: 'grid', label: 'Grid', icon: <GridIcon /> },
]}
defaultValue="list"
/>Block (full-width)
<Segmented
options={['A', 'B', 'C']}
block
/>Sizes
<Segmented options={['A', 'B']} size="sm" />
<Segmented options={['A', 'B']} size="md" /> {/* default */}
<Segmented options={['A', 'B']} size="lg" />All Segmented props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| options | (string \| number \| SegmentedOption)[] | — | Required. Option list |
| value | string \| number | — | Controlled selected value |
| defaultValue | string \| number | — | Uncontrolled initial value (defaults to first option) |
| onChange | (value) => void | — | Called when active segment changes |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| disabled | boolean | false | Disable all segments |
| block | boolean | false | Stretch to fill container |
| activeBold | boolean | false | Bold the active segment label |
| background | string | — | Custom track background color |
| activeBackground | string | — | Custom active indicator color |
| textColor | string | — | Custom inactive text color |
| activeTextColor | string | — | Custom active text color |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius |
| className | string | — | Extra CSS class |
SegmentedOption shape
interface SegmentedOption {
value: string | number;
label: ReactNode;
disabled?: boolean;
icon?: ReactNode;
}Tooltip
Lightweight floating label triggered on hover, click, or focus.
import { Tooltip } from 'dosieungon-ui';Basic
<Tooltip title="This is a tooltip">
<Button>Hover me</Button>
</Tooltip>Placement
<Tooltip title="Top left" placement="topLeft">
<Button>TL</Button>
</Tooltip>Available placements: top, topLeft, topRight, bottom, bottomLeft, bottomRight, left, leftTop, leftBottom, right, rightTop, rightBottom
Click trigger
<Tooltip title="Click tooltip" trigger="click">
<Button>Click me</Button>
</Tooltip>Custom color
<Tooltip title="Custom color" color="#7c3aed">
<Button>Purple</Button>
</Tooltip>Disable
<Tooltip title="Disabled tooltip" disabled>
<Button>No tooltip</Button>
</Tooltip>All Tooltip props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | ReactNode | — | Required. Tooltip content — null/undefined disables it |
| placement | TooltipPlacement | 'top' | Position relative to the trigger |
| trigger | 'hover' \| 'click' \| 'focus' \| TooltipTrigger[] | 'hover' | How to activate |
| open | boolean | — | Controlled open state |
| defaultOpen | boolean | false | Uncontrolled initial state |
| onOpenChange | (open) => void | — | Called when visibility changes |
| color | string | — | Custom background + arrow color |
| arrow | boolean | true | Show the directional arrow |
| mouseEnterDelay | number | 100 | Delay before showing (ms) |
| mouseLeaveDelay | number | 100 | Delay before hiding (ms) |
| disabled | boolean | false | Disable the tooltip |
| overlayClassName | string | — | Extra class on the overlay |
Popover
Rich floating card with a title, body content, and optional close button.
import { Popover } from 'dosieungon-ui';Basic
<Popover
title="Information"
content={<p>Popover body content goes here.</p>}
>
<Button>Click me</Button>
</Popover>Hover trigger
<Popover
content="Quick info on hover"
trigger="hover"
>
<span>Hover me</span>
</Popover>With close button
<Popover
title="Settings"
content={<SettingsForm />}
showCloseButton
>
<Button>Open</Button>
</Popover>Placement
<Popover content="Bottom right" placement="bottomRight">
<Button>BR</Button>
</Popover>All Popover props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| content | ReactNode | — | Required. Body content |
| title | ReactNode | — | Header title |
| placement | PopoverPlacement | 'top' | Position relative to trigger |
| trigger | 'hover' \| 'click' \| 'focus' \| PopoverTrigger[] | 'click' | How to activate |
| open | boolean | — | Controlled open state |
| defaultOpen | boolean | false | Uncontrolled initial state |
| onOpenChange | (open) => void | — | Called when visibility changes |
| arrow | boolean | true | Show the directional arrow |
| showCloseButton | boolean | false | Show × button in header |
| mouseEnterDelay | number | 100 | Delay before showing on hover (ms) |
| mouseLeaveDelay | number | 100 | Delay before hiding on hover (ms) |
| disabled | boolean | false | Disable the popover |
| overlayClassName | string | — | Extra class on the overlay |
Popconfirm
Inline confirmation bubble — asks for confirmation before a destructive action.
import { Popconfirm, Button } from 'dosieungon-ui';Basic
<Popconfirm
title="Delete this item?"
onConfirm={() => handleDelete()}
onCancel={() => console.log('cancelled')}
>
<Button variant="danger">Delete</Button>
</Popconfirm>With description
<Popconfirm
title="Are you sure?"
description="This action cannot be undone."
onConfirm={handleDelete}
>
<Button variant="danger">Delete</Button>
</Popconfirm>Async confirm (shows loading)
<Popconfirm
title="Submit changes?"
onConfirm={async () => {
await saveToServer();
}}
>
<Button>Save</Button>
</Popconfirm>Custom button labels
<Popconfirm
title="Remove member?"
okText="Yes, remove"
cancelText="Keep"
okType="danger"
onConfirm={handleRemove}
>
<Button variant="outline" color="danger">Remove</Button>
</Popconfirm>All Popconfirm props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | ReactNode | — | Required. Confirmation question |
| description | ReactNode | — | Secondary text below the title |
| icon | ReactNode | warning icon | Icon next to the title |
| placement | PopconfirmPlacement | 'top' | Position relative to trigger |
| open | boolean | — | Controlled open state |
| defaultOpen | boolean | false | Uncontrolled initial state |
| onOpenChange | (open) => void | — | Called when visibility changes |
| onConfirm | (e) => void \| Promise<void> | — | Called on OK — return a Promise to show loading |
| onCancel | (e) => void | — | Called on Cancel |
| okText | ReactNode | 'OK' | OK button label |
| cancelText | ReactNode | 'Cancel' | Cancel button label |
| okType | ButtonVariant | 'danger' | Variant for the OK button |
| arrow | boolean | true | Show the directional arrow |
| disabled | boolean | false | Prevent the popconfirm from opening |
| overlayClassName | string | — | Extra class on the overlay |
Notification
Toast notifications rendered in a portal. Use the useNotification hook to get an API + a context holder element.
import { useNotification } from 'dosieungon-ui';Setup
Render contextHolder once near the top of your component tree. It's the portal anchor for all notifications.
function App() {
const [api, contextHolder] = useNotification();
return (
<>
{contextHolder}
<Button onClick={() => api.success({ message: 'Saved!' })}>
Save
</Button>
</>
);
}Types
api.success({ message: 'Operation successful', description: 'Your changes have been saved.' });
api.error({ message: 'Something went wrong', description: 'Please try again.' });
api.warning({ message: 'Warning', description: 'Storage is almost full.' });
api.info({ message: 'Info', description: 'A new version is available.' });Placement
api.info({
message: 'Bottom right',
placement: 'bottomRight', // default: 'topRight'
});Available placements: topLeft, topCenter, topRight, bottomLeft, bottomCenter, bottomRight
Persistent notification (no auto-dismiss)
api.warning({ message: 'Action required', duration: 0 });Update an existing notification
api.info({ key: 'upload', message: 'Uploading…' });
// later:
api.success({ key: 'upload', message: 'Upload complete!' });Dismiss programmatically
api.destroy('upload'); // by key
api.destroy(); // dismiss allAction button
api.error({
message: 'Connection lost',
btn: <Button size="sm" onClick={retry}>Retry</Button>,
duration: 0,
});Global config via hook options
const [api, contextHolder] = useNotification({
placement: 'bottomRight',
duration: 3000,
maxCount: 5,
});useNotification config
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| placement | NotificationPlacement | 'topRight' | Default placement for all notifications |
| duration | number | 4500 | Default auto-dismiss delay in ms |
| maxCount | number | — | Max simultaneous notifications |
NotificationConfig (per-notification)
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| key | string | auto | Unique key — reuse to update an existing notification |
| type | 'success' \| 'error' \| 'warning' \| 'info' | 'info' | Notification type |
| message | ReactNode | — | Bold title text |
| description | ReactNode | — | Secondary body text |
| duration | number | 4500 | Auto-dismiss delay — 0 = no auto-dismiss |
| closable | boolean | true | Show × close button |
| onClose | () => void | — | Called when dismissed |
| icon | ReactNode | — | Replace the default type icon |
| btn | ReactNode | — | Action button below the description |
| placement | NotificationPlacement | hook default | Override placement for this notification |
DatePicker
Calendar picker for selecting a date.
import { DatePicker } from 'dosieungon-ui';Basic
const [date, setDate] = useState<Date | null>(null);
<DatePicker value={date} onChange={setDate} />With label and error
<DatePicker
label="Date of birth"
required
value={dob}
onChange={setDob}
error={!dob ? 'Required' : undefined}
/>Custom format
<DatePicker
value={date}
onChange={setDate}
format="DD/MM/YYYY"
/>Clearable with min/max
<DatePicker
value={date}
onChange={setDate}
clearable
minDate={new Date()}
/>Disable specific dates
<DatePicker
value={date}
onChange={setDate}
disabledDate={(d) => d.getDay() === 0 || d.getDay() === 6} // disable weekends
/>Locale
// Auto-detect from <html lang="vi"> — no prop needed
// Or pass explicitly:
<DatePicker value={date} onChange={setDate} locale="vi" />Built-in locales: en, vi. Pass a custom DatePickerLocale object for full control.
All DatePicker props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | Date \| null | — | Controlled date value |
| defaultValue | Date \| null | — | Uncontrolled initial value |
| onChange | (date: Date \| null) => void | — | Called on date select or clear |
| placeholder | string | 'Select date' | Placeholder text |
| disabled | boolean | false | Disable the picker |
| clearable | boolean | false | Show clear button when a date is selected |
| format | string | 'MM/DD/YYYY' | Display format (YYYY, MM, DD tokens) |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| minDate | Date | — | Earliest selectable date |
| maxDate | Date | — | Latest selectable date |
| disabledDate | (date: Date) => boolean | — | Return true to disable a specific date |
| label | string | — | Label above the trigger |
| required | boolean | false | Adds * to label |
| error | string | — | Error message |
| hint | string | — | Helper text |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius |
| showIcon | boolean | true | Show the calendar icon |
| icon | ReactNode | — | Replace the default calendar icon |
| locale | string \| DatePickerLocale | auto | Panel locale (BCP-47 tag or custom object) |
| className | string | — | Extra CSS class |
TimePicker
Scroll-column picker for selecting a time value.
import { TimePicker } from 'dosieungon-ui';Basic
const [time, setTime] = useState<string | null>(null);
<TimePicker value={time} onChange={setTime} />With seconds
<TimePicker
value={time}
onChange={setTime}
timeMode="HH:mm:ss"
/>Hour only
<TimePicker
value={hour}
onChange={setHour}
timeMode="HH"
placeholder="Select hour"
/>With label and clearable
<TimePicker
label="Meeting time"
required
clearable
value={time}
onChange={setTime}
/>All TimePicker props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string \| null | — | Controlled value (format matches timeMode) |
| defaultValue | string \| null | — | Uncontrolled initial value |
| onChange | (time: string \| null) => void | — | Called on confirm or clear |
| timeMode | 'HH' \| 'HH:mm' \| 'HH:mm:ss' | 'HH:mm' | Which time units to show |
| placeholder | string | 'Select time' | Placeholder text |
| disabled | boolean | false | Disable the picker |
| clearable | boolean | false | Show clear button |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| label | string | — | Label above the trigger |
| required | boolean | false | Adds * to label |
| error | string | — | Error message |
| hint | string | — | Helper text |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius |
| showIcon | boolean | true | Show the clock icon |
| icon | ReactNode | — | Replace the default clock icon |
| locale | string \| TimePickerLocale | auto | Panel locale (en/vi or custom object) |
| className | string | — | Extra CSS class |
DateTimePicker
Combined date + time picker in a single panel.
import { DateTimePicker } from 'dosieungon-ui';Basic
const [dt, setDt] = useState<Date | null>(null);
<DateTimePicker value={dt} onChange={setDt} />With seconds
<DateTimePicker
value={dt}
onChange={setDt}
timeMode="HH:mm:ss"
/>With label, clearable, and min date
<DateTimePicker
label="Scheduled at"
required
clearable
value={dt}
onChange={setDt}
minDate={new Date()}
/>Custom format
<DateTimePicker
value={dt}
onChange={setDt}
format="DD/MM/YYYY HH:mm"
/>All DateTimePicker props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | Date \| null | — | Controlled value |
| defaultValue | Date \| null | — | Uncontrolled initial value |
| onChange | (date: Date \| null) => void | — | Called on confirm or clear |
| timeMode | 'HH' \| 'HH:mm' \| 'HH:mm:ss' | 'HH:mm' | Which time units to show |
| placeholder | string | 'Select date & time' | Placeholder text |
| disabled | boolean | false | Disable the picker |
| clearable | boolean | false | Show clear button |
| format | string | auto | Display format (auto-set from timeMode) |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| minDate | Date | — | Earliest selectable date |
| maxDate | Date | — | Latest selectable date |
| disabledDate | (date: Date) => boolean | — | Return true to disable a date |
| label | string | — | Label above the trigger |
| required | boolean | false | Adds * to label |
| error | string | — | Error message |
| hint | string | — | Helper text |
| rounded | 'none' \| 'sm' \| 'md' \| 'lg' \| 'full' | — | Override corner radius |
| showIcon | boolean | true | Show the icon |
| icon | ReactNode | — | Replace the default icon |
| locale | string \| DateTimePickerLocale | auto | Panel locale |
| className | string | — | Extra CSS class |
ColorPicker
HSV canvas color picker with solid and gradient modes.
import { ColorPicker } from 'dosieungon-ui';Solid color
const [color, setColor] = useState('#0285F7');
<ColorPicker value={color} onChange={setColor} />Gradient mode
const [gradient, setGradient] = useState('linear-gradient(90deg, #0285F7 0%, #8B5CF6 100%)');
<ColorPicker
value={gradient}
onChange={setGradient}
mode="gradient"
/>Both modes (tab switcher)
<ColorPicker
value={value}
onChange={setValue}
mode="both"
/>Compact trigger (swatch only)
<ColorPicker
value={color}
onChange={setColor}
showColorCode={false}
showArrow={false}
/>Custom trigger
<ColorPicker
value={color}
onChange={setColor}
trigger={({ value }) => (
<div style={{ width: 32, height: 32, background: value, borderRadius: 4 }} />
)}
/>All ColorPicker props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | — | Controlled value — hex (#RRGGBB) or linear-gradient(…) |
| defaultValue | string | — | Uncontrolled initial value |
| onChange | (value: string) => void | — | Called on every color change |
| mode | 'solid' \| 'gradient' \| 'both' | 'both' | Which mode(s) to show |
| disabled | boolean | false | Disable the picker |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Trigger button size |
| presets | string[] | 12 default swatches | Quick-access color presets |
| showInput | boolean | true | Show hex input field |
| showPresets | boolean | true | Show preset swatches |
| showArrow | boolean | true | Show chevron on trigger |
| showColorCode | boolean | true | Show hex code on trigger |
| triggerRadius | string \| number | — | Custom border-radius for the trigger |
| trigger | ReactNode \| (state) => ReactNode | — | Replace the default trigger |
| placeholder | string | 'Pick a color' | Trigger placeholder when no value |
| className | string | — | Extra CSS class |
Skeleton
Loading placeholder that mimics content layout before data arrives.
import { Skeleton } from 'dosieungon-ui';Basic (wraps real content)
<Skeleton loading={isLoading}>
<ArticleContent />
</Skeleton>With avatar
<Skeleton loading={isLoading} active avatar>
<UserProfile />
</Skeleton>Custom paragraph rows
<Skeleton
loading={isLoading}
active
paragraph={{ rows: 5 }}
title={{ width: '40%' }}
>
<Content />
</Skeleton>Standalone sub-components
{/* Button skeleton */}
<Skeleton.Button active />
<Skeleton.Button active size="lg" block />
{/* Avatar skeleton */}
<Skeleton.Avatar active />
<Skeleton.Avatar active size="lg" shape="square" />
{/* Input skeleton */}
<Skeleton.Input active />
<Skeleton.Input active block />
{/* Image skeleton */}
<Skeleton.Image active />Skeleton props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| loading | boolean | true | When false, renders children instead |
| active | boolean | false | Enable the shimmer animation |
| avatar | boolean \| SkeletonAvatarConfig | false | Show an avatar placeholder |
| title | boolean \| SkeletonTitleConfig | true | Show a title line |
| paragraph | boolean \| SkeletonParagraphConfig | true | Show paragraph lines |
| round | boolean | false | Pill-shaped corners on all elements |
| className | string | — | Extra CSS class |
SkeletonAvatarConfig
| Prop | Type | Default |
|------|------|---------|
| size | 'sm' \| 'md' \| 'lg' | 'md' |
| shape | 'circle' \| 'square' | 'circle' |
SkeletonTitleConfig
| Prop | Type | Description |
|------|------|-------------|
| width | string \| number | Width of the title bar |
SkeletonParagraphConfig
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| rows | number | 3 | Number of lines |
| width | string \| number \| Array<string\|number> | — | Width(s) of the lines |
Skeleton.Button props
| Prop | Type | Default |
|------|------|---------|
| size | 'sm' \| 'md' \| 'lg' | 'md' |
| active | boolean | false |
| round | boolean | false |
| block | boolean | false |
Skeleton.Avatar props
| Prop | Type | Default |
|------|------|---------|
| size | 'sm' \| 'md' \| 'lg' | 'md' |
| shape | 'circle' \| 'square' | 'circle' |
| active | boolean | false |
Skeleton.Input props
| Prop | Type | Default |
|------|------|---------|
| size | 'sm' \| 'md' \| 'lg' | 'md' |
| active | boolean | false |
| block | boolean | false |
Customization
Pass className for one-off overrides
Every component accepts className. Your classes are merged with library classes and applied last so they take precedence.
<Button className="w-full">Full Width</Button>
<Card className="border-2 border-blue-500">Custom border</Card>Override design tokens globally
All colors, sizes, and spacing are CSS custom properties. Override them once in your global CSS:
/* globals.css */
:root {
--dsg-primary: #7c3aed; /* purple instead of blue */
--dsg-primary-hover: #6d28d9;
--dsg-primary-active: #5b21b6;
--dsg-primary-subtle: rgba(124, 58, 237, 0.1);
--dsg-radius: 4px; /* sharper corners */
}Override for a specific section
<div style={{ '--dsg-primary': '#7c3aed' } as React.CSSProperties}>
<Button>Purple Button</Button>
</div>DSNProvider & Dark Mode
Wrap your app once with DSNProvider — all library components inside automatically respond to the active theme.
import { DSNProvider } from 'dosieungon-ui';
export default function App({ Component, pageProps }) {
return (
<DSNProvider>
<Component {...pageProps} />
</DSNProvider>
);
}What it does by default:
- Reads
localStoragefor a previously saved preference - Falls back to
prefers-color-scheme(system) - Persists the user's choice back to
localStorage - Sets
data-dsg-themeon<html>so portals (Modal, Drawer) also get the theme - Toggles
class="dark"/"light"on<html>(Tailwinddark:utilities work out of the box)
Toggle theme
import { useTheme } from 'dosieungon-ui';
function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}>
{resolvedTheme === 'dark' ? '☀️ Light' : '🌙 Dark'}
</button>
);
}DSNProvider props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| defaultTheme | 'light' \| 'dark' \| 'system' | 'system' | Initial theme when no localStorage value exists |
| theme | 'light' \| 'dark' \| 'system' | — | Controlled theme — makes Provider controlled |
| storageKey | string | 'dsg-theme' | localStorage key to persist the choice |
| syncDocument | boolean | true | Mirror theme onto <html> for portals and Tailwind |
useTheme return value
| Field | Type | Description |
|-------|------|-------------|
| theme | 'light' \| 'dark' \| 'system' | Active setting (may be 'system') |
| resolvedTheme | 'light' \| 'dark' | Actual rendered theme — always resolved |
| setTheme | (theme) => void | Update the theme |
Force a specific theme
// Always dark, no persistence
<DSNProvider theme="dark">
<Card>Always dark card</Card>
</DSNProvider>
// Scoped to a section (no document sync)
<DSNProvider theme="dark" syncDocument={false}>
<Card>Dark section only</Card>
</DSNProvider>Next.js Setup
No extra setup required. Just import and use.
// app/page.tsx or pages/index.tsx
import { Button } from 'dosieungon-ui';
export default function Page() {
return <Button>Hello</Button>;
}For interactive components (Modal, Select, Drawer, etc.) in App Router, add 'use client':
'use client';
import { useState } from 'react';
import { Modal, Button } from 'dosieungon-ui';
export default function Page() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Open</Button>
<Modal open={open} onClose={() => setOpen(false)} title="Hello">
Content
</Modal>
</>
);
}