Aici Design System — V.1.0
Consistent, accessible
interfaces built at scale.
A customised shadcn/ui component library built on Radix UI. Neutral palette, Inter typography, zero extra dependencies.
Installation
Create project
TypeScript, Tailwind CSS, App Router, no src/ directory, @/* import alias.
pnpm create next-app@latest my-app \
--typescript --tailwind --app --src-dir no --import-alias "@/*"
cd my-appAdd Aici Design System
Run from inside your project directory. Copies all components, injects design tokens into globals.css, and installs dependencies — automatically.
npx @aici/ui .Same command to update — re-runs cleanly without touching your own code.
Add Inter font in layout.tsx
Load Inter via next/font/google and attach it as --font-inter CSS variable on <html>.
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
display: "swap",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
);
}Import components and build
All components live in @/components/ui/. Dark mode is toggled by adding or removing the "dark" class on <html>.
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";Logo & Icons
App Icon (preset: app="appName")
Props: app | iconColor, backgroundColor, borderColor, size
Usage
import AppIcon, { APP_PRESETS, type AppPresetName } from "@/components/ui/app-icon";
// With preset (iconColor, backgroundColor applied automatically, no border)
<AppIcon app="Education" size={40} />
<AppIcon app="Site" size={32} />
<AppIcon app="InUse" size={56} />
// Manual colors, no border
<AppIcon iconColor="#262626" backgroundColor="#9C9C9C" size={40} />
// Manual colors + border (1px when borderColor is set)
<AppIcon iconColor="#262626" backgroundColor="#E8E8E8" borderColor="#D5B56E" size={40} />
// Bordered only (transparent bg, icon + border #D5B56E)
<AppIcon iconColor="#D5B56E" backgroundColor="transparent" borderColor="#D5B56E" size={40} />A Icon (standalone)
Default 18×22px at size 40; scales proportionally. Props: size, color.
import AIcon from "@/components/ui/a-icon";
// Default (size 40 = 18×22px)
<AIcon />
// Custom size (scales proportionally: 60 → 27×33)
<AIcon size={40} />
<AIcon size={60} />
// Custom color
<AIcon color="#262626" />
<AIcon color="#D5B56E" size={60} />AICI Logo
Default 90×15px; scales proportionally. Props: size, color.
import AiciLogo from "@/components/ui/aici-logo";
// Default 90×15
<AiciLogo />
// Custom size (scales proportionally)
<AiciLogo size={90} />
<AiciLogo size={120} />
// Custom color
<AiciLogo color="#262626" />
<AiciLogo size={60} color="#D5B56E" />Chevron Icons
Props: size, stroke (default #262626), strokeWidth (default 0.5).
Sizes · #262626
import { Chevron } from "@/components/ui/chevron-icons";
// or individual: LeftChevron, RightChevron, UpChevron, DownChevron
// Unified component — variant: "left" | "right" | "up" | "down"
<Chevron variant="left" />
<Chevron variant="right" />
<Chevron variant="up" />
<Chevron variant="down" />
// Props: variant, size, stroke (default #262626), strokeWidth (default 0.5)
<Chevron variant="left" size={16} stroke="#262626" />
<Chevron variant="down" size={20} stroke="#9c9c9c" strokeWidth={1} />Colors
Foreground
Muted
Background
White
Typography
Heading
15 / 700
Subheading
15 / 500
Body
14 / 300
Label
12 / 500
Caption
10 / 300
Label Uppercase
12 / 500 + 0.15em
Caption Uppercase
10 / 300 + 0.15em
Micro Uppercase
8 / 300 + 0.15em
Theme
Copy tailwind.config.js to your project root.
/* globals.css — Tailwind v4 (CSS-only config) */
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@layer base {
:root {
/* Brand palette (static) */
--brand: #262626; --brand-light: #f7f7f7;
--brand-muted: #9c9c9c; --gold: #d5b56e;
/* Semantic — light */
--background: #f7f7f7; --foreground: #262626;
--primary: #262626; --primary-foreground: #f7f7f7;
--secondary: #f7f7f7; --secondary-foreground: #262626;
--muted: oklch(0.94 0 0); --muted-foreground: #9c9c9c;
--accent: #9c9c9c; --accent-foreground: #262626;
--destructive: oklch(0.58 0.22 27);
--border: oklch(0.88 0 0); --input: oklch(0.88 0 0);
--ring: #9c9c9c; --radius: 0.5rem;
}
html.dark {
/* Semantic — dark */
--background: #262626; --foreground: #f7f7f7;
--primary: #f7f7f7; --primary-foreground: #262626;
--secondary: #262626; --secondary-foreground: #f7f7f7;
--muted: oklch(0.24 0 0);
--border: oklch(1 0 0 / 12%); --input: oklch(1 0 0 / 12%);
}
}
@theme inline {
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
/* Font sizes */
--text-2xs: 0.5rem; --text-3xs: 0.5625rem;
--text-4xs: 0.625rem; --text-11: 0.6875rem;
/* Tracking */
--tracking-tight: -0.02em; --tracking-ui: 0.06em;
--tracking-label: 0.08em; --tracking-nav: 0.12em;
--tracking-caps: 0.15em; --tracking-hero: 0.22em;
/* Spacing */
--spacing-1\.25: 0.3125rem; /* 5px */
/* Radius */
--radius-none: 0; --radius-sm: 0.3125rem;
--radius: 0.5rem; --radius-full: 9999px;
/* Brand */
--color-brand: var(--brand);
--color-brand-light: var(--brand-light);
--color-brand-muted: var(--brand-muted);
--color-gold: var(--gold);
/* Semantic */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
@layer utilities {
.interactive {
opacity: 0.8; transition: opacity 150ms ease;
}
.interactive:hover, .interactive:active { opacity: 1; }
.interactive:disabled { opacity: 0.4; cursor: not-allowed; }
}
Inputs
import { Input } from "@/components/ui/input";
<Input placeholder="Full name" />
<Input type="email" placeholder="Email" />
<Input variant="borderless" placeholder="No border" />
<Input disabled placeholder="Disabled" />import { Textarea } from "@/components/ui/textarea";
<Textarea placeholder="Write your message..." rows={3} />
<Textarea variant="borderless" placeholder="No border" rows={2} />import Select from "@/components/ui/select";
const [value, setValue] = useState("design");
<Select
options={["design", "engineering", "product"]}
value={value}
onChange={setValue}
/>import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
const [checked, setChecked] = useState(false);
<div className="flex items-center gap-2.5">
<Checkbox
checked={checked}
onCheckedChange={(v) => setChecked(v === true)}
/>
<Label>Subscribe to newsletter</Label>
</div>import RoundedInput from "@/components/ui/rounded-input";
<RoundedInput placeholder="Email or username" />
<RoundedInput
type="pwd"
placeholder="Password"
showLabel="Show"
hideLabel="Hide"
/>