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

dravyn-ui

v0.1.0

Published

Dark-first React component library by Dravyn Tech. Zero dependencies, TypeScript, fully accessible.

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/ui

Peer dependencies — make sure these are in your project:

npm install react react-dom

Setup

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

  1. Clone the repo: git clone https://github.com/dravyn/ui
  2. Install dependencies: npm install
  3. Start watch mode: npm run dev
  4. Make your changes in src/components/
  5. 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 MyComponent

Then add the export to src/index.ts.


Built with care by Dravyn Tech · MIT License