dravyn-ui
v0.1.0
Published
Dark-first React component library by Dravyn Tech. Zero dependencies, TypeScript, fully accessible.
Maintainers
Readme
@dravyn/ui
Dark-first React component library by Dravyn Tech.
Production-grade, fully typed, zero runtime dependencies beyond React.
Built for developers who want a consistent, sharp design system without fighting a framework.
Contents
Installation
# npm
npm install @dravyn/ui
# yarn
yarn add @dravyn/ui
# pnpm
pnpm add @dravyn/uiPeer dependencies — make sure these are in your project:
npm install react react-domSetup
Import the design tokens once at the root of your app. This loads all CSS variables that every component depends on.
Next.js — add to app/layout.tsx or pages/_app.tsx:
import '@dravyn/ui/tokens';Vite / CRA — add to main.tsx or index.tsx:
import '@dravyn/ui/tokens';That's it. Now import any component anywhere in your project:
import { Button, Card, Badge } from '@dravyn/ui';Components
Button
The core interactive element. Five variants, three sizes, loading state, icon slots, and a forwarded ref.
import { Button } from '@dravyn/ui';
// Basic usage
<Button>Click me</Button>
// Variants
<Button variant="primary">Primary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Delete</Button>
<Button variant="success">Save</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="md">Medium</Button> {/* default */}
<Button size="lg">Large</Button>
// Loading state — disables the button and shows a spinner
<Button loading>Saving...</Button>
// Full width
<Button fullWidth>Submit form</Button>
// With icons — pass any React node
import { IconBolt, IconTrash } from '@tabler/icons-react';
<Button variant="primary" leftIcon={<IconBolt size={16} />}>
Deploy
</Button>
<Button variant="danger" rightIcon={<IconTrash size={16} />}>
Delete project
</Button>
// Disabled
<Button disabled>Not available</Button>
// As a link — spread native button props
<Button onClick={() => router.push('/dashboard')}>
Go to dashboard
</Button>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'primary' \| 'outline' \| 'ghost' \| 'danger' \| 'success' | 'outline' | Visual style |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| loading | boolean | false | Shows spinner, disables interaction |
| fullWidth | boolean | false | Stretches to container width |
| leftIcon | React.ReactNode | — | Icon before the label |
| rightIcon | React.ReactNode | — | Icon after the label |
| disabled | boolean | false | Native disabled state |
All native <button> HTML attributes are also accepted (onClick, type, aria-*, etc.).
Badge
Compact status labels and category tags. Seven colour variants with optional dot indicator or icon.
import { Badge } from '@dravyn/ui';
// Basic
<Badge>Draft</Badge>
// Variants
<Badge variant="teal">Active</Badge>
<Badge variant="red">Error</Badge>
<Badge variant="amber">Beta</Badge>
<Badge variant="blue">Info</Badge>
<Badge variant="green">Published</Badge>
<Badge variant="purple">Pro</Badge>
<Badge variant="gray">Archived</Badge>
// With a status dot
<Badge variant="teal" dot>Online</Badge>
<Badge variant="red" dot>Offline</Badge>
// With an icon
import { IconBolt } from '@tabler/icons-react';
<Badge variant="blue" icon={<IconBolt size={11} />}>AI</Badge>
// Common pattern — badge next to a title
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<h2>ClassSync Pro</h2>
<Badge variant="purple">Pro</Badge>
</div>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'teal' \| 'red' \| 'amber' \| 'blue' \| 'green' \| 'purple' \| 'gray' | 'gray' | Colour theme |
| dot | boolean | false | Shows a coloured dot before the label |
| icon | React.ReactNode | — | Icon before the label |
Input
A fully accessible text input with label, hint text, error state, and left/right icon slots.
import { Input } from '@dravyn/ui';
// Basic with label
<Input label="Email address" placeholder="[email protected]" type="email" />
// With hint text
<Input
label="Username"
placeholder="jeremiah_dev"
hint="This will appear on your public profile."
/>
// Error state — show validation feedback
<Input
label="Project slug"
value="my project"
error="Slugs can't contain spaces. Use hyphens instead."
/>
// Required field — shows a red asterisk
<Input label="Full name" required />
// With icons — pass any React node
import { IconMail, IconSearch, IconEye } from '@tabler/icons-react';
<Input
label="Email"
type="email"
leftIcon={<IconMail size={15} />}
placeholder="[email protected]"
/>
<Input
label="Search"
leftIcon={<IconSearch size={15} />}
placeholder="Search projects..."
/>
// Controlled usage
const [email, setEmail] = React.useState('');
<Input
label="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
// Forwarded ref (e.g. for auto-focus)
const inputRef = React.useRef<HTMLInputElement>(null);
<Input ref={inputRef} label="Name" />Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| label | string | — | Label above the input |
| hint | string | — | Helper text below the input |
| error | string | — | Error message — also applies error styles |
| leftIcon | React.ReactNode | — | Icon inside the left edge |
| rightIcon | React.ReactNode | — | Icon inside the right edge |
| fullWidth | boolean | true | Stretches to container width |
| required | boolean | false | Shows red asterisk on label |
All native <input> HTML attributes are also accepted.
Textarea
Multi-line input with the same label/hint/error API as Input, plus optional character count.
import { Textarea } from '@dravyn/ui';
// Basic
<Textarea label="Description" placeholder="Tell us about your project..." />
// Character counter — shows live count against maxLength
<Textarea
label="Bio"
maxLength={160}
showCount
placeholder="Short bio..."
/>
// With error
<Textarea
label="Message"
error="Message is required."
/>
// Controlled
<Textarea
label="Notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={6}
/>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| label | string | — | Label above the textarea |
| hint | string | — | Helper text below |
| error | string | — | Error message |
| showCount | boolean | false | Shows current/max character count — requires maxLength |
| fullWidth | boolean | true | Stretches to container width |
All native <textarea> HTML attributes are also accepted.
Card
A flexible container component. Use it standalone or compose it with CardHeader and CardFooter.
import { Card, CardHeader, CardFooter } from '@dravyn/ui';
// Basic card
<Card>
<p>Any content goes here.</p>
</Card>
// With sub-components
<Card>
<CardHeader
title="ClassSync Pro"
description="Unlimited AI, study rooms, and lecture transcription."
/>
<p>More card content here.</p>
<CardFooter align="between">
<span>₦2,500/mo</span>
<Button variant="primary" size="sm">Upgrade</Button>
</CardFooter>
</Card>
// Featured card — teal top border accent
<Card featured>
<CardHeader title="Recommended plan" />
</Card>
// Clickable card — adds hover state and pointer cursor
<Card onClick={() => router.push('/project/123')}>
<CardHeader title="My Project" description="Last updated 2 days ago." />
</Card>
// Padding presets
<Card padding="sm">Compact card</Card>
<Card padding="md">Default card</Card>
<Card padding="lg">Spacious card</Card>
// CardHeader with a right-side action
<CardHeader
title="Team members"
description="3 active members"
action={<Button size="sm" variant="outline">Invite</Button>}
/>
// CardFooter alignment
<CardFooter align="left">Left-aligned actions</CardFooter>
<CardFooter align="right">Right-aligned actions</CardFooter> {/* default */}
<CardFooter align="between">
<span>Cancel</span>
<Button variant="primary">Confirm</Button>
</CardFooter>Card Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| featured | boolean | false | Adds teal top border accent |
| flat | boolean | false | Removes background and border |
| padding | 'sm' \| 'md' \| 'lg' | 'md' | Internal padding |
| onClick | function | — | Makes the card interactive |
CardHeader Props
| Prop | Type | Description |
|------|------|-------------|
| title | string | Required — the card heading |
| description | string | Optional subtitle |
| action | React.ReactNode | Right-side element (button, badge, etc.) |
CardFooter Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| align | 'left' \| 'right' \| 'between' | 'right' | How footer children are distributed |
Alert
Contextual feedback banners in four semantic types. Supports titles, descriptions, custom icons, and a dismiss button.
import { Alert } from '@dravyn/ui';
// Basic — just a message
<Alert variant="info">Your session will expire in 30 minutes.</Alert>
// With a title
<Alert variant="success" title="Deployed successfully">
ClassSync v1.1 is live at classsync.ink.
</Alert>
<Alert variant="warning" title="Approaching limit">
You've used 87% of your free tier storage.
</Alert>
<Alert variant="danger" title="Build failed">
expo build:android exited with code 1. Check your eas.json.
</Alert>
// Dismissible — pass onClose
const [visible, setVisible] = React.useState(true);
{visible && (
<Alert
variant="info"
title="New update available"
onClose={() => setVisible(false)}
>
Refresh the page to get the latest version.
</Alert>
)}
// Custom icon
import { IconRocket } from '@tabler/icons-react';
<Alert variant="success" icon={<IconRocket size={16} />} title="Launched!">
Your app is now live.
</Alert>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| variant | 'info' \| 'success' \| 'warning' \| 'danger' | 'info' | Semantic colour and icon |
| title | string | — | Bold heading line |
| description | string | — | Body text (alternative to children) |
| icon | React.ReactNode | — | Replaces the default variant icon |
| onClose | () => void | — | Renders a close button and calls this on click |
Avatar
User profile picture with smart initials fallback, status dot, five sizes, and six colour variants. The colour is auto-assigned from the name so the same person always gets the same colour.
import { Avatar } from '@dravyn/ui';
// From a name — auto-generates initials and colour
<Avatar name="Jeremiah Adeniyi" />
<Avatar name="Gabriel Akin" />
// From an image URL — falls back to initials if the image fails
<Avatar
name="Jeremiah Adeniyi"
src="https://example.com/avatar.jpg"
alt="Jeremiah's profile picture"
/>
// Explicit initials and variant
<Avatar initials="DJ" variant="teal" />
<Avatar initials="GA" variant="green" />
// Sizes
<Avatar name="Jerry" size="xs" /> {/* 24px */}
<Avatar name="Jerry" size="sm" /> {/* 32px */}
<Avatar name="Jerry" size="md" /> {/* 40px — default */}
<Avatar name="Jerry" size="lg" /> {/* 52px */}
<Avatar name="Jerry" size="xl" /> {/* 64px */}
// With a status dot
<Avatar name="Jeremiah Adeniyi" status="online" />
<Avatar name="Gabriel Akin" status="away" />
<Avatar name="Alex Obi" status="offline" />
// Avatar group — overlap them with negative margin
<div style={{ display: 'flex' }}>
<Avatar name="Jeremiah Adeniyi" size="sm" />
<Avatar name="Gabriel Akin" size="sm" style={{ marginLeft: -8 }} />
<Avatar name="Alex Obi" size="sm" style={{ marginLeft: -8 }} />
</div>Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| name | string | — | Full name — used for initials + colour |
| src | string | — | Image URL |
| alt | string | — | Alt text for image |
| initials | string | — | Explicit initials (overrides name) |
| size | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' | 'md' | Size preset |
| variant | 'teal' \| 'purple' \| 'blue' \| 'green' \| 'amber' \| 'red' | auto | Colour variant |
| status | 'online' \| 'away' \| 'offline' | — | Status dot |
Toggle
An accessible on/off switch. Works controlled or uncontrolled.
import { Toggle } from '@dravyn/ui';
// Uncontrolled — manages its own state
<Toggle label="Dark mode" defaultChecked />
// Controlled
const [enabled, setEnabled] = React.useState(false);
<Toggle
label="Email notifications"
checked={enabled}
onChange={setEnabled}
/>
// Label on the left
<Toggle label="Auto-save" labelPosition="left" defaultChecked />
// Small size
<Toggle label="Compact view" size="sm" />
// Disabled
<Toggle label="Feature flag" disabled defaultChecked />Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| checked | boolean | — | Controlled state |
| defaultChecked | boolean | false | Initial state (uncontrolled) |
| onChange | (checked: boolean) => void | — | Called with the new state on change |
| label | string | — | Text label beside the toggle |
| labelPosition | 'left' \| 'right' | 'right' | Which side the label appears |
| size | 'sm' \| 'md' | 'md' | Size preset |
| disabled | boolean | false | Disables interaction |
Modal
An accessible dialog with entrance animation, keyboard support (Escape to close), backdrop click to close, and scroll lock on the body.
import { Modal, Button } from '@dravyn/ui';
// Basic usage
const [open, setOpen] = React.useState(false);
<Button onClick={() => setOpen(true)}>Open modal</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
title="Confirm deletion"
description="This action cannot be undone. The project and all its data will be permanently removed."
footer={
<>
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="danger" onClick={handleDelete}>Delete project</Button>
</>
}
/>
// With body content
<Modal
open={open}
onClose={() => setOpen(false)}
title="Edit profile"
>
<Input label="Display name" defaultValue="Jeremiah Adeniyi" />
<Textarea label="Bio" placeholder="Short bio..." style={{ marginTop: 14 }} />
</Modal>
// Sizes
<Modal size="sm" .../> {/* 380px */}
<Modal size="md" .../> {/* 520px — default */}
<Modal size="lg" .../> {/* 720px */}
<Modal size="full" .../> {/* nearly full screen */}
// Prevent closing via backdrop click
<Modal disableBackdropClose open={open} onClose={() => setOpen(false)} ... />Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | — | Required. Controls visibility |
| onClose | () => void | — | Required. Called on Escape or backdrop click |
| title | string | — | Modal heading |
| description | string | — | Subtitle below the heading |
| footer | React.ReactNode | — | Action area at the bottom |
| size | 'sm' \| 'md' \| 'lg' \| 'full' | 'md' | Width preset |
| disableBackdropClose | boolean | false | Prevents backdrop click from closing |
Spinner
A simple loading indicator. Use it inline, as a page loader, or inside buttons.
import { Spinner } from '@dravyn/ui';
// Basic
<Spinner />
// Sizes
<Spinner size="xs" /> {/* 12px */}
<Spinner size="sm" /> {/* 18px */}
<Spinner size="md" /> {/* 26px — default */}
<Spinner size="lg" /> {/* 38px */}
// Custom accessible label
<Spinner label="Fetching posts..." />
// Centre it on a page
<div style={{ display: 'flex', justifyContent: 'center', padding: '80px 0' }}>
<Spinner size="lg" />
</div>
// Inline loading state (when you're not using Button's built-in `loading` prop)
{isLoading ? <Spinner size="sm" /> : <MyContent />}Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| size | 'xs' \| 'sm' \| 'md' \| 'lg' | 'md' | Size preset |
| label | string | 'Loading…' | Screen reader announcement |
Theming
All colours, spacing, and motion values live in CSS custom properties defined in the tokens file. Override them on :root (global) or on any container element (scoped).
/* globals.css — override any token */
:root {
/* Change the primary accent from teal to your brand colour */
--dui-teal-400: #6366f1; /* indigo */
--dui-teal-600: #4f46e5;
--dui-teal-800: #3730a3;
/* Change the base background */
--dui-bg: #0f0f23;
--dui-bg-raised: #1a1a2e;
}Light mode — add data-theme="light" to your <html> tag or any container:
// Toggle between modes
document.documentElement.setAttribute('data-theme', isDark ? '' : 'light');The library ships with sensible light mode overrides out of the box — your app inherits them automatically.
Scoped theming — apply a different palette to one section:
<div data-theme="light" style={{ padding: 24 }}>
{/* These components will use light mode even if the rest of the app is dark */}
<Card>
<CardHeader title="Light section" />
</Card>
</div>TypeScript
Every component exports its props interface. Import them when building wrappers or typed helper functions:
import type {
ButtonProps,
ButtonVariant,
BadgeVariant,
AlertVariant,
AvatarSize,
ModalProps,
} from '@dravyn/ui';
// Example — a typed wrapper
function ConfirmButton({ variant = 'primary', ...props }: ButtonProps) {
return <Button variant={variant} {...props} />;
}
// Example — a dynamic badge
function StatusBadge({ status }: { status: string }) {
const variantMap: Record<string, BadgeVariant> = {
active: 'teal',
error: 'red',
pending: 'amber',
archived: 'gray',
};
return (
<Badge variant={variantMap[status] ?? 'gray'} dot>
{status}
</Badge>
);
}Contributing
- Clone the repo:
git clone https://github.com/dravyn/ui - Install dependencies:
npm install - Start watch mode:
npm run dev - Make your changes in
src/components/ - Submit a PR with a clear description
Adding a new component:
src/components/
MyComponent/
MyComponent.tsx ← component logic and JSX
MyComponent.css ← scoped styles using token variables
index.ts ← re-exports MyComponentThen add the export to src/index.ts.
Built with care by Dravyn Tech · MIT License
