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');
41 components across 7 categories, each available as HTML/CSS and React + TypeScript.
Primary, secondary, and tertiary styles with multiple sizes.
Tags and skill badges for labeling content.
Horizontal tab navigation with keyboard support.
Content cards with image, title, and description.
Clickable images with hover overlay effects.
Vertical sticky timeline for case study pages.
Inline notifications with info, success, warning, and error variants.
Accessible dialog overlay with focus trapping.
Frosted-glass surface for elevated UI layers.
Top nav bar with logo, links, and theme toggle.
Site footer with links and brand elements.
Article list layout for blog and case study indexes.
3D flip card with front and back faces.
Text input field with label, hint, and validation states.
Custom checkbox with label, checked, and disabled states.
Radio button group for single-selection options.
On/off switch for boolean settings.
Multi-line text input with auto-resize support.
Styled dropdown for choosing from a list of options.
Drag-and-drop file picker with file list display.
Search field with icon and clear button.
Transient notification messages with auto-dismiss.
Animated loading indicator in multiple sizes.
Placeholder shimmer for content loading states.
Linear progress indicator with labeled value.
Contextual label on hover, positioned in four directions.
Rich overlay panel triggered by a button click.
Side panel that slides in from left or right.
Hierarchical path trail with separator glyphs.
Page-by-page navigation with prev/next controls.
Step-by-step progress indicator for multi-stage flows.
Collapsible sections for progressive disclosure of content.
Horizontal or vertical rule for visual separation.
Data table with striped rows and responsive overflow.
Key-value pairs for structured metadata display.
User portrait or initials fallback in multiple sizes.
Compact label for categories, filters, and selections.
Prominent inline block for tips, notes, and warnings.
Illustrated placeholder for zero-content views.
Keyboard shortcut key display for documentation.
Metric display with value, label, and trend indicator.
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 motion in Peraa is driven by a shared set of duration and easing tokens. Every transition and keyframe animation below draws from these primitives — making motion consistent, predictable, and easy to update from a single place.
| Token | Value | Usage |
|---|---|---|
--peraa-duration-75 | 75ms | Instant micro-feedback (icon swap) |
--peraa-duration-100 | 100ms | Tooltip appear/dismiss |
--peraa-duration-150 | 150ms | Hover color, focus ring, radio dot |
--peraa-duration-200 | 200ms | Tab fade-in, toast exit, popover in |
--peraa-duration-300 | 300ms | Accordion expand, progress fill, toast enter, drawer overlay |
--peraa-duration-500 | 500ms | Flip card 3D rotation |
| Token | Curve | Usage |
|---|---|---|
--peraa-ease-linear | linear | Spinner rotation, progress indeterminate |
--peraa-ease-in | cubic-bezier(0.4,0,1,1) | Elements leaving the screen (toast out) |
--peraa-ease-out | cubic-bezier(0,0,0.2,1) | Elements entering (most transitions & animations) |
--peraa-ease-in-out | cubic-bezier(0.4,0,0.2,1) | Symmetric motion — drawer slide, flip card |
Hover each row to preview the curve
Looping and one-shot animations defined with @keyframes. All respect prefers-reduced-motion.
| Keyframe | peraa-spin |
| Duration | --peraa-duration-500 (500ms) |
| Easing | ease-linear |
| Iteration | infinite |
0° → 360° rotation. Used for all loading indicators while awaiting async operations.
| Keyframe | peraa-shimmer |
| Duration | 1500ms |
| Easing | ease-in-out |
| Iteration | infinite |
Gradient sweeps left-to-right on skeleton placeholders, signalling content is loading.
| Keyframe | peraa-progress-indeterminate |
| Duration | 1500ms |
| Easing | ease-in-out |
| Iteration | infinite |
Bar translates across the full track, used when progress percentage is unknown.
State-change transitions triggered by user interaction. Hover the demos to preview.
| Property | max-height |
| Duration | 300ms |
| Easing | ease-out |
| Property | transform: translateX |
| Duration | 300ms |
| Easing | ease-in-out |
| Property | transform: rotateY |
| Duration | 500ms |
| Easing | ease-in-out |
| Properties | transform, box-shadow, border-color |
| Duration | 150ms |
| Easing | ease-out |
| Properties | background-color, box-shadow, transform |
| Duration | 150ms |
| Easing | ease-out |
| Properties | border-color, box-shadow |
| Duration | 150ms |
| Easing | ease-out |
| Radio dot | transform: scale 150ms ease-out |
| Checkbox | background-color 150ms ease-out |
Every animation and transition in this system is wrapped with a prefers-reduced-motion media query. When the user's OS has reduced motion enabled, all durations collapse to 0.01ms and all looping animations are paused. No extra code is required in consuming components.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
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 |