@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
- Installation
- Quick Start
- Theming with Paradigms
- Customizing the Design Tokens
- Shared Conventions
- Component Reference
- TypeScript
- License
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,
sideEffectslimited 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/uiReact 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 withuseState. raised— most components accept araisedboolean that adds an elevated shadow (great with theskeuo/neoparadigms).size— sizing scales are component-specific but predictable: usually"sm" | "md" | "lg", with buttons/spinners/avatars also offeringxs/xl.variant/color— visual style and semantic intent. Variant types are exported (e.g.ButtonVariant).className— every component forwards aclassNamefor one-off overrides; it is merged with the internal classes.- Native props pass through where it makes sense —
Button,Input, andTextareaextend the native element props, soonClick,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 toastFor 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
