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

devign

v3.0.0

Published

A beautiful React component library with glassmorphism effects, premium animations, and a fully themeable design system

Readme

Devign

A React component library with glassmorphism effects, premium micro-animations, and a fully themeable design system built on Tailwind CSS v4.

npm version npm downloads license

Live Demo → — See all components in action with light/dark mode toggle.

🎨 Want to customize the theme? Run npx devign init to generate a ready-made theme file with every design token — then just tweak the values you want. See Customization.

Migrating from yems-ui? Just change your import — devign is a drop-in replacement. See Migration.


Table of Contents


Installation

npm install devign
# or
pnpm add devign
# or
yarn add devign

Peer dependencies (install if not already present):

npm install react react-dom

Setup

Devign requires Tailwind CSS v4 and the @tailwindcss/vite plugin.

1. Install Tailwind CSS v4

npm install tailwindcss @tailwindcss/vite

2. Configure Vite

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

3. Import the styles

In your app's entry CSS file (e.g. src/index.css):

@import "tailwindcss";

/* Tell Tailwind to scan devign's dist folder for class names */
@source "../node_modules/devign/dist";

/* Import the design system tokens and base styles */
@import "devign/styles.css";

The @source directive is required. Without it, Tailwind won't generate the utility classes used by Devign components.

4. Generate a theme file

Recommended: Run this command to get a fully commented theme file with every design token — ready to customize.

npx devign init

This creates src/devign.css with every design token ready to customize. Import it after Devign styles in your CSS:

@import "./devign.css"; /* add this line after @import "devign/styles.css" */

See Customization for full details.

5. Import styles in your entry file

// src/main.tsx
import "./index.css";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

6. Use components

import { Button, Card, CardHeader, CardTitle, CardContent } from "devign";

function App() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Hello World</CardTitle>
      </CardHeader>
      <CardContent>
        <Button variant="primary">Get Started</Button>
      </CardContent>
    </Card>
  );
}

Customization

Devign is designed to be dead-simple to customize. One command generates a theme file with every CSS variable, fully commented — just change the values you want.

Quick start (recommended)

Run npx devign init to generate your theme file instantly. You can also specify a custom output directory with --dir.

npx devign init

This creates src/devign.css with all of Devign's design tokens pre-filled and commented. Open it, tweak any values, and import it in your main CSS:

@import "tailwindcss";
@source "../node_modules/devign/dist";
@import "devign/styles.css";
@import "./devign.css";          /* ← your overrides */

That's it. Every component in the library instantly reflects your changes.

Custom output directory

npx devign init --dir=assets/css
npx devign init --dir=styles

What's in the generated file?

The devign.css file contains every customizable variable organized by category:

| Section | What to change | | ---------------- | ----------------------------------------------------------------- | | Brand palette | --brand-500, --brand-900 — change your primary color in one place | | Accent palette | --accent-500, --accent-700 — your secondary highlight color | | Neutrals | --neutral-* — background, border, and text shades | | Semantic tokens | Rewire which palette color maps to which role (advanced) | | Shape | --radius — controls every border radius in the library | | Glassmorphism | Blur, transparency, and shadows for the glass effect | | Shadows | Elevation levels from sm to xl plus brand-tinted glows | | Typography | Font family tokens (Devign doesn't load fonts — you choose) | | Transitions | Speed and easing curves (set to 0ms to disable motion) | | Dark mode | All of the above, scoped to .dark on <html> |

Tip: You only need to keep the lines you change. Delete everything else and Devign uses its built-in defaults for the rest.

Example: Blue theme in 3 lines

/* src/devign.css — delete everything except: */
:root {
  --brand-500: #0ea5e9;
  --brand-900: #0c4a6e;
  --accent-500: #f59e0b;
}

Every button, input, card, focus ring, and glass effect in the library updates automatically.


Theming

Devign uses a two-layer CSS variable system built into Tailwind v4's @theme. You can override any part of it in your own CSS — or run npx devign init to generate a ready-made theme file with all tokens pre-filled (see Customization above).

How it works

Layer 1 — Raw palette      →    Layer 2 — Semantic tokens    →    Components
--brand-500: #5000ab       →    --color-primary: var(--brand-500)  →  bg-primary
--accent-500: #e3b23c      →    --color-accent: var(--accent-500)  →  bg-accent

To retheme the library, you only need to override Layer 1 values. Everything cascades automatically.


Changing the primary color

Add this to your CSS file after importing Devign styles:

@import "tailwindcss";
@source "../node_modules/devign/dist";
@import "devign/styles.css";

/* Override just what you want to change */
:root {
  --brand-500: #0066ff; /* new primary — all buttons, links, rings update */
  --brand-900: #001a66; /* new secondary / dark shade */
  --accent-500: #f59e0b; /* new accent color */
}

All themeable tokens

Brand palette (Layer 1)

| Token | Default | Purpose | | -------------- | --------- | ---------------------- | | --brand-50 | #ede8f7 | Lightest brand tint | | --brand-100 | #d4c5f0 | Light brand tint | | --brand-200 | #a98cdf | | | --brand-300 | #7e53ce | | | --brand-400 | #6529be | | | --brand-500 | #5000ab | Main primary color | | --brand-600 | #40008a | | | --brand-700 | #300069 | | | --brand-800 | #200048 | | | --brand-900 | #1c0636 | Secondary / dark shade | | --accent-500 | #e3b23c | Main accent color | | --accent-700 | #bb4d00 | Ember / dark accent |

Semantic tokens (Layer 2)

These are what components use internally. You can override these individually if you want to rewire which color plays which role:

| Token | Default maps to | Used for | | ---------------------------- | --------------- | ----------------------------------- | | --color-primary | --brand-500 | Primary buttons, links, focus rings | | --color-primary-foreground | --neutral-50 | Text on primary backgrounds | | --color-secondary | --brand-900 | Secondary buttons | | --color-accent | --accent-500 | Accent buttons, highlights | | --color-ember | --accent-700 | Ember variant | | --color-destructive | --color-error | Destructive actions | | --color-background | --neutral-100 | Page background | | --color-foreground | --neutral-900 | Body text | | --color-muted | --neutral-100 | Subtle backgrounds | | --color-muted-foreground | --neutral-500 | Placeholder / hint text | | --color-border | --neutral-200 | Borders and dividers | | --color-ring | --brand-500 | Focus ring color |

Shape & spacing

| Token | Default | Purpose | | ---------- | ------- | ------------------------------------- | | --radius | 12px | Base border radius for all components |

:root {
  --radius: 8px; /* more angular */
  /* or */
  --radius: 20px; /* more pill-shaped */
}

Shadows

| Token | Default | Purpose | | ------------------ | ------------- | -------------------- | | --shadow-sm | subtle | Small elevation | | --shadow-md | medium | Default cards | | --shadow-lg | prominent | Dropdowns, modals | | --shadow-xl | strong | Tooltips, popovers | | --shadow-primary | brand-tinted | Primary button hover | | --shadow-accent | accent-tinted | Accent button hover |

:root {
  /* Softer shadows for a flatter look */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.12);
}

Glassmorphism

| Token | Light default | Purpose | | ------------------- | ------------------------ | --------------------- | | --glass-bg | rgba(255,255,255,0.65) | Glass background | | --glass-bg-strong | rgba(255,255,255,0.85) | Strong glass | | --glass-border | rgba(0,0,0,0.08) | Glass border | | --glass-blur | 16px | Backdrop blur amount | | --glass-card-bg | rgba(255,255,255,0.75) | Card glass background | | --glass-shadow | subtle blue-tinted | Glass drop shadow |

:root {
  /* Heavier blur for more prominent glass effect */
  --glass-blur: 24px;
  --glass-bg: rgba(255, 255, 255, 0.5);

  /* Or minimal glass — nearly transparent */
  --glass-blur: 8px;
  --glass-bg: rgba(255, 255, 255, 0.9);
}

Typography

| Token | Default | Purpose | | ---------------- | ---------------------- | -------------------- | | --font-sans | "Poppins", system-ui | Body font | | --font-display | "Otama EP", Georgia | Display/heading font |

:root {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
}

Transitions

| Token | Default | Purpose | | --------------------- | ------- | ---------------- | | --transition-fast | 150ms | Hover states | | --transition-normal | 250ms | Most animations | | --transition-slow | 350ms | Page transitions |

:root {
  /* Snappier animations */
  --transition-fast: 100ms;
  --transition-normal: 180ms;

  /* Or disable motion for accessibility */
  --transition-fast: 0ms;
  --transition-normal: 0ms;
}

Complete theme example

Tip: Instead of writing this by hand, run npx devign init to get a pre-filled file you can edit.

@import "tailwindcss";
@source "../node_modules/devign/dist";
@import "devign/styles.css";

/* A blue/teal theme with softer radius and minimal glass */
:root {
  /* Brand */
  --brand-500: #0ea5e9;
  --brand-900: #0c4a6e;
  --accent-500: #f59e0b;
  --accent-700: #d97706;

  /* Shape */
  --radius: 8px;

  /* Glass — subtle */
  --glass-blur: 8px;
  --glass-bg: rgba(255, 255, 255, 0.8);
  --glass-card-bg: rgba(255, 255, 255, 0.9);

  /* Shadows — flatter */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
  --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.1);
}

Per-component override via className

Every component accepts a className prop for one-off overrides without touching theme variables:

<Button variant="primary" className="rounded-full px-8">
  Pill Button
</Button>

<Card className="border-2 border-primary/30 shadow-xl">
  Custom Card
</Card>

<Input className="h-14 text-lg" placeholder="Large input" />

Components

Button

import { Button } from "devign";

// Variants
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="accent">Accent</Button>
<Button variant="ember">Ember</Button>
<Button variant="destructive">Delete</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="outline">Outline</Button>
<Button variant="link">Link</Button>

// Outline variants
<Button variant="outline-primary">Outline Primary</Button>
<Button variant="outline-secondary">Outline Secondary</Button>
<Button variant="outline-accent">Outline Accent</Button>
<Button variant="outline-ember">Outline Ember</Button>
<Button variant="outline-destructive">Outline Destructive</Button>

// Ghost variants
<Button variant="ghost-primary">Ghost Primary</Button>
<Button variant="ghost-secondary">Ghost Secondary</Button>

// Sizes
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="xl">Extra Large</Button>
<Button size="icon"><SearchIcon /></Button>

// With icons
<Button leftIcon={<PlusIcon />}>Add Item</Button>
<Button rightIcon={<ArrowRight />}>Continue</Button>

// Loading state
<Button isLoading>Saving...</Button>

// As a link (renders as child element)
<Button asChild>
  <a href="/dashboard">Dashboard</a>
</Button>

Props:

| Prop | Type | Default | Description | | ----------- | --------------------------------------------------------- | --------- | ------------------------------------- | | variant | see above | primary | Visual style | | size | sm \| default \| lg \| xl \| icon \| icon-sm \| icon-lg | default | Size | | isLoading | boolean | false | Shows spinner, disables interaction | | leftIcon | ReactNode | — | Icon before label | | rightIcon | ReactNode | — | Icon after label | | asChild | boolean | false | Renders as child element (e.g. <a>) |


Card

import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, StatCard } from "devign";

<Card>
  <CardHeader>
    <CardTitle>Card Title</CardTitle>
    <CardDescription>A short description.</CardDescription>
  </CardHeader>
  <CardContent>
    Content goes here.
  </CardContent>
  <CardFooter>
    <Button variant="outline-primary">Action</Button>
  </CardFooter>
</Card>

// With hover animation
<Card hover>
  <CardContent>Lifts on hover</CardContent>
</Card>

// Stats card
<StatCard
  title="Total Revenue"
  value="$12,340"
  icon={<DollarSign />}
  trend={{ value: 12.5, isPositive: true }}
  description="vs last month"
/>

StatCard props:

| Prop | Type | Description | | ------------- | ---------------------------------------- | --------------------------- | | title | string | Label above the value | | value | string \| number | The main figure | | icon | ReactNode | Icon shown top-right | | trend | { value: number, isPositive: boolean } | Percentage change indicator | | description | string | Sub-label below the value |


Input

import { Input, Label, FormField, Textarea } from "devign";

// Variants
<Input placeholder="Default" />
<Input placeholder="Filled" variant="filled" />
<Input placeholder="Ghost" variant="ghost" />

// Sizes
<Input placeholder="Small" inputSize="sm" />
<Input placeholder="Large" inputSize="lg" />

// With icons and addons
<Input leftIcon={<Search className="h-4 w-4" />} placeholder="Search..." />
<Input leftAddon="https://" placeholder="yoursite.com" />
<Input rightAddon=".com" placeholder="domain" />

// Validation states
<Input state="error" error="This field is required" />
<Input state="success" hint="Looks good!" />

// Label + FormField (handles layout, label, error, hint together)
<FormField label="Email" htmlFor="email" required error="Invalid email">
  <Input id="email" type="email" />
</FormField>

// Textarea
<Textarea placeholder="Write something..." rows={4} />
<Textarea variant="filled" state="error" error="Required" />

Alert

import { Alert, AlertTitle, AlertDescription } from "devign";

<Alert variant="info">
  <InfoIcon className="h-4 w-4" />
  <AlertTitle>Heads up</AlertTitle>
  <AlertDescription>Something you should know.</AlertDescription>
</Alert>

// Variants: default | info | success | warning | error
<Alert variant="success">...</Alert>
<Alert variant="warning">...</Alert>
<Alert variant="error">...</Alert>

// Dismissible
<Alert variant="info" dismissible onDismiss={() => console.log("dismissed")}>
  <AlertTitle>Dismissible</AlertTitle>
</Alert>

Badge

import { Badge, StatusBadge } from "devign";

// Filled
<Badge variant="primary">Primary</Badge>
<Badge variant="success">Active</Badge>
<Badge variant="error">Failed</Badge>

// Soft (light background, colored text)
<Badge variant="soft-success">Paid</Badge>
<Badge variant="soft-warning">Pending</Badge>
<Badge variant="soft-error">Unpaid</Badge>

// Outline
<Badge variant="outline-primary">Draft</Badge>

// With dot indicator
<Badge variant="soft-success" dot>Online</Badge>

// Sizes
<Badge size="sm">Small</Badge>
<Badge size="lg">Large</Badge>

// Preset status badge
<StatusBadge status="active" />
<StatusBadge status="pending" />
<StatusBadge status="inactive" />

Dialog

import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
} from "devign";

<Dialog>
  <DialogTrigger asChild>
    <Button>Open Dialog</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Edit Profile</DialogTitle>
      <DialogDescription>Make changes to your profile here.</DialogDescription>
    </DialogHeader>
    <div className="py-4">
      <Input placeholder="Name" />
    </div>
    <DialogFooter>
      <Button variant="primary">Save</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

Select

import {
  Select,
  SelectTrigger,
  SelectValue,
  SelectContent,
  SelectItem,
} from "devign";

const [value, setValue] = useState("");

<Select value={value} onValueChange={setValue}>
  <SelectTrigger className="w-48">
    <SelectValue placeholder="Choose one" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="option1">Option 1</SelectItem>
    <SelectItem value="option2">Option 2</SelectItem>
    <SelectItem value="option3">Option 3</SelectItem>
  </SelectContent>
</Select>

Tabs

import { Tabs, TabsList, TabsTrigger, TabsContent } from "devign";

<Tabs defaultValue="overview">
  <TabsList>
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="analytics">Analytics</TabsTrigger>
    <TabsTrigger value="settings">Settings</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">Overview content</TabsContent>
  <TabsContent value="analytics">Analytics content</TabsContent>
  <TabsContent value="settings">Settings content</TabsContent>
</Tabs>

Toast

import { Toaster, useToast } from "devign";

// Add <Toaster /> once near the root of your app
function App() {
  return (
    <>
      <YourApp />
      <Toaster />
    </>
  );
}

// Use the hook anywhere
function MyComponent() {
  const { toast } = useToast();

  return (
    <Button
      onClick={() =>
        toast({ title: "Saved!", description: "Your changes have been saved." })
      }
    >
      Save
    </Button>
  );
}

// Destructive variant
toast({
  variant: "destructive",
  title: "Error",
  description: "Something went wrong.",
});

Skeleton

import { Skeleton, SkeletonCard, SkeletonText, SkeletonAvatar, SkeletonTable } from "devign";

// Basic
<Skeleton className="h-4 w-48" />
<Skeleton variant="circular" className="h-10 w-10" />

// Animation variants
<Skeleton animation="pulse" className="h-4 w-full" />   // default
<Skeleton animation="wave" className="h-4 w-full" />
<Skeleton animation="none" className="h-4 w-full" />

// Preset layouts
<SkeletonCard />
<SkeletonText lines={4} />
<SkeletonAvatar size="lg" />
<SkeletonTable rows={5} columns={4} />

Typography

import { Heading, Text, Code, Lead, Blockquote } from "devign";

// Heading — renders h1-h6 with size/weight/gradient variants
<Heading as="h1" size="4xl" gradient="primary">Welcome to Devign</Heading>
<Heading as="h2" size="2xl" weight="bold">Section Title</Heading>
<Heading size="lg" gradient="accent">Accent Heading</Heading>

// Text — inline or block text with semantic color variants
<Text size="lg" variant="muted" leading="relaxed">Subtitle text</Text>
<Text size="sm" variant="error">Error message</Text>
<Text as="span" weight="semibold" variant="primary">Highlighted</Text>

// Code
<Code>npm install devign</Code>              // inline
<Code block>{`const x = 1;`}</Code>           // block / pre

// Lead — large intro paragraph
<Lead>A short description that introduces the section content.</Lead>

// Blockquote
<Blockquote>Design is not just what it looks like. Design is how it works.</Blockquote>

Heading props:

| Prop | Values | Default | | ---------- | --------------------------------------------------- | ---------- | | as | h1 h2 h3 h4 h5 h6 | h2 | | size | 4xl 3xl 2xl xl lg md | 2xl | | weight | light normal medium semibold bold black | semibold | | gradient | none primary accent cool | none | | align | left center right | left |

Text props:

| Prop | Values | Default | | ---------- | ---------------------------------------------------------------- | --------- | | as | p span div label small strong em | p | | size | xs sm md lg xl | md | | variant | default muted primary accent success warning error | default | | weight | light normal medium semibold bold | normal | | leading | tight snug normal relaxed loose | normal | | truncate | true false | false |


Spinner

import { Spinner, LoadingOverlay } from "devign";

<Spinner />                                // default medium primary
<Spinner size="lg" variant="accent" />
<Spinner size="xs" variant="white" />      // inside dark buttons

// LoadingOverlay — wraps any content
<LoadingOverlay loading={isLoading} label="Fetching data...">
  <MyDataTable />
</LoadingOverlay>

| Prop | Values | Default | | --------- | ---------------------------------------------- | --------- | | size | xs sm md lg xl | md | | variant | primary secondary accent white muted | primary |


Kbd

import { Kbd, Shortcut } from "devign";

<Kbd>⌘</Kbd>
<Kbd size="lg">Enter</Kbd>

// Shortcut renders a key combination
<Shortcut keys={["⌘", "K"]} />
<Shortcut keys={["Ctrl", "Shift", "P"]} size="sm" />

AvatarGroup

import { AvatarGroup } from "devign";

<AvatarGroup
  avatars={[
    { src: "/alice.jpg", fallback: "AL", alt: "Alice" },
    { src: "/bob.jpg", fallback: "BO", alt: "Bob" },
    { fallback: "CW", alt: "Charlie" },
    { fallback: "DM", alt: "Diana" },
    { fallback: "EK", alt: "Eve" },
    { fallback: "FP", alt: "Frank" }, // overflows → shows +1
  ]}
  max={5}
  size="md"
  spacing="normal"
/>

| Prop | Values | Default | | --------- | ------------------------ | -------- | | max | number | 5 | | size | sm md lg | md | | spacing | tight normal loose | normal |


NumberInput

import { NumberInput } from "devign";

const [qty, setQty] = useState(1);

<NumberInput value={qty} onChange={setQty} min={1} max={99} />
<NumberInput value={qty} onChange={setQty} step={5} size="lg" />
<NumberInput value={qty} onChange={setQty} error="Must be at least 1" />

| Prop | Type | Default | | ---------- | ------------------------- | ----------- | | value | number | 0 | | onChange | (value: number) => void | — | | min | number | -Infinity | | max | number | Infinity | | step | number | 1 | | size | sm md lg | md |


Layout Primitives

import { Container, Stack, Grid, Divider } from "devign";

// Container — max-width page wrapper
<Container size="xl" padded>
  <YourPageContent />
</Container>

// Stack — flex column or row with gap
<Stack direction="col" gap={6}>
  <Card>One</Card>
  <Card>Two</Card>
</Stack>

<Stack direction="row" gap={4} align="center" justify="between">
  <Logo />
  <Nav />
</Stack>

// Grid — responsive columns
<Grid cols={1} mdCols={2} lgCols={3} gap={6}>
  <Card>A</Card>
  <Card>B</Card>
  <Card>C</Card>
</Grid>

// Divider
<Divider />
<Divider label="or continue with" />
<Divider orientation="vertical" />

Stack props:

| Prop | Values | Default | | ----------- | -------------------------------------------------- | --------- | | direction | col row | col | | gap | 0 1 2 3 4 6 8 10 12 | 4 | | align | start center end stretch baseline | stretch | | justify | start center end between around evenly | start | | wrap | boolean | false |


Other components

| Component | Import | Notes | | -------------- | ----------------------------------------------------------------------------- | -------------------------------------------------- | | Accordion | AccordionItem, AccordionTrigger, AccordionContent | Radix-based, animated | | Avatar | Avatar, AvatarImage, AvatarFallback | With image fallback | | Breadcrumbs | Breadcrumbs | Accepts items: { label, href? }[] | | Checkbox | Checkbox | Controlled via checked + onCheckedChange | | DropdownMenu | DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem | Radix-based | | EmptyState | EmptyState | Props: title, description, icon, action | | Pagination | Pagination | Props: currentPage, totalPages, onPageChange | | Popover | Popover, PopoverTrigger, PopoverContent | Radix-based | | Progress | Progress | Accepts value (0–100) | | RadioGroup | RadioGroup, RadioGroupItem | Controlled via value + onValueChange | | Separator | Separator | Horizontal or vertical divider | | Switch | Switch | Controlled via checked + onCheckedChange | | Table | Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableCaption | Standard table layout | | Tooltip | TooltipProvider, Tooltip, TooltipTrigger, TooltipContent | Wrap app in TooltipProvider |


Dark Mode

Devign supports dark mode via a .dark class on the <html> element. Add or remove it to switch themes:

// Toggle dark mode
document.documentElement.classList.toggle("dark");

A simple React hook to manage this:

function useDarkMode() {
  const [isDark, setIsDark] = useState(() =>
    document.documentElement.classList.contains("dark"),
  );

  const toggle = () => {
    document.documentElement.classList.toggle("dark");
    setIsDark((prev) => !prev);
    localStorage.setItem("theme", isDark ? "light" : "dark");
  };

  // Restore on mount
  useEffect(() => {
    const saved = localStorage.getItem("theme");
    const prefersDark = window.matchMedia(
      "(prefers-color-scheme: dark)",
    ).matches;
    if (saved === "dark" || (!saved && prefersDark)) {
      document.documentElement.classList.add("dark");
      setIsDark(true);
    }
  }, []);

  return { isDark, toggle };
}

// Usage
function Header() {
  const { isDark, toggle } = useDarkMode();
  return (
    <Button variant="ghost" size="icon" onClick={toggle}>
      {isDark ? <Sun /> : <Moon />}
    </Button>
  );
}

Dark mode overrides the glass variables and all semantic tokens automatically — no extra setup needed.


TypeScript

All components are fully typed. Props interfaces are exported:

import type {
  ButtonProps,
  CardProps,
  InputProps,
  BadgeProps,
  StatCardProps,
  EmptyStateProps,
  PaginationProps,
  SkeletonProps,
} from "devign";

Framework notes

| Framework | Notes | | ------------------------ | --------------------------------------------------------------------------------- | | Vite | Full support. See setup above. | | Next.js App Router | Add "use client" to files using hooks or event handlers. CSS setup is the same. | | Next.js Pages Router | Import styles in _app.tsx. Works without "use client". | | Remix | Import styles in root.tsx links export. | | Astro | Use inside .tsx components with client:load or client:visible. |


Fonts

Devign declares font tokens but does not load fonts itself — you choose the method that fits your project.

Option A — Google Fonts

Add to your index.html <head>:

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;0,900;1,400&display=swap"
  rel="stylesheet"
/>

Option B — Fontsource (self-hosted, no CDN)

npm install @fontsource/poppins

Then in your app entry file:

import "@fontsource/poppins/400.css";
import "@fontsource/poppins/500.css";
import "@fontsource/poppins/600.css";
import "@fontsource/poppins/700.css";

Option C — Bring your own font

Override the CSS token in your stylesheet (after importing Devign styles):

:root {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "Fira Code", ui-monospace, monospace;
}

Live Theme Builder

The live demo includes an interactive Theme Builder panel — click the Customize button in the bottom-right corner to:

  • Pick from preset themes (Ocean, Forest, Rose, Slate, Sharp, Pill, Snappy)
  • Adjust primary and accent colors with a color picker
  • Tune border radius, glass blur, and animation speed with sliders
  • Copy the generated CSS to paste directly into your project

Animated Icons with itshover

Devign pairs well with itshover.com — a library of 186+ animated React icon components built on motion/react (which Devign already includes).

Install an icon

npx shadcn@latest add https://itshover.com/r/heart.json

Use with Devign components

import HeartIcon from "@/icons/heart-icon";
import { Button, Card, CardContent, EmptyState } from "devign";

// Animated icon in a button
<Button leftIcon={<HeartIcon className="h-4 w-4" />}>Like</Button>

// Animated icon in an empty state
<EmptyState
  icon={<RocketIcon className="h-12 w-12" />}
  title="No projects yet"
  description="Create your first project to get started."
  action={{ label: "Create Project", onClick: () => {} }}
/>

// Animated icon in a card
<Card hover>
  <CardContent className="flex items-center gap-3 p-4">
    <SparklesIcon className="h-5 w-5 text-accent" />
    <span>Featured item</span>
  </CardContent>
</Card>

Browse all available icons at itshover.com/icons.


Migration from yems-ui

Devign is the successor to yems-ui. The API is 100% backward compatible — all component names, props, and exports are identical.

Steps to migrate

  1. Swap the package:
npm uninstall yems-ui
npm install devign
  1. Find and replace imports in your codebase:
- import { Button, Card } from "yems-ui";
+ import { Button, Card } from "devign";
  1. Update your CSS:
- @source "../node_modules/yems-ui/dist";
- @import "yems-ui/styles.css";
+ @source "../node_modules/devign/dist";
+ @import "devign/styles.css";

That's it. No component changes, no prop changes, no breaking changes.


License

MIT © Yemi Ogundairo