@overdoser/react-toolkit
v0.5.0
Published
A modern, themeable React component library with SCSS modules.
Readme
@overdoser/react-toolkit
A modern, themeable React component library with SCSS modules.
Installation
npm install @overdoser/react-toolkitSetup
Import the stylesheet in your app entry point:
import '@overdoser/react-toolkit/theme.css';Theming
Override --crk-* CSS custom properties to customize the theme:
:root {
--crk-color-primary: #3b82f6;
--crk-color-danger: #ef4444;
--crk-font-family: 'Inter', sans-serif;
}For per-element tweaks, every component accepts className and a classes prop
(internal class names are hashed — never target them directly).
theme.css vs theme.layered.css
The package ships two stylesheets with identical content — same rules, same
hashed class names, same --crk-* tokens. The only difference is that
theme.layered.css wraps everything in a CSS cascade layer:
@layer crk {
/* ...the entire stylesheet... */
}That wrapper changes how your overrides compete with the toolkit's styles. Import exactly one of the two (never both).
The problem with plain theme.css. The overrides you pass via className /
classes.* are plain CSS classes, so they tie with the toolkit's own class on
specificity — the winner is decided by stylesheet source order, which your
bundler controls. In many setups the toolkit CSS is injected after your app
CSS, so your overrides silently lose.
What the layer fixes. A core rule of cascade layers: any unlayered style
beats any layered style, regardless of specificity or import order. Since
theme.layered.css puts the whole toolkit in the crk layer, your own
(unlayered) rules — including every className / classes.* override — always
win, deterministically:
import '@overdoser/react-toolkit/theme.layered.css'; // instead of theme.css(If you import CSS from a .css entry instead of JS, the equivalent is
@import '@overdoser/react-toolkit/theme.css' layer(crk); as the first line.)
| | theme.css | theme.layered.css |
| --- | --- | --- |
| Override wins by | specificity, then import order (bundler-dependent) | always (unlayered beats layered) |
| --crk-* token theming | ✅ | ✅ (identical) |
| Affected by a global CSS reset | only via normal specificity/order | ⚠️ an unlayered reset also beats the toolkit's base styles |
The one caveat. Because layered styles lose to all unlayered styles, an
aggressive global reset (e.g. button { font: inherit }) would also override the
toolkit's base styles under theme.layered.css. If you ship a heavy reset, put
it in its own earlier layer so the order is explicit:
@layer reset, crk;Rule of thumb: if you heavily customize component internals via
classes / className, use theme.layered.css. Otherwise theme.css is fine.
Theme-token (--crk-*) overrides work the same in both.
Components
| Component | Description |
| --- | --- |
| Button | Variants: primary, secondary, danger, ghost. Multiple sizes. Loading states with dots, shimmer, and border animations. |
| Link | Styled link with variants and external link support (inline new-tab icon, toggleable). |
| Typography | Renders h1-h6, p, span, label. Supports weight, color, align, and truncate. |
| List / ListItem | Ordered and unordered lists with configurable spacing. |
| Table | Sortable columns, multi-sort with Ctrl+click, pagination, and server-side sort support. |
| Dropdown | Menu dropdown with chevron indicator. Also works as a selectable form input with options, value, and onChange. |
| Popover | Positioned popover anchored to a trigger element, with viewport-aware auto-flip. |
| Modal | Portal-based modal with Header, Body, and Footer compound components. Includes focus trap and escape/backdrop close. |
| Draggable | Free-move container for any content. Optional Draggable.Handle, axis lock, viewport/parent bounds, keyboard moves. |
| Toast | Imperative notifications: ToastProvider renders, standalone toast() (or useToast()) triggers. Variants, positions, auto-dismiss with hover-pause. |
| Tabs | Accessible tabs (Tabs.List / Tabs.Tab / Tabs.Panel). Line/solid/pill variants, vertical, keyboard nav. |
| Alert | Inline status message (info/success/warning/danger) with optional title, icon, and dismiss. |
| Badge | Small status pill. Variants, soft/solid/outline appearance, sizes, optional status dot. |
| Spinner | Indeterminate loading spinner; inherits currentColor. |
| Skeleton | Loading placeholder — text (multi-line), circle, or rect; pulse/wave animation. |
| Drawer | Off-canvas panel sliding from any edge. Header/Body/Footer, focus trap, Escape/backdrop close. |
| Tooltip | Lightweight hover/focus label with open delay and viewport-aware flipping. |
| Avatar | Image with initials/icon fallback, sizes, circle/square, presence dot. |
| Divider | Horizontal/vertical separator, optional centered label. |
| Breadcrumbs | Breadcrumb nav with auto separators and aria-current. |
| Pagination | Windowed page nav with ellipses (1 … 6 7 8 9 10 … 99), prev/next/first/last. |
| Slider | Single or dual-thumb range input; pointer + keyboard. |
| NumberInput | Numeric field with steppers, min/max clamp, arrow-key step. |
| Dropzone | File upload via drag-drop or click, with accept/maxSize validation. |
| Timer | Countdown/stopwatch, digital or ring, attachable label, imperative controls. |
| DatePicker | Date field with a pop-up calendar, keyboard nav, min/max and disabled days. |
| Form / FormField | Form wrapper with react-hook-form integration. |
| Input | Text input with sizes, error state, and prefix/suffix slots. |
| Select | Native <select> with a custom arrow indicator. |
| Checkbox | Checkbox with label and indeterminate state support; variant="switch" for a toggle switch. |
| CheckboxGroup | Multi-value selection as a checkbox list or toggleable chips. |
| Radio / RadioGroup | Context-based radio group. |
| Textarea | Textarea with resize control. |
Hooks
| Hook | Description |
| --- | --- |
| useClickOutside | Detect clicks outside a ref element. |
| useFocusTrap | Trap focus within a container. |
| useKeyboard | Bind keyboard shortcuts. |
| useTableSort | Manage table sort state (single and multi-column). |
Usage
Button
import { Button } from '@overdoser/react-toolkit';
<Button variant="primary" size="md" onClick={handleClick}>
Save
</Button>
<Button variant="danger" loading loadingStyle="dots">
Deleting...
</Button>Form
import { Form, FormField, Input, Button } from '@overdoser/react-toolkit';
import { useForm } from 'react-hook-form';
function LoginForm() {
const form = useForm();
return (
<Form form={form} onSubmit={(values) => console.log(values)}>
<FormField name="email" label="Email">
<Input />
</FormField>
<FormField name="password" label="Password">
<Input type="password" />
</FormField>
<Button type="submit" variant="primary">Log in</Button>
</Form>
);
}Toast
Mount ToastProvider once (it's the renderer), then fire notifications with the
standalone toast() — no hook or context required:
import { ToastProvider, toast, Button } from '@overdoser/react-toolkit';
function App() {
return (
<ToastProvider position="top-right">
<Button onClick={() => toast({ variant: 'success', title: 'Saved', description: 'All set.' })}>
Save
</Button>
</ToastProvider>
);
}toast() is importable and callable from anywhere (event handlers, utilities,
outside React). It returns an id and exposes toast.dismiss(id) /
toast.dismissAll(). A useToast() hook returning the same
{ toast, dismiss, dismissAll } is also available if you prefer.
Peer Dependencies
react>= 18react-dom>= 18react-hook-form>= 7 (optional, needed for Form/FormField)
License
MIT
