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

@cuneytyildirim/ui

v0.2.1

Published

A modern React component library built with **React 19**, **TypeScript**, and **Tailwind CSS v4**. It ships ~28 accessible, fully-typed components with a unique multi-paradigm theming system, GSAP-powered micro-interactions, and zero runtime CSS-in-JS.

Readme

@cuneytyildirim/ui

A modern React component library built with React 19, TypeScript, and Tailwind CSS v4. It ships ~28 accessible, fully-typed components with a unique multi-paradigm theming system, GSAP-powered micro-interactions, and zero runtime CSS-in-JS.

Table of Contents

Features

  • 🎨 Multi-paradigm theming — switch the entire UI between Flat, Skeuomorphic, Neumorphic, and Glassmorphic styles at runtime via a single provider.
  • 🧩 ~28 components — from buttons and inputs to data tables, date-range pickers, modals, drawers, and toasts.
  • 🔒 Fully typed — every component and its variant types are exported for first-class TypeScript autocompletion.
  • Motion built in — subtle GSAP animations (magnetic buttons, tilt cards) with no extra setup.
  • 🪶 Tree-shakeable — ESM + CJS builds, sideEffects limited to the stylesheet.
  • 🎯 Tailwind v4 native — themed entirely through CSS custom properties, so it adapts to your design tokens.

Installation

npm install @cuneytyildirim/ui
# or
pnpm add @cuneytyildirim/ui
# or
yarn add @cuneytyildirim/ui

React 18+ and React DOM 18+ are required as peer dependencies.

Quick Start

Import the stylesheet once at the root of your app, then use components anywhere.

// app entry (e.g. main.tsx / layout.tsx)
import "@cuneytyildirim/ui/style.css";
import { Button, Input, Card } from "@cuneytyildirim/ui";

export default function Example() {
  return (
    <Card title="Sign in">
      <Input label="Email" placeholder="[email protected]" />
      <Button
        name="Submit"
        variant="primary"
        size="md"
        onClick={() => console.log("clicked")}
      />
    </Card>
  );
}

Theming with Paradigms

The standout feature is the Paradigm system. Wrap your app (or any subtree) in a ParadigmProvider to choose a visual language, and let users switch between them with the built-in ParadigmSwitcher.

import {
  ParadigmProvider,
  ParadigmSwitcher,
  useParadigm,
  Button,
} from "@cuneytyildirim/ui";
import "@cuneytyildirim/ui/style.css";

export default function App() {
  return (
    <ParadigmProvider defaultParadigm="flat">
      <ParadigmSwitcher />
      <Button name="Themed Button" variant="primary" />
    </ParadigmProvider>
  );
}

Available paradigms:

| Paradigm | Value | Feel | | -------- | ------- | ----------------------------- | | Flat | flat | Clean · Minimal · Solid | | Skeuo | skeuo | Layered · Physical · Textured | | Neo | neo | Soft · Extruded · Tactile | | Glass | glass | Frosted · Blurred · Luminous |

ParadigmProvider renders a wrapper <div data-paradigm="...">, so all theming cascades from there. Read or change the active paradigm from any descendant with the useParadigm hook:

import { useParadigm } from "@cuneytyildirim/ui";

function MyToolbar() {
  const { paradigm, setParadigm } = useParadigm();
  return (
    <button onClick={() => setParadigm(paradigm === "flat" ? "glass" : "flat")}>
      Current: {paradigm}
    </button>
  );
}

ParadigmProvider props

| Prop | Type | Default | Description | | ----------------- | --------------------------------------- | -------- | ----------------------------------- | | defaultParadigm | "flat" \| "skeuo" \| "neo" \| "glass" | "flat" | Paradigm active on first render. | | className | string | "" | Extra classes on the wrapper div. | | children | ReactNode | — | Your app. |

Customizing the Design Tokens

Every color, surface and border is a CSS custom property defined on :root. Override any of them in your own global stylesheet (loaded after style.css) to rebrand the whole library — no component overrides needed.

/* your-globals.css */
:root {
  --primary: #6366f1; /* brand color */
  --primary-foreground: #ffffff;
  --secondary: #ec4899;
  --surface: #ffffff; /* card/input backgrounds */
  --surface-muted: #f5f5f5;
  --border: #e5e7eb;
  --border-strong: #d1d5db;
  --text: #334155;
  --text-heading: #1e293b;
  --text-muted: #94a3b8;
  --success: #22c55e;
  --danger: #ef4444;
  --warning: #f59e0b;
  --info: #3b82f6;
}

Because the library is built on Tailwind v4, these tokens are also exposed as utility colors (bg-primary, text-text-muted, border-border-strong, etc.), which you can use in your own markup to stay consistent with the components.

Shared Conventions

A few patterns are consistent across the library — learn them once and they apply everywhere:

  • Controlled by default. Inputs take a value + change handler (e.g. checked/onChange, value/onChange). Manage state yourself with useState.
  • raised — most components accept a raised boolean that adds an elevated shadow (great with the skeuo/neo paradigms).
  • size — sizing scales are component-specific but predictable: usually "sm" | "md" | "lg", with buttons/spinners/avatars also offering xs/xl.
  • variant / color — visual style and semantic intent. Variant types are exported (e.g. ButtonVariant).
  • className — every component forwards a className for one-off overrides; it is merged with the internal classes.
  • Native props pass through where it makes sense — Button, Input, and Textarea extend the native element props, so onClick, name, maxLength, aria-*, etc. work as expected.

Component Reference

Inputs & Forms

Input

Text field with label, hint, error state, icons, and five visual variants. Forwards a ref and all native <input> props.

import { Input } from "@cuneytyildirim/ui";

<Input
  label="Email"
  hint="We'll never share it."
  placeholder="[email protected]"
  variant="default"
  size="md"
  leftIcon={<MailIcon />}
  error={errors.email}
/>;

| Prop | Type | Default | | ---------------------- | -------------------------------------------------------------- | ----------- | | label | string | — | | hint | string | — | | error | string (renders red border + message) | — | | variant | "default" \| "filled" \| "ghost" \| "primary" \| "secondary" | "default" | | size | "sm" \| "md" \| "lg" | "md" | | leftIcon/rightIcon | ReactNode | — | | raised | boolean | false | | …native input props | value, onChange, disabled, placeholder, etc. | — |

Textarea

Multi-line input with optional character counter.

<Textarea
  label="Bio"
  rows={4}
  showCount
  maxLength={280}
  variant="filled"
  value={bio}
  onChange={(e) => setBio(e.target.value)}
/>

| Prop | Type | Default | | -------------------------- | ----------------------------------------------- | ----------- | | label / hint / error | string | — | | rows | number | — | | showCount | boolean (shows current / maxLength) | false | | variant | "default" \| "filled" \| "ghost" \| "primary" | "default" | | size | "sm" \| "md" \| "lg" | "md" | | raised | boolean | false | | containerClassName | string | — |

Select

Single-select dropdown with optional search.

const [value, setValue] = useState("");

<Select
  label="Country"
  placeholder="Choose one"
  searchable
  value={value}
  onChange={setValue}
  options={[
    { value: "tr", label: "Türkiye" },
    { value: "de", label: "Germany" },
    { value: "us", label: "United States", disabled: true },
  ]}
/>;

| Prop | Type | Default | | ------------------------------------------ | ------------------------- | ------- | | options | SelectOption[] | — | | value | string | — | | onChange | (value: string) => void | — | | searchable | boolean | false | | size | "sm" \| "md" \| "lg" | "md" | | label / hint / error / placeholder | string | — | | raised / disabled | boolean | false |

SelectOption = { value: string; label: string; disabled?: boolean }

MultiSelect

Multi-value picker with removable tags and search.

const [selected, setSelected] = useState<string[]>([]);

<MultiSelect
  label="Tags"
  searchable
  value={selected}
  onChange={setSelected}
  options={[
    { value: "react", label: "React" },
    { value: "ts", label: "TypeScript" },
  ]}
/>;

| Prop | Type | Default | | ------------ | ------------------------------ | ------- | | options | MultiSelectOption[] | — | | value | string[] | [] | | onChange | (selected: string[]) => void | — | | searchable | boolean | false | | multiple | boolean | true | | size | "sm" \| "md" \| "lg" | "md" |

Checkbox

const [checked, setChecked] = useState(false);

<Checkbox
  checked={checked}
  onChange={setChecked}
  label="Subscribe to newsletter"
  description="One email per week, no spam."
  color="primary"
  size="md"
/>;

| Prop | Type | Default | | ----------------------- | ------------------------------------------------- | ----------- | | checked | boolean | — | | onChange | (checked: boolean) => void | — | | indeterminate | boolean | false | | label / description | string | — | | color | "primary" \| "success" \| "danger" \| "warning" | "primary" | | size | "sm" \| "md" \| "lg" | "md" |

Radio & RadioGroup

Use RadioGroup for sets; Radio for a single control.

const [plan, setPlan] = useState("pro");

<RadioGroup
  label="Plan"
  value={plan}
  onChange={setPlan}
  direction="column"
  options={[
    { value: "free", label: "Free" },
    { value: "pro", label: "Pro" },
  ]}
/>;

| RadioGroup prop | Type | Default | | ----------------- | ------------------------- | ----------- | | value | string | — | | onChange | (value: string) => void | — | | options | RadioOption[] | — | | direction | "row" \| "column" | "column" | | color | RadioColor | "primary" | | size | "sm" \| "md" \| "lg" | "md" |

Toggle

const [on, setOn] = useState(false);

<Toggle
  checked={on}
  onChange={setOn}
  label="Dark mode"
  labelPosition="right"
  color="green"
  size="md"
/>;

| Prop | Type | Default | | --------------- | -------------------------------------------------- | --------- | | checked | boolean | — | | onChange | (checked: boolean) => void | — | | label | string | — | | labelPosition | "left" \| "right" | "right" | | color | "slate" \| "blue" \| "green" \| "red" \| "amber" | "slate" | | size | "sm" \| "md" \| "lg" | "md" | | raised | boolean | false |

DatePicker

const [date, setDate] = useState<Date | null>(null);

<DatePicker
  label="Start date"
  value={date}
  onChange={setDate}
  minDate={new Date()}
  size="md"
/>;

| Prop | Type | Default | | ----------------------- | ------------------------------ | ------- | | value | Date \| null | — | | onChange | (date: Date \| null) => void | — | | minDate / maxDate | Date | — | | label / placeholder | string | — | | size | "sm" \| "md" \| "lg" | "md" |

DateRangePicker

const [range, setRange] = useState<DateRange>({ start: null, end: null });

<DateRangePicker label="Trip dates" value={range} onChange={setRange} />;

| Prop | Type | Default | | ------------------------------------- | ------------------------------ | ------- | | value | DateRange ({ start, end }) | — | | onChange | (range: DateRange) => void | — | | startPlaceholder / endPlaceholder | string | — | | minDate / maxDate | Date | — |

Actions & Navigation

Button

Extends native <button>. The visible text is the name prop (not children).

<Button
  name="Save changes"
  variant="primary"
  size="lg"
  radius="full"
  leftIcon={<SaveIcon />}
  isLoading={saving}
  magnetic
  fullWidth
  onClick={handleSave}
/>

| Prop | Type | Default | | ------------------------ | ------------------------------------------------------------------------------------- | ----------- | | name | string (button label, required) | — | | variant | "primary" \| "secondary" \| "ghost" \| "outline" \| "danger" \| "success" \| "warn" | "primary" | | size | "xs" \| "sm" \| "md" \| "lg" \| "xl" | "md" | | radius | "sm" \| "md" \| "lg" \| "full" | "md" | | isLoading | boolean (shows spinner, disables) | false | | leftIcon / rightIcon | ReactNode | — | | magnetic | boolean (GSAP cursor-follow effect) | false | | fullWidth | boolean | false | | raised | boolean | false | | error | string | — | | …native button props | onClick, type, disabled, etc. | — |

DropdownMenu

<DropdownMenu
  trigger={<Button name="Options" variant="outline" />}
  placement="bottom-start"
  items={[
    { key: "edit", label: "Edit", icon: <EditIcon />, onClick: onEdit },
    { key: "sep", label: "", separator: true },
    { key: "del", label: "Delete", danger: true, onClick: onDelete },
  ]}
/>

| Prop | Type | Default | | ----------- | ------------------------------------------------------------ | ---------------- | | trigger | ReactNode | — | | items | DropdownMenuItem[] | — | | placement | "bottom-start" \| "bottom-end" \| "top-start" \| "top-end" | "bottom-start" |

DropdownMenuItem = { key; label; icon?; danger?; disabled?; separator?; onClick? }

Pagination

const [page, setPage] = useState(1);

<Pagination
  total={240}
  page={page}
  pageSize={20}
  onChange={setPage}
  showTotal
/>;

| Prop | Type | Default | | -------------- | ------------------------ | ------- | | total | number (item count) | — | | page | number (1-based) | — | | pageSize | number | 10 | | onChange | (page: number) => void | — | | siblingCount | number | 1 | | showTotal | boolean | false |

Breadcrumb

<Breadcrumb
  items={[
    { label: "Home", href: "/" },
    { label: "Projects", href: "/projects" },
    { label: "Current" },
  ]}
/>

BreadcrumbItem = { label: string; href?: string; icon?: ReactNode }. Pass a custom separator node to override the default chevron.

Tabs

<Tabs
  variant="pills"
  defaultActiveKey="overview"
  items={[
    { key: "overview", label: "Overview", children: <Overview /> },
    {
      key: "settings",
      label: "Settings",
      icon: <Gear />,
      children: <Settings />,
    },
  ]}
/>

| Prop | Type | Default | | ------------------------ | ----------------------------------- | ------------- | | items | TabItem[] | — | | defaultActiveKey | string (uncontrolled) | — | | activeKey / onChange | controlled mode | — | | variant | "underline" \| "pills" \| "boxed" | "underline" |

TabItem = { key; label; icon?; children; disabled? }

Stepper

<Stepper
  currentStep={1}
  orientation="horizontal"
  steps={[
    { key: "a", label: "Account", description: "Your details" },
    { key: "b", label: "Payment" },
    { key: "c", label: "Done" },
  ]}
  onStepClick={setStep}
/>

| Prop | Type | Default | | ------------- | ---------------------------- | -------------- | | steps | StepItem[] | — | | currentStep | number (0-based index) | — | | orientation | "horizontal" \| "vertical" | "horizontal" | | onStepClick | (index: number) => void | — |

Data Display

Card

<Card
  title="Revenue"
  subtitle="Last 30 days"
  headerRight={<Badge color="green">+12%</Badge>}
  footer={<Button name="Details" variant="ghost" />}
  variant="elevated"
  padding="lg"
  tilt
>
  <Chart />
</Card>

| Prop | Type | Default | | ------------------------ | -------------------------------- | ------- | | title / subtitle | string | — | | headerRight / footer | ReactNode | — | | variant | CardVariant | — | | padding | "none" \| "sm" \| "md" \| "lg" | "md" | | tilt | boolean (3D hover tilt) | false | | raised | boolean | false |

Table

Generic, typed, with sorting, selection, sticky header, and custom cell rendering.

type User = { id: string; name: string; role: string };

<Table<User>
  data={users}
  rowKey={(row) => row.id}
  selectable
  selectedKeys={selected}
  onSelect={setSelected}
  onSort={(key, dir) => sortBy(key, dir)}
  stickyHeader
  maxHeight="400px"
  columns={[
    { key: "name", title: "Name", sortable: true },
    { key: "role", title: "Role", align: "center" },
    {
      key: "actions",
      title: "",
      render: (_value, row) => <Button name="Edit" size="sm" />,
    },
  ]}
/>;

| Prop | Type | Default | | --------------------------- | --------------------------------------------- | ------- | | columns | TableColumn<T>[] | — | | data | T[] | — | | rowKey | (row: T, index: number) => string | — | | selectable | boolean | false | | selectedKeys / onSelect | controlled selection | — | | onSort | (key: string, dir: "asc" \| "desc") => void | — | | stickyHeader | boolean | false | | maxHeight | string (CSS value, enables scroll) | — | | emptyText | string | — |

TableColumn = { key; title; sortable?; width?; align?; render? }

Accordion

<Accordion
  variant="separated"
  multiple
  defaultOpen={["faq-1"]}
  items={[
    { key: "faq-1", title: "What is this?", content: <p>An answer.</p> },
    { key: "faq-2", title: "Disabled item", content: "…", disabled: true },
  ]}
/>

| Prop | Type | Default | | ------------- | ---------------------------------------- | ----------- | | items | AccordionItem[] | — | | defaultOpen | string[] (open keys) | [] | | multiple | boolean (allow many open at once) | false | | variant | "default" \| "bordered" \| "separated" | "default" |

Avatar

<Avatar src="/me.jpg" name="Ada Lovelace" size="lg" shape="circle" />;
{
  /* falls back to initials when src is missing or fails */
}

| Prop | Type | Default | | --------- | ----------------------------------------------- | ---------- | | src | string | — | | name | string (used for initials + alt) | — | | size | "xs" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl" | "md" | | shape | "circle" \| "square" | "circle" | | onClick | () => void | — |

Badge

<Badge color="purple" variant="subtle" size="sm" animated>
  New
</Badge>

| Prop | Type | Default | | ---------- | -------------------------------------------------------------------------------------- | ---------- | | children | ReactNode | — | | color | "slate" \| "blue" \| "green" \| "red" \| "amber" \| "purple" \| "indigo" \| "orange" | "slate" | | variant | "subtle" \| "filled" \| "outline" | "subtle" | | size | "sm" \| "md" \| "lg" | "md" | | animated | boolean (pulse) | false |

Progress

Bar or ring, determinate or indeterminate.

<Progress value={62} color="success" showLabel striped animated />
<Progress variant="ring" value={75} size="lg" showLabel />
<Progress /> {/* indeterminate when value is undefined */}

| Prop | Type | Default | | ---------------------- | ----------------------------------------------------------- | ----------- | | value | number (0–100; omit for indeterminate) | — | | variant | "bar" \| "ring" | "bar" | | color | "primary" \| "success" \| "danger" \| "warning" \| "info" | "primary" | | size | "xs" \| "sm" \| "md" \| "lg" | "md" | | showLabel | boolean | false | | striped / animated | boolean | false |

Skeleton

<Skeleton variant="line" lines={3} gap="0.5rem" />
<Skeleton variant="circle" width={48} height={48} />
<Skeleton variant="rect" width="100%" height={160} />

| Prop | Type | Default | | ------------------ | ------------------------------ | -------- | | variant | "line" \| "circle" \| "rect" | "line" | | width / height | string \| number | — | | lines | number (for line) | 1 | | gap | string | — | | animated | boolean | true |

Spinner

<Spinner size="md" color="blue" label="Loading…" />

| Prop | Type | Default | | ------- | ------------------------------------------------------------- | --------- | | size | "xs" \| "sm" \| "md" \| "lg" \| "xl" | "md" | | color | "slate" \| "blue" \| "green" \| "red" \| "amber" \| "white" | "slate" | | label | string | — |

Tooltip

<Tooltip content="Copy to clipboard" placement="top" delay={200}>
  <Button name="Copy" variant="ghost" />
</Tooltip>

| Prop | Type | Default | | ----------- | ---------------------------------------- | ------- | | content | string | — | | children | ReactNode (the trigger) | — | | placement | "top" \| "bottom" \| "left" \| "right" | "top" | | delay | number (ms) | — |

Feedback & Overlays

Alert

<Alert
  type="warning"
  variant="subtle"
  title="Heads up"
  description="Your trial ends in 3 days."
  dismissible
  onDismiss={() => {}}
  action={{ label: "Upgrade", onClick: upgrade }}
/>

| Prop | Type | Default | | ----------------------- | --------------------------------------------- | ---------- | | type | "success" \| "error" \| "warning" \| "info" | "info" | | variant | "subtle" \| "filled" \| "outline" | "subtle" | | title / description | string | — | | dismissible | boolean + onDismiss | false | | action | { label: string; onClick: () => void } | — |

Modal

const [open, setOpen] = useState(false);

<Modal
  isOpen={open}
  onClose={() => setOpen(false)}
  title="Confirm"
  size="md"
  footer={
    <>
      <Button name="Cancel" variant="ghost" onClick={() => setOpen(false)} />
      <Button name="Delete" variant="danger" onClick={confirm} />
    </>
  }
>
  Are you sure you want to delete this?
</Modal>;

| Prop | Type | Default | | ----------------- | ---------------------------------------- | ------- | | isOpen | boolean | — | | onClose | () => void | — | | title | string | — | | footer | ReactNode | — | | size | "sm" \| "md" \| "lg" \| "xl" \| "full" | "md" | | closeOnBackdrop | boolean | true | | showCloseButton | boolean | true |

Drawer

<Drawer open={open} onClose={close} side="right" size="md" title="Filters">
  <FiltersForm />
</Drawer>

| Prop | Type | Default | | ------------------ | ---------------------------------------- | --------- | | open | boolean | — | | onClose | () => void | — | | side | "left" \| "right" \| "top" \| "bottom" | "right" | | size | "sm" \| "md" \| "lg" \| "full" | "md" | | title / footer | string / ReactNode | — | | showOverlay | boolean | true |

Toast

Toasts use an imperative API. Mount ToastContainer once near the root, then call toast from anywhere — including outside React components.

import { ToastContainer, toast } from "@cuneytyildirim/ui";

function Root() {
  return (
    <>
      <ToastContainer />
      <Button name="Save" onClick={() => toast.success("Saved!")} />
    </>
  );
}

// available methods (each returns the toast id)
toast.success("Done");
toast.error("Something went wrong");
toast.warning("Heads up");
toast.info("FYI", 6000); // optional duration in ms (default 4000)
toast.dismiss(id); // dismiss a specific toast

For advanced cases you can subscribe to the toast stream directly with onToastAdd / onToastRemove (both return an unsubscribe function).

TypeScript

The package ships full type declarations (dist/index.d.ts). Every component's variant/prop helper types are exported, so you can build typed wrappers:

import type {
  ButtonVariant,
  BadgeColor,
  SelectOption,
  TableColumn,
  ToastType,
} from "@cuneytyildirim/ui";

No additional @types packages are needed.

License

MIT © Cuneyt Yildirim