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

dosieungon-ui

v0.1.6

Published

A lightweight, beautiful React UI component library

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


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 |


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 localStorage for a previously saved preference
  • Falls back to prefers-color-scheme (system)
  • Persists the user's choice back to localStorage
  • Sets data-dsg-theme on <html> so portals (Modal, Drawer) also get the theme
  • Toggles class="dark"/"light" on <html> (Tailwind dark: 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>
    </>
  );
}