Introduction

GWAN is a high-performance component library and design system built for Next.js projects. It provides a curated set of production-ready components, a fully typed API, and a Tailwind CSS v4 theming layer — all with first-class dark mode support baked in.

GWAN is designed with a plug-and-play philosophy. Drop it into any Next.js project, configure your tokens once, and every component automatically adapts to your brand and colour scheme.

49+

Components

191+

Icons

v1.0.6

Version

MIT

License

Installation

Install the GWAN package and its peer dependencies into your existing Next.js project.

1. Install via npm

bash
npm install gwan-design-system

2. Install peer dependencies

GWAN requires React 19, Tailwind CSS v4, and next-themes for dark mode support.

bash
npm install tailwindcss@^4 @tailwindcss/postcss next-themes
NoteGWAN targets Next.js 15+ and React 19. If you are on an older version, check the compatibility table in the repository.

Tailwind Setup

GWAN uses Tailwind CSS v4 with the new @import "tailwindcss" syntax and the @theme directive. There is no tailwind.config.js required — all tokens live in your CSS file.

1. Configure PostCSS

Create or update postcss.config.mjs at the root of your project.

js
// postcss.config.mjs
const config = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

export default config;

2. Configure content paths

Create tailwind.config.ts and point it at your source files.

ts
// tailwind.config.ts
const config = {
  content: [
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/templates/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  darkMode: "class",
  theme: {},
  plugins: [],
};

export default config;

3. Import Tailwind in globals.css

css
/* src/app/globals.css */
@import "tailwindcss";

/* Your theme tokens go here — see Theme Setup below */

Theme Setup

GWAN's theming system is built entirely on CSS custom properties and Tailwind's @theme directive. You define your colour tokens once in globals.css — both a light and dark variant — and every component reads from those tokens automatically.

1. Define your tokens

Add the following to your globals.css. Customise the hex values to match your brand.

css
/* ─── Light theme ─────────────────────── */
:root {
  --background:      #f8f9f5;
  --foreground:      #1c2218;

  --surface:         #ffffff;
  --surface-raised:  #eef2e8;
  --surface-overlay: #e3e9da;

  --border:          #c4cdb8;
  --muted-fg:        #60705a;

  --primary-default: #435240;
  --accent:          #0f766e;

  --success:         #059669;
  --danger:          #e11d48;
  --warning:         #d97706;
}

/* ─── Dark theme ──────────────────────── */
.dark {
  --background:      #131a10;
  --foreground:      #e2eada;

  --surface:         #1c241a;
  --surface-raised:  #252e22;
  --surface-overlay: #2d392a;

  --border:          #3c4d38;
  --muted-fg:        #9ea593;

  --primary-default: #adc09e;
  --accent:          #2dd4bf;

  --success:         #34d399;
  --danger:          #fb7185;
  --warning:         #fbbf24;
}

2. Register tokens with Tailwind

Use the @theme directive to expose every CSS variable as a Tailwind utility class.

css
@theme {
  --color-background:     var(--background);
  --color-foreground:     var(--foreground);
  --color-surface:        var(--surface);
  --color-surface-raised: var(--surface-raised);
  --color-border:         var(--border);
  --color-muted-fg:       var(--muted-fg);
  --color-accent:         var(--accent);
  --color-success:        var(--success);
  --color-danger:         var(--danger);
  --color-warning:        var(--warning);

  /* Primary scale */
  --color-primary-50:  #f4f7f1;
  --color-primary-100: #e2eada;
  --color-primary-500: #9ea593;
  --color-primary-800: #435240;
  --color-primary-900: #2c3629;
}
TipOnce registered in @theme, use these as normal Tailwind utilities — bg-background, text-foreground, border-border, bg-primary-500, etc.

Dark Mode

GWAN uses next-themes to manage dark mode. The library toggles a .dark class on the <html> element, which switches the CSS custom properties you defined above.

1. Wrap your app with ThemeProvider

tsx
// src/app/layout.tsx
import { ThemeProvider } from "next-themes";

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

2. Add a theme toggle

Use the useTheme hook and guard against hydration mismatch with a mounted check.

tsx
"use client";

import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);

  useEffect(() => { setMounted(true); }, []);
  if (!mounted) return null;

  return (
    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      {theme === "dark" ? "Light mode" : "Dark mode"}
    </button>
  );
}
WarningAlways check mounted before rendering theme-dependent UI. The server renders without knowing the user's preference, so reading theme before mount will produce a hydration mismatch.

Using Components

Every GWAN component is a typed React functional component. Import directly from the package and pass the required props. Each component exposes an enum for its variant or type prop to ensure type safety.

Button

tsx
import Button, { BUTTON_VARIANTS } from "gwan-design-system/button";

<Button
  variant={BUTTON_VARIANTS.PRIMARY}
  label="Save Changes"
  onClick={() => console.log("clicked")}
/>

<Button
  variant={BUTTON_VARIANTS.SECONDARY}
  label="Cancel"
  onClick={() => {}}
  disabled
/>

Tag

tsx
import Tag, { TAG_TYPE } from "gwan-design-system/tag";

<Tag type={TAG_TYPE.SUCCESS} label="Completed" />
<Tag type={TAG_TYPE.DANGER}  label="Failed" />
<Tag type={TAG_TYPE.WARNING} label="Pending" />
<Tag type={TAG_TYPE.INFO}    label="In Transit" />

Avatar

tsx
import Avatar, { AVATAR_VARIANT } from "gwan-design-system/avatar";

// With image
<Avatar
  name="Alex Chon"
  email="alex@gwan.dev"
  image="/images/profile.png"
  variant={AVATAR_VARIANT.IMAGE_WITH_FULL}
/>

// Initials only
<Avatar
  name="Sam Rivera"
  email="sam@gwan.dev"
  variant={AVATAR_VARIANT.INITIALS_ONLY}
/>

Input

tsx
import Input from "gwan-design-system/input";

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

<Input
  label="Email address"
  type="email"
  value={value}
  placeholder="you@example.com"
  onChange={(e) => setValue(e.target.value)}
  onClear={value ? () => setValue("") : undefined}
/>

Snackbar

tsx
import Snackbar, { SNACK_BAR_TYPE } from "gwan-design-system/snackBar";
import { CheckSVG } from "gwan-design-system/icons";

<Snackbar
  type={SNACK_BAR_TYPE.SUCCESS}
  message="Profile updated successfully"
  icon={<CheckSVG />}
/>

Plug & Play

The real power of GWAN is how quickly you can assemble full UI surfaces. Below is a complete user profile card built entirely from GWAN components — no custom styles required.

tsx
"use client";

import { useState } from "react";
import Button, { BUTTON_VARIANTS } from "gwan-design-system/button";
import Avatar, { AVATAR_VARIANT } from "gwan-design-system/avatar";
import Tag, { TAG_TYPE }           from "gwan-design-system/tag";
import Input                        from "gwan-design-system/input";
import Snackbar, { SNACK_BAR_TYPE } from "gwan-design-system/snackBar";
import { CheckSVG }                 from "gwan-design-system/icons";

export default function ProfileCard() {
  const [name, setName]   = useState("Alex Chon");
  const [saved, setSaved] = useState(false);

  const handleSave = () => {
    setSaved(true);
    setTimeout(() => setSaved(false), 3000);
  };

  return (
    <div className="bg-surface border border-border rounded-2xl p-6 flex flex-col gap-6 max-w-sm">
      <Avatar
        name={name}
        email="alex@gwan.dev"
        variant={AVATAR_VARIANT.INITIALS_WITH_FULL}
      />

      <div className="flex gap-2">
        <Tag type={TAG_TYPE.SUCCESS} label="Active" />
        <Tag type={TAG_TYPE.INFO}    label="Admin" />
      </div>

      <Input
        label="Display name"
        value={name}
        onChange={(e) => setName(e.target.value)}
        onClear={name ? () => setName("") : undefined}
      />

      <Button
        variant={BUTTON_VARIANTS.PRIMARY}
        label="Save Changes"
        icon={<CheckSVG />}
        onClick={handleSave}
      />

      {saved && (
        <Snackbar
          type={SNACK_BAR_TYPE.SUCCESS}
          message="Profile saved successfully"
          icon={<CheckSVG />}
        />
      )}
    </div>
  );
}

That is the full component. No utility class overrides, no theme wiring — it picks up light and dark mode automatically from the CSS variables you set up once.

Client vs Server Components

Most GWAN components are pure presentational components and work in both server and client contexts. A small number — those that use useState, useEffect, or browser APIs — must be used inside a "use client" boundary.

ComponentServer SafeReason
Accordion✗ ClientUses useState for open/close state per item
Alert✗ ClientUses useState for dismiss state
Avatar✗ ClientUses useState + useEffect for pastel colour generation
AvatarGroup✗ ClientUses useState for overflow tooltip visibility
Badge✓ YesPure presentational
Banner✓ YesPure presentational
Breadcrumb✓ YesPure presentational
Button✓ YesAccepts onClick as a prop — no internal hooks
Callout✓ YesPure presentational
Card✓ YesPure presentational
Carousel✗ ClientUses useState + useEffect for slide index and auto-play
Checkbox✓ YesControlled — accepts onChange as a prop, no internal hooks
Chip✓ YesAccepts onClick as a prop — no internal hooks
CircularProgress✓ YesPure presentational — SVG ring driven by value prop
ColorPicker✗ ClientMarked use client — uses useState for colour state
CommandPalette✗ ClientMarked use client — uses useState + keyboard listeners
ContextMenu✗ ClientUses useState + useRef for cursor position and click-outside detection
DatePicker✗ ClientMarked use client — uses useState for calendar state
Divider✓ YesPure presentational
Drawer✗ ClientUses useEffect to lock body scroll
DropdownMenu✗ ClientUses useState + useRef for dropdown state and click-outside detection
Ellipsis✗ ClientUses useState for tooltip visibility
FileUploader✗ ClientUses useRef to trigger the hidden file input
FilterDropdown✗ ClientUses useState for dropdown visibility
Icons✓ YesPure SVG components — no hooks
Input✓ YesControlled — accepts onChange as a prop, no internal hooks
MenuBar✗ ClientUses useState + ResizeObserver for overflow and mobile menu
Modal✗ ClientUses useEffect + useRef for focus trap and scroll lock
MultiSelect✗ ClientUses useState + useRef for dropdown state, search, and keyboard navigation
NavBar✗ ClientUses useState + useEffect for collapse, tooltips, active item
Pagination✗ ClientUses useState for page-size dropdown
Popover✗ ClientMarked use client — uses useState + click-outside detection
ProgressBar✓ YesPure presentational
RadioButton✓ YesControlled — accepts onChange as a prop, no internal hooks
SearchInput✗ ClientUses useCallback + useEffect + useRef for debounced search
SelectDropdown✗ ClientUses useState + useEffect + useRef for dropdown state
Skeleton✓ YesPure presentational
Snackbar✓ YesPure presentational — visibility controlled by consumer
Spinner✓ YesPure presentational — CSS animation only
StatCard✓ YesPure presentational
States✓ YesPure presentational
Stepper✓ YesPure presentational — step state controlled by consumer
Switch✓ YesControlled — accepts onChange as a prop, no internal hooks
Table✓ YesPure presentational — data and sorting controlled by consumer
Tabs✓ YesControlled — accepts activeTab + onChange as props, no internal hooks
Tag✓ YesPure presentational
TextArea✓ YesControlled — accepts onChange as a prop, no internal hooks
TimeLine✗ ClientUses useState + useEffect for animated step progression
Tooltip✓ YesPure presentational — visibility controlled by consumer
TipWhen importing client-only components inside a Server Component page, wrap them in their own file marked "use client" rather than marking the entire page client-side.

Design Tokens

All tokens are CSS custom properties defined in :root and .dark. Overriding a token changes every component that references it — there is no cascading specificity to manage.

Semantic tokens

TokenLightDarkUsage
--background#f8f9f5#131a10Page background
--foreground#1c2218#e2eadaPrimary text
--surface#ffffff#1c241aCards, panels
--surface-raised#eef2e8#252e22Elevated surfaces
--border#c4cdb8#3c4d38All borders
--muted-fg#60705a#9ea593Secondary text
--accent#0f766e#2dd4bfLinks, highlights
--success#059669#34d399Success states
--danger#e11d48#fb7185Error states
--warning#d97706#fbbf24Warning states

Primary Colour Scale

The three semantic primary tokens (--primary-default, --primary-default-fg, --primary-muted) control the main brand colour at a high level. However, many components also reference fixed steps on the primary palette — primary-100 through primary-900 — for interactive states such as hover, focus rings, disabled backgrounds, and active fills.

These scale steps are registered once in @theme {} and do not change between light and dark mode. If you rebrand to a different hue, you must replace the entire scale so that button hover states, switch tracks, checkbox fills, stepper indicators, tabs, and progress bars all match your brand.

Components that use the primary scale

ComponentScale steps used
Button100, 200, 300 (secondary); 500, 600, 700 (primary)
Tabs500 (active underline / pill)
Stepper500 (active & complete step, connector)
Switch500 (active track)
Progress Bar500 (default fill)
Radio Button500 (checked ring + fill)
Checkbox300, 400 (checked state)
Date Picker500 (selected date, today ring)

Replacing the scale when rebranding

The Theme Generator includes a built-in scale builder — pick a seed colour and it auto-generates the full 10-step scale. Copy the output directly into your globals.css. To override manually, replace the values below.

css
/* In globals.css — replace ALL steps to match your brand hue */
@theme {
  --color-primary-50:  #f4f7f1;  /* lightest tint  */
  --color-primary-100: #e2eada;
  --color-primary-200: #c8d5be;
  --color-primary-300: #adc09e;
  --color-primary-400: #a4b496;
  --color-primary-500: #9ea593;  /* mid-point      */
  --color-primary-600: #7e8c73;
  --color-primary-700: #60705a;
  --color-primary-800: #435240;
  --color-primary-900: #2c3629;  /* darkest shade  */
}
TipAlways replace all 10 steps together. Using only a few steps from a different hue creates inconsistent hover and disabled states across components. Use the Theme Generator to generate a matched scale from a single seed colour.

Extending Themes

To rebrand GWAN for your project, override the CSS custom properties in your globals.css. You do not need to fork or patch the library — the components read whatever values you define.

Example: rebrand to an indigo palette

css
/* Override in your globals.css — after @import "tailwindcss" */
:root {
  --background:      #f5f5ff;
  --foreground:      #1a1a3e;
  --surface:         #ffffff;
  --surface-raised:  #ececff;
  --border:          #c7c7e8;
  --muted-fg:        #6464a0;
  --primary-default: #4f46e5; /* indigo-600 */
  --accent:          #7c3aed; /* violet-600 */
}

.dark {
  --background:      #0f0f1a;
  --foreground:      #e0e0ff;
  --surface:         #16162a;
  --surface-raised:  #1e1e38;
  --border:          #2e2e60;
  --muted-fg:        #8080c0;
  --primary-default: #818cf8; /* indigo-400 */
  --accent:          #a78bfa; /* violet-400 */
}

@theme {
  --color-primary-500: #6366f1; /* your brand base */
}
TipYou only need to override the tokens you want to change. Any tokens you leave out will fall back to the GWAN defaults.

Adding custom component styles

Every component accepts a className prop for one-off overrides without touching the source.

tsx
<Button
  variant={BUTTON_VARIANTS.PRIMARY}
  label="Wide CTA"
  onClick={() => {}}
  className="w-full py-4 text-lg"
/>

Versioning

GWAN follows Semantic Versioning (semver). The version is automatically injected at build time via the NEXT_PUBLIC_APP_VERSION environment variable, which reads from the latest Git tag.

bash
# Tag a new release
git tag v0.2.0
git push origin v0.2.0

# The build script picks it up automatically:
# "prebuild": "echo NEXT_PUBLIC_APP_VERSION=$(git describe --tags --abbrev=0) > .env.local"

You can display the current version in your UI anywhere:

tsx
<span className="text-muted-fg text-xs">
  {process.env.NEXT_PUBLIC_APP_VERSION}
</span>
NoteBreaking changes are communicated via a major version bump. Minor versions add new components or tokens. Patch versions are bug fixes and documentation updates.

© 2026 GWAN.DEV