Card Title
Brief description of the content.
Design System
A minimal, token-based design system I created for building clean, accessible interfaces. Yes, I created a design system and use it for my portfolio, because Design Systems is what I do. (The system is actively maintained and expanded. A standalone public release is planned.)
Peraa is a lightweight design system built with vanilla CSS custom properties. It provides a comprehensive token system and reusable components for building consistent, accessible user interfaces. There is also a React and Tailwind CSS implementation.
Peraa is available in three formats. Choose the one that fits your project:
Vanilla implementation with CSS custom properties. Zero dependencies.
React components with props, TypeScript support, and composable patterns.
Tailwind config preset with utility classes and component styles.
<!-- Include the stylesheet -->
<link rel="stylesheet" href="css/main.css">
<!-- Include the JavaScript (for theme toggle, tabs) -->
<script src="js/main.js"></script>
// Import Peraa styles in your app entry
import '@peraa/css/main.css';
// Import components
import { Button, Badge, Card, Tabs } from '@peraa/react';
// Use in your components
function App() {
return (
<Button intent="primary">Click me</Button>
);
}
// tailwind.config.js
const peraaConfig = require('@peraa/tailwind/tailwind.config');
module.exports = {
presets: [peraaConfig],
content: ['./src/**/*.{html,js,jsx,ts,tsx}'],
// your customizations...
};
// In your CSS
@import '@peraa/tailwind/components.css';
Peraa uses a two-tier token system:
Peraa supports light and dark modes out of the box. The theme automatically respects the user's system preference and can be toggled manually.
// Toggle theme programmatically
window.Peraa.ThemeManager.toggle();
// Set specific theme
window.Peraa.ThemeManager.setTheme('dark');
window.Peraa.ThemeManager.setTheme('light');
Primary, secondary, and tertiary button styles with multiple sizes.
Horizontal tab navigation with keyboard support.
Vertical sticky timeline for case study pages.
Content cards with image, title, and description.
Tags and skill badges for labeling content.
Clickable images with hover effects.
Buttons trigger actions and events. Peraa provides three button intents for different levels of emphasis: primary, secondary, and tertiary.
<button class="peraa-btn peraa-btn--primary">
Primary
</button>
<button class="peraa-btn peraa-btn--secondary">
Secondary
</button>
<button class="peraa-btn peraa-btn--tertiary">
Tertiary
</button>
import { Button } from '@peraa/react';
<Button intent="primary">Primary</Button>
<Button intent="secondary">Secondary</Button>
<Button intent="tertiary">Tertiary</Button>
// With additional props
<Button intent="primary" size="lg">
Large Button
</Button>
<Button intent="primary" disabled>
Disabled
</Button>
<button class="peraa-btn-primary">
Primary
</button>
<button class="peraa-btn-secondary">
Secondary
</button>
<button class="peraa-btn-tertiary">
Tertiary
</button>
<!-- With sizes -->
<button class="peraa-btn-primary peraa-btn-sm">Small</button>
<button class="peraa-btn-primary peraa-btn-lg">Large</button>
| Class | Description | Required |
|---|---|---|
.peraa-btn |
Base button styles | Yes |
.peraa-btn--primary |
High emphasis, solid background | No |
.peraa-btn--secondary |
Medium emphasis, outlined | No |
.peraa-btn--tertiary |
Low emphasis, text only | No |
.peraa-btn--sm |
Small size variant | No |
.peraa-btn--lg |
Large size variant | No |
.peraa-btn--icon-only |
Square button for icons | No |
.peraa-btn--full |
Full-width button | No |
| Semantic Token | Primitive | Value | Description |
|---|---|---|---|
--peraa-interactive-primary-bg |
--peraa-color-purple-500 |
#6F1FAC | Primary button background |
--peraa-interactive-primary-bg-hover |
--peraa-color-purple-600 |
#5c1a8f | Primary button hover |
--peraa-interactive-primary-text |
--peraa-color-neutral-0 |
#ffffff | Primary button text |
--peraa-interactive-secondary-border |
--peraa-color-purple-500 |
#6F1FAC | Secondary button border |
--peraa-interactive-secondary-text |
--peraa-color-purple-500 |
#6F1FAC | Secondary button text |
All design tokens used in the Peraa design system. Tokens are organized into two layers: primitives (raw values — colors, sizes, timing) and semantic tokens (contextual mappings — always prefer these in components).
Dark mode is the default
| Token | Value | Preview | Description |
|---|---|---|---|
--peraa-color-purple-50 |
#f5e9fc | Lightest purple | |
--peraa-color-purple-100 |
#e5c7f7 | Very light purple | |
--peraa-color-purple-200 |
#d3a1f1 | Light purple | |
--peraa-color-purple-300 |
#bf79eb | Medium light purple | |
--peraa-color-purple-400 |
#ab51e5 | Medium purple | |
--peraa-color-purple-500 |
#6F1FAC | Primary brand color | |
--peraa-color-purple-600 |
#5c1a8f | Dark purple | |
--peraa-color-purple-700 |
#491572 | Darker purple | |
--peraa-color-purple-800 |
#361055 | Very dark purple | |
--peraa-color-purple-900 |
#230b38 | Darkest purple |
| Token | Value | Preview | Description |
|---|---|---|---|
--peraa-color-neutral-0 |
#ffffff | White | |
--peraa-color-neutral-50 |
#fafafa | Off-white | |
--peraa-color-neutral-100 |
#f4f4f5 | Light gray | |
--peraa-color-neutral-200 |
#e4e4e7 | Border gray | |
--peraa-color-neutral-300 |
#d4d4d8 | Medium light gray | |
--peraa-color-neutral-400 |
#a1a1aa | Disabled text | |
--peraa-color-neutral-500 |
#71717a | Tertiary text | |
--peraa-color-neutral-600 |
#52525b | Secondary text | |
--peraa-color-neutral-700 |
#3f3f46 | Dark gray | |
--peraa-color-neutral-800 |
#27272a | Very dark gray | |
--peraa-color-neutral-900 |
#18181b | Near black | |
--peraa-color-neutral-950 |
#09090b | Darkest |
The secondary brand palette. Used for ambient glows, interactive hover states, card accent borders, and CTAs that need to read as "active" without purple. The 400–600 range is the primary active zone.
| Token | Value | Preview | Usage |
|---|---|---|---|
--peraa-color-teal-50 | #f0fdfa | Lightest tint — alert backgrounds | |
--peraa-color-teal-100 | #ccfbf1 | Success backgrounds | |
--peraa-color-teal-200 | #99f6e4 | Hover fills (light mode) | |
--peraa-color-teal-300 | #5eead4 | Subtle accents | |
--peraa-color-teal-400 | #2dd4bf | Active accents in dark mode | |
--peraa-color-teal-500 | #14b8a6 | Brand accent — glow base, card hover borders | |
--peraa-color-teal-600 | #0d9488 | Pressed / active state | |
--peraa-color-teal-700 | #0f766e | Dark variant fills | |
--peraa-color-teal-800 | #115e59 | Deep background tints | |
--peraa-color-teal-900 | #134e4a | Darkest tint | |
--peraa-color-teal-950 | #042f2e | Near-black for overlays |
Semantic tokens are the layer between raw color values and your UI. Always prefer semantic tokens over primitives — they automatically respond to the active theme (light or dark) so components remain consistent without per-component media queries.
| Token | Light value | Dark value | Description |
|---|---|---|---|
--peraa-surface-page | #fafafa | #09090b | Main page background. Dark mode carries ambient teal glow as background-image layers. |
--peraa-surface-card | #ffffff | #18181b | Card / panel background |
--peraa-surface-elevated | #ffffff | #27272a | Dropdowns, tooltips, modals — above card level |
--peraa-surface-subtle | #f4f4f5 | #3f3f46 | Muted fills, hover states, tag backgrounds |
--peraa-surface-overlay | rgba(0,0,0,0.4) | rgba(0,0,0,0.65) | Modal / drawer scrim overlay |
| Token | Light value | Dark value | Description |
|---|---|---|---|
--peraa-text-primary | #09090b | #fafafa | Body text, headings |
--peraa-text-secondary | #71717a | #a1a1aa | Descriptions, subheadings |
--peraa-text-tertiary | #a1a1aa | #52525b | Placeholders, hints, disabled labels |
--peraa-text-disabled | #d4d4d8 | #3f3f46 | Fully disabled state text |
--peraa-text-brand | #6F1FAC | #ab51e5 | Overlines, active nav links, brand callouts |
--peraa-text-inverse | #fafafa | #09090b | Text on filled/inverted surfaces |
| Token | Light value | Dark value | Description |
|---|---|---|---|
--peraa-border-default | #e4e4e7 | #27272a | Default dividers, card borders |
--peraa-border-subtle | #f4f4f5 | #18181b | Very light separation |
--peraa-border-strong | #a1a1aa | #52525b | Emphasis borders |
--peraa-border-brand | #6F1FAC | #ab51e5 | Brand-colored border |
--peraa-border-focus | #6F1FAC | #ab51e5 | Keyboard focus ring |
| Token | Light value | Dark value | Usage |
|---|---|---|---|
--peraa-interactive-primary-bg | #6F1FAC | #6F1FAC | Primary button fill |
--peraa-interactive-primary-bg-hover | #5c1a8f | #ab51e5 | Primary button hover |
--peraa-interactive-primary-text | #ffffff | #ffffff | Text on primary button |
--peraa-interactive-secondary-bg | transparent | transparent | Secondary button fill |
--peraa-interactive-secondary-border | #6F1FAC | #ab51e5 | Secondary button border |
--peraa-interactive-tertiary-text | #6F1FAC | #ab51e5 | Tertiary / text-only button |
| Token | Value | Pixels | Description |
|---|---|---|---|
--peraa-spacing-1 | 0.25rem | 4px | Extra small |
--peraa-spacing-1-5 | 0.375rem | 6px | Form gap, tight inline spacing |
--peraa-spacing-2 | 0.5rem | 8px | Small |
--peraa-spacing-2-5 | 0.625rem | 10px | Input padding vertical |
--peraa-spacing-3 | 0.75rem | 12px | Medium small |
--peraa-spacing-4 | 1rem | 16px | Base |
--peraa-spacing-5 | 1.25rem | 20px | Medium |
--peraa-spacing-6 | 1.5rem | 24px | Large |
--peraa-spacing-8 | 2rem | 32px | Extra large |
--peraa-spacing-10 | 2.5rem | 40px | 2X large |
--peraa-spacing-12 | 3rem | 48px | 3X large |
--peraa-spacing-16 | 4rem | 64px | 4X large |
--peraa-spacing-20 | 5rem | 80px | 5X large |
--peraa-spacing-24 | 6rem | 96px | 6X large |
| Token | Value | Description |
|---|---|---|
--peraa-font-family-sans | 'Outfit', sans-serif | Primary UI font — all headings and body |
--peraa-font-family-mono | 'JetBrains Mono', monospace | Code blocks, tokens, technical labels |
--peraa-font-size-xs | 0.75rem (12px) | Captions, badges, fine print |
--peraa-font-size-sm | 0.875rem (14px) | Small body, table cells |
--peraa-font-size-base | 1rem (16px) | Default body text |
--peraa-font-size-lg | 1.125rem (18px) | Lead / intro copy |
--peraa-font-size-xl | 1.25rem (20px) | Section subheadings |
--peraa-font-size-2xl | 1.5rem (24px) | .peraa-heading-4 |
--peraa-font-size-3xl | 1.875rem (30px) | .peraa-heading-3 |
--peraa-font-size-4xl | 2.25rem (36px) | .peraa-heading-2 |
--peraa-font-size-5xl | 3rem (48px) | .peraa-heading-1 |
--peraa-font-size-6xl | 3.75rem (60px) | Hero / display headings |
--peraa-font-weight-regular | 400 | Body text, descriptions |
--peraa-font-weight-medium | 500 | h3–h6, UI labels, overlines |
--peraa-font-weight-semibold | 600 | h1–h2 (Direction A editorial style) |
--peraa-font-weight-bold | 700 | Reserved — use sparingly for max emphasis |
| Token | Value | Preview | Description |
|---|---|---|---|
--peraa-radius-none |
0 | No radius | |
--peraa-radius-sm |
0.25rem (4px) | Small | |
--peraa-radius-md |
0.5rem (8px) | Medium (default) | |
--peraa-radius-lg |
0.75rem (12px) | Large | |
--peraa-radius-xl |
1rem (16px) | Extra large | |
--peraa-radius-2xl |
1.5rem (24px) | Large cards, modals | |
--peraa-radius-full |
9999px | Pill shape |
| Token | Preview | Description |
|---|---|---|
--peraa-shadow-xs |
Extra small shadow | |
--peraa-shadow-sm |
Small shadow | |
--peraa-shadow-md |
Medium shadow | |
--peraa-shadow-lg |
Large shadow | |
--peraa-shadow-xl |
Extra large shadow | |
--peraa-shadow-2xl |
2X large — modals, overlays |
| Token | Value | Description |
|---|---|---|
--peraa-line-height-tight | 1.2 | Large headings (h1, h2) — condensed for display use |
--peraa-line-height-snug | 1.375 | Subheadings (h3–h4) |
--peraa-line-height-normal | 1.5 | Body text — default reading rhythm |
--peraa-line-height-relaxed | 1.625 | Long-form reading, articles |
--peraa-line-height-loose | 2 | Spacious UI labels, table rows |
Direction A (Premium Dark Editorial) uses negative tracking on large headings to tighten display type. Overlines use wide tracking to add visual contrast against heading copy.
| Token | Value | Used on |
|---|---|---|
--peraa-letter-spacing-tighter | -0.05em | Reserved for extreme display use |
--peraa-letter-spacing-tight | -0.025em | .peraa-heading-2 (Direction A) |
--peraa-letter-spacing-normal | 0 | Body text, most UI elements |
--peraa-letter-spacing-wide | 0.025em | Overlines, small labels |
--peraa-letter-spacing-wider | 0.05em | ALL CAPS labels |
--peraa-letter-spacing-widest | 0.1em | Decorative overlines at small sizes |
| Token | Value | Description |
|---|---|---|
--peraa-border-width-0 | 0 | No border (use to override) |
--peraa-border-width-1 | 1px | Default — all cards, dividers, inputs |
--peraa-border-width-2 | 2px | Active tab indicator, focus rings |
--peraa-border-width-4 | 4px | Alert left-border accent, timeline tracks |
| Token | Value | Use case |
|---|---|---|
--peraa-duration-75 | 75ms | Instant micro-interactions (checkbox tick) |
--peraa-duration-100 | 100ms | Icon state changes |
--peraa-duration-150 | 150ms | Button press feedback |
--peraa-duration-200 | 200ms | Hover transitions (color, border) |
--peraa-duration-300 | 300ms | Panel reveals, tab content switches |
--peraa-duration-500 | 500ms | Page entrance animations, theme fade |
| Token | Value | Use case |
|---|---|---|
--peraa-ease-linear | linear | Progress bars, loaders |
--peraa-ease-in | cubic-bezier(0.4, 0, 1, 1) | Elements leaving the screen |
--peraa-ease-out | cubic-bezier(0, 0, 0.2, 1) | Elements entering the screen (default) |
--peraa-ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) | State toggles, flip cards, modals |
Use only the defined Z layers — never use arbitrary values. Layers establish a clear stacking context that prevents unresolvable overlap bugs.
| Token | Value | Intended layer |
|---|---|---|
--peraa-z-0 | 0 | Base / no stacking context needed |
--peraa-z-10 | 10 | Sticky elements — table headers, inline toolbars |
--peraa-z-20 | 20 | Dropdowns, floating cards |
--peraa-z-30 | 30 | Sticky site header / navigation bar |
--peraa-z-40 | 40 | Drawers, side panels |
--peraa-z-50 | 50 | Modals, dialogs |
--peraa-z-100 | 100 | Toasts, alerts, top-level notifications |
All values are defined per theme. Apply via the .peraa-glass component class — do not use these tokens directly in component styles unless you are building a glass variant.
| Token | Dark value | Light value |
|---|---|---|
--peraa-glass-bg | neutral-800 solid | Translucent white + teal/purple radial gradients |
--peraa-glass-border | neutral-700 | rgba(255,255,255,0.75) |
--peraa-glass-blur | none | blur(20px) saturate(1.4) |
--peraa-glass-shadow | none | Layered outer shadow + inner top highlight |
--peraa-glass-radius | --peraa-radius-xl (1rem) | |
| Token | Dark | Light |
|---|---|---|
--peraa-focus-ring-shadow | 0 0 0 3px rgba(purple-500, 0.2) | 0 0 0 3px rgba(purple-500, 0.15) |
Low-opacity status backgrounds used for feedback banners and toast surfaces. Theme-aware.
| Token | Description |
|---|---|
--peraa-status-success-bg-subtle | Muted green tint — success feedback banners |
--peraa-status-warning-bg-subtle | Muted amber tint — warning feedback banners |
--peraa-status-error-bg-subtle | Muted red tint — error feedback banners |
--peraa-status-info-bg-subtle | Muted blue tint — info feedback banners |