@dravyn/ui
Dark-first React component library. Zero runtime dependencies beyond React. Fully typed. Accessible. Built to compete.
Installation
Install with your package manager of choice. React and ReactDOM are the only peer dependencies.
# npm
npm install @dravyn/ui
# yarn
yarn add @dravyn/ui
# pnpm
pnpm add @dravyn/ui
Setup
Import the design tokens once at your app root. This loads all the CSS variables every component uses. Then import components anywhere.
// app/layout.tsx (Next.js) or main.tsx (Vite)
import '@dravyn/ui/tokens';
// Then in any component file:
import { Button, Card, Badge } from '@dravyn/ui';
Button
Five variants, three sizes, loading state with spinner, icon slots, full-width mode, and a forwarded ref. Accepts all native button HTML attributes.
Variants
Sizes + states
import { Button } from '@dravyn/ui';
// Variants
<Button variant="primary">Primary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
// Loading — disables and shows spinner
<Button loading>Saving...</Button>
// With icons
<Button variant="primary" leftIcon={<IconBolt size={16} />}>Deploy</Button>
<Button variant="danger" rightIcon={<IconTrash size={16} />}>Delete</Button>
// Full width
<Button fullWidth>Submit form</Button>
Badge
Seven colour variants for status tags and category labels. Supports a dot indicator and custom icon.
All variants
Active
Error
Beta
Info
Published
Pro
Archived
import { Badge } from '@dravyn/ui';
<Badge variant="teal" dot>Active</Badge>
<Badge variant="red" dot>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>
Input
Accessible text input with label, hint, error state, required indicator, and left/right icon slots. Forwarded ref supported.
Input states
✉
Shown on your public profile.
🔒
Slugs can't contain spaces.
import { Input } from '@dravyn/ui';
// With label and hint
<Input label="Username" hint="Shown on your public profile." />
// Error state
<Input label="Project slug" error="Slugs can't contain spaces." />
// With icon
<Input label="Email" leftIcon={<IconMail size={15} />} type="email" />
// Required
<Input label="Full name" required />
Textarea
Multi-line input with the same label/hint/error API as Input, plus an optional live character counter.
With character count
0/160
import { Textarea } from '@dravyn/ui';
<Textarea label="Bio" maxLength={160} showCount placeholder="Short bio..." />
<Textarea label="Message" error="Message is required." />
Card
Flexible container. Compose with CardHeader and CardFooter, or use as a plain wrapper. Supports a featured accent and clickable hover state.
Card variants
Standard card
Use for content sections, settings panels, dashboards, or any grouped information.
ClassSync Pro
Pro
Unlimited AI, study rooms, lecture transcription. Everything you need.
import { Card, CardHeader, CardFooter } from '@dravyn/ui';
<Card>
<CardHeader title="Project name" description="Last updated 2 days ago." />
<CardFooter align="between">
<Button variant="ghost">Cancel</Button>
<Button variant="primary">Save</Button>
</CardFooter>
</Card>
// Featured — teal top accent
<Card featured>...</Card>
// Clickable
<Card onClick={() => router.push('/project')}>...</Card>
Alert
Four semantic feedback banners. Supports a title, body text, custom icon, and a dismiss button.
All variants
ℹ
Heads up
Your API key expires in 3 days. Rotate it from the dashboard.
✓
Deployed successfully
ClassSync v1.1 is live at classsync.ink.
⚠
Approaching limit
You've used 87% of your free tier storage.
✕
Build failed
expo build:android exited with code 1. Check your eas.json.
import { Alert } from '@dravyn/ui';
<Alert variant="info" title="Heads up">Your key expires in 3 days.</Alert>
<Alert variant="success" title="Deployed">v1.1 is live.</Alert>
<Alert variant="warning" title="Approaching limit" onClose={() => hide()} />
<Alert variant="danger" title="Build failed">Check eas.json.</Alert>
Avatar
Profile pictures with smart initials fallback. Colour is auto-assigned from the name — same person always gets the same colour. Five sizes, six colour variants, status dot.
Sizes, variants, status
JD
JD
JD
GA
AO
JD
MK
CB
import { Avatar } from '@dravyn/ui';
// Auto-generates initials + colour from name
<Avatar name="Jeremiah Adeniyi" />
// From image URL — falls back to initials if image fails
<Avatar name="Jeremiah Adeniyi" src="https://..." />
// Sizes
<Avatar name="Jerry" size="xs" />
<Avatar name="Jerry" size="xl" />
// With status dot
<Avatar name="Jeremiah Adeniyi" status="online" />
<Avatar name="Gabriel Akin" status="away" />
Toggle
Accessible on/off switch. Works controlled or uncontrolled. Label can appear on either side.
Interactive — click to toggle
import { Toggle } from '@dravyn/ui';
// Uncontrolled
<Toggle label="Dark mode" defaultChecked />
// Controlled
const [on, setOn] = useState(false);
<Toggle label="Notifications" checked={on} onChange={setOn} />
// Label on the left
<Toggle label="Auto-save" labelPosition="left" />
Modal
Accessible dialog. Entrance animation, Escape to close, backdrop click to close, body scroll lock. Four size presets.
Live demo
import { Modal, Button } from '@dravyn/ui';
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
title="Confirm deletion"
description="This action cannot be undone."
footer={
<>
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="danger" onClick={handleDelete}>Delete</Button>
</>
}
/>
Spinner
Loading indicator in four sizes. Announces itself to screen readers via an aria-label.
Sizes
import { Spinner } from '@dravyn/ui';
<Spinner />
<Spinner size="sm" />
<Spinner size="lg" label="Fetching posts..." />
{isLoading ? <Spinner /> : <MyContent />}
Theming
All colours, spacing, and motion values are CSS custom properties. Override them on
:root globally or on any container for scoped overrides. Light mode is built in./* globals.css — change the primary accent to your brand colour */
:root {
--dui-teal-400: #6366f1; /* indigo instead of teal */
--dui-teal-600: #4f46e5;
--dui-bg: #0f0f23;
--dui-bg-raised: #1a1a2e;
}
/* Light mode — add data-theme="light" to your html tag */
document.documentElement.setAttribute('data-theme', 'light');
| Token | Default (dark) | Purpose |
|---|---|---|
| --dui-teal-400 | #4ecdc4 | Primary accent — buttons, focus rings, featured accents |
| --dui-bg | #0d0d0d | Page background |
| --dui-bg-raised | #161616 | Card and input backgrounds |
| --dui-bg-overlay | #1e1e1e | Hover states, dropdowns |
| --dui-border | rgba(255,255,255,0.07) | Subtle borders |
| --dui-border-strong | rgba(255,255,255,0.16) | Input and card borders |
| --dui-text | #f0f0f0 | Primary text |
| --dui-text-muted | #888888 | Labels, secondary text |
| --dui-text-hint | #555555 | Placeholders, disabled text |
| --dui-radius | 8px | Default border radius |
| --dui-radius-lg | 12px | Cards, panels |
| --dui-transition | 150ms ease | Fast hover/state transitions |
@dravyn/ui
Built by Dravyn Tech · MIT License
Dark-first
TypeScript
Zero deps