
Frontend Design
- 2.3k installs
- 133 repo stars
- Updated February 24, 2026
- jwynia/agent-skills
frontend-design creates distinctive UIs via Deno scripts for CSS audit, token extraction, palette, typography, and component generation.
About
The frontend-design skill combines design philosophy with Deno scripts to analyze existing CSS and generate palettes, typography, tokens, and framework components that avoid generic AI aesthetics. Core principle demands a clear aesthetic direction executed with precision, whether maximalist or minimal. Three modes cover Analyze for style and accessibility audits, Specify via JSON specs in assets, and Generate for palettes, typography scales, tokens, and React Vue Svelte or HTML components. Prerequisites require Deno with allow-read and allow-write permissions. Analyze scripts inventory colors, typography, spacing inconsistencies, extract tokens to css scss tailwind or style-dictionary formats, and check WCAG AA or AAA contrast, focus, touch targets, and motion preferences. Generate scripts support seed colors, themes warm cool neutral vibrant muted dark light, type scales from minor-second through golden-ratio, and component templates with aesthetic variants minimal bold organic brutalist glassmorphism or neumorphism. Skip for backend-only work or simple text edits. Teams invoke it when building web UIs, auditing legacy CSS, or bootstrapping design systems with intentional visual.
- Analyze, Specify, and Generate modes with Deno TypeScript scripts.
- Audit CSS colors, typography, spacing, and accessibility contrast.
- Generate palettes, typography scales, tokens, and multi-framework components.
- Aesthetic variants: minimal, bold, organic, brutalist, glassmorphism, neumorphism.
- Core principle: intentional aesthetic direction, not generic AI slop.
Frontend Design by the numbers
- 2,297 all-time installs (skills.sh)
- +2 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #132 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
frontend-design capabilities & compatibility
- Capabilities
- css style analysis and inconsistency detection · token extraction to multiple output formats · palette and typography system generation · multi framework component template generation · wcag contrast and focus accessibility checks
- Use cases
- frontend · ui design · web design
What frontend-design says it does
Choose a clear aesthetic direction and execute it with precision.
npx skills add https://github.com/jwynia/agent-skills --skill frontend-designAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.3k |
|---|---|
| repo stars | ★ 133 |
| Security audit | 3 / 3 scanners passed |
| Last updated | February 24, 2026 |
| Repository | jwynia/agent-skills ↗ |
How do I build or audit a frontend with cohesive tokens, accessibility, and a deliberate aesthetic instead of generic AI styling?
Create distinctive production-grade frontend UIs with Deno tooling for style audit, token extraction, palette generation, typography, and component templates.
Who is it for?
Web UI work in React, Vue, Svelte, or HTML with Deno tooling for design system artifacts.
Skip if: Skip for backend-only changes, simple copy edits, or non-visual functionality.
When should I use this skill?
User asks for frontend design, color palette, typography system, design tokens, or CSS accessibility audit.
What you get
Analyzed style report or generated palette, typography, tokens, and framework component files with WCAG checks.
- component source files
- validated JSON spec
By the numbers
- Supports 7 component type enums in JSON schema
- Component name maxLength 64 with PascalCase pattern
Files
Frontend Design
Create distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. This skill combines design philosophy guidance with practical TypeScript tooling for analyzing existing designs and generating design system artifacts.
Core Principle: Choose a clear aesthetic direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
When to Use This Skill
Use when:
- Building web components, pages, or applications
- Auditing existing CSS for design inconsistencies
- Extracting design tokens from legacy codebases
- Generating color palettes and typography systems
- Creating component templates across frameworks
- Checking accessibility compliance for design elements
Don't use when:
- Simple text content changes
- Backend-only work
- Non-visual functionality
Prerequisites
- Deno runtime (for script execution)
- Optional: Existing CSS/design files for analysis
- Optional: Design tokens file for reference
Quick Start
This skill operates in three modes:
1. Analyze Mode
Audit existing styles, extract tokens, check accessibility.
# Audit CSS for design patterns and inconsistencies
deno run --allow-read scripts/analyze-styles.ts styles.css
# Extract design tokens from existing CSS
deno run --allow-read scripts/extract-tokens.ts ./src --format css
# Check accessibility (contrast, focus states)
deno run --allow-read scripts/analyze-accessibility.ts component.tsx2. Specify Mode
Create JSON specifications defining what to generate. See assets/ for schema examples.
3. Generate Mode
Create palettes, typography systems, tokens, and components.
# Generate a color palette
deno run --allow-read --allow-write scripts/generate-palette.ts --seed "#2563eb" --theme warm
# Generate typography system
deno run --allow-read --allow-write scripts/generate-typography.ts --display "Playfair Display" --body "Source Sans Pro"
# Generate design tokens file
deno run --allow-read --allow-write scripts/generate-tokens.ts tokens-spec.json ./output/
# Generate a component
deno run --allow-read --allow-write scripts/generate-component.ts --name Button --framework react --styling tailwind---
Instructions
Phase 1: Analysis (Optional but Recommended)
Before creating new designs, audit existing code to understand current patterns.
1a. Style Analysis
Analyze CSS files to identify colors, typography, spacing, and inconsistencies:
deno run --allow-read scripts/analyze-styles.ts <input> [options]
Options:
--tokens <file> Compare against existing design tokens
--pretty Pretty-print JSON output
--format <type> Output format: json (default), summaryOutput includes:
- Color usage inventory with hex normalization
- Typography patterns (fonts, sizes, weights)
- Spacing value distribution
- Inconsistencies and recommendations
1b. Token Extraction
Extract design tokens from CSS files into standardized formats:
deno run --allow-read scripts/extract-tokens.ts <input> [options]
Options:
--format <type> Output: css, scss, tailwind, style-dictionary, tokens-studio
--output-css Also output CSS variables file1c. Accessibility Audit
Check design-related accessibility issues:
deno run --allow-read scripts/analyze-accessibility.ts <input> [options]
Options:
--format <type> Output: json, summary
--level <AA|AAA> WCAG conformance level (default: AA)Checks include:
- Color contrast ratios
- Focus indicator presence
- Touch target sizes
- Motion preference respect
---
Phase 2: Generation
2a. Color Palette Generation
Generate cohesive color palettes from seed colors or themes:
deno run --allow-read --allow-write scripts/generate-palette.ts [options] <output>
Options:
--seed <color> Primary seed color (hex)
--theme <type> warm, cool, neutral, vibrant, muted, dark, light
--style <type> minimalist, bold, organic, corporate, playful
--shades Generate 50-950 shade scale
--semantic Generate success/warning/error colors
--contrast <lvl> Target contrast: AA (default), AAA
--format <type> css, scss, tailwind, tokens, jsonExample specification (palette-spec.json):
{
"seedColors": {
"primary": "#2563eb",
"accent": "#f59e0b"
},
"theme": "cool",
"generateShades": true,
"generateSemantics": true,
"contrastTarget": "AA",
"outputFormat": "css"
}2b. Typography System Generation
Generate typography systems with font stacks and scales:
deno run --allow-read --allow-write scripts/generate-typography.ts [options] <output>
Options:
--display <font> Display/heading font family
--body <font> Body text font family
--mono <font> Monospace font family
--scale <type> minor-second, major-second, minor-third, major-third, perfect-fourth, golden-ratio
--base <px> Base font size (default: 16)
--line-height tight, normal, relaxed
--responsive Generate responsive breakpoints
--format <type> css, scss, tailwind, tokensType scales:
| Scale | Ratio | Character |
|---|---|---|
| minor-second | 1.067 | Subtle, conservative |
| major-second | 1.125 | Balanced, professional |
| minor-third | 1.200 | Clear hierarchy |
| major-third | 1.250 | Strong presence |
| perfect-fourth | 1.333 | Bold, impactful |
| golden-ratio | 1.618 | Dramatic, artistic |
2c. Design Token Generation
Generate comprehensive design token files in multiple formats:
deno run --allow-read --allow-write scripts/generate-tokens.ts <spec> <output-dir>Specification structure:
{
"name": "my-design-system",
"tokens": {
"color": { "primary": "#2563eb", "secondary": "#64748b" },
"spacing": { "xs": "0.25rem", "sm": "0.5rem", "md": "1rem" },
"typography": {
"fontFamilies": { "display": "Playfair Display", "body": "Inter" },
"fontSizes": { "sm": "0.875rem", "base": "1rem", "lg": "1.125rem" }
},
"shadow": { "sm": "0 1px 2px rgba(0,0,0,0.05)" },
"border": { "radius": { "sm": "0.25rem", "md": "0.5rem" } },
"animation": { "duration": { "fast": "150ms", "normal": "300ms" } }
},
"outputFormats": ["css", "scss", "tailwind", "ts"]
}2d. Component Generation
Generate component templates with design-focused styling:
deno run --allow-read --allow-write scripts/generate-component.ts [options] <output-dir>
Options:
--name <name> Component name (required)
--type <type> button, card, input, modal, navigation, hero, custom
--framework <fw> react, vue, svelte, html
--styling <type> css, tailwind, css-modules, styled-components, emotion
--aesthetic <type> minimal, bold, organic, brutalist, glassmorphism, neumorphism
--animation <lvl> none, subtle, expressive
--dark-mode Include dark mode support
--tokens <file> Use design tokens fileFramework output examples:
React with Tailwind:
export function Button({ variant = 'primary', children }: ButtonProps) {
return (
<button className="px-4 py-2 rounded-lg font-medium transition-all">
{children}
</button>
);
}Vue with scoped CSS:
<template>
<button :class="['btn', `btn--${variant}`]"><slot /></button>
</template>
<script setup lang="ts">
defineProps<{ variant?: 'primary' | 'secondary' }>();
</script>
<style scoped>.btn { /* styles */ }</style>Svelte:
<script lang="ts">
export let variant: 'primary' | 'secondary' = 'primary';
</script>
<button class="btn btn--{variant}"><slot /></button>
<style>.btn { /* styles */ }</style>---
Examples
Example 1: Audit and Modernize Legacy CSS
# 1. Analyze existing styles
deno run --allow-read scripts/analyze-styles.ts ./legacy/styles.css --pretty > audit.json
# 2. Extract tokens from the analysis
deno run --allow-read scripts/extract-tokens.ts ./legacy/styles.css --format css > tokens.css
# 3. Generate improved palette from dominant color
deno run --allow-read --allow-write scripts/generate-palette.ts --seed "#336699" --shades --format css palette.cssExample 2: Create Design System from Scratch
# 1. Generate color palette
deno run --allow-read --allow-write scripts/generate-palette.ts \
--seed "#8b5cf6" --theme vibrant --shades --semantic --format css colors.css
# 2. Generate typography system
deno run --allow-read --allow-write scripts/generate-typography.ts \
--display "Space Grotesk" --body "Inter" --scale major-third --responsive --format css typography.css
# 3. Generate comprehensive tokens
deno run --allow-read --allow-write scripts/generate-tokens.ts design-spec.json ./tokens/Example 3: Generate Component Library
# Generate button component for React with Tailwind
deno run --allow-read --allow-write scripts/generate-component.ts \
--name Button --framework react --styling tailwind --aesthetic bold ./components/
# Generate card component for Vue with CSS modules
deno run --allow-read --allow-write scripts/generate-component.ts \
--name Card --framework vue --styling css-modules --aesthetic glassmorphism ./components/
# Generate input for Svelte
deno run --allow-read --allow-write scripts/generate-component.ts \
--name Input --framework svelte --styling css --aesthetic minimal ./components/Example 4: Accessibility Check Before Launch
# Audit all components
deno run --allow-read scripts/analyze-accessibility.ts ./src/components --level AAA --format summary---
Script Reference
| Script | Purpose | Permissions |
|---|---|---|
analyze-styles.ts | Audit CSS for patterns and inconsistencies | --allow-read |
extract-tokens.ts | Extract design tokens from CSS | --allow-read |
analyze-accessibility.ts | Check design accessibility | --allow-read |
generate-palette.ts | Generate color palettes | --allow-read --allow-write |
generate-typography.ts | Generate typography systems | --allow-read --allow-write |
generate-tokens.ts | Generate multi-format tokens | --allow-read --allow-write |
generate-component.ts | Generate framework components | --allow-read --allow-write |
---
Design Philosophy
Avoid generic "AI slop" aesthetics. See references/design-philosophy.md for detailed guidance.
Key principles:
- Typography: Choose distinctive fonts, not defaults (avoid Arial, Inter, Roboto)
- Color: Dominant colors with sharp accents, not timid palettes
- Motion: Orchestrated reveals over scattered micro-interactions
- Composition: Asymmetry, overlap, grid-breaking elements
- Texture: Gradients, noise, patterns - not flat solid colors
Never converge on:
- Purple gradients on white backgrounds
- System fonts or overused families
- Predictable card-based layouts
- Cookie-cutter Bootstrap aesthetics
---
Common Issues
"CSS parsing failed"
- Ensure valid CSS syntax
- SCSS/PostCSS may need preprocessing first
"Color contrast check failed"
- Check for CSS variables that couldn't be resolved
- Provide the actual color values or tokens file
"Unknown framework"
- Supported: react, vue, svelte, html
- Check spelling and lowercase
"Token format not supported"
- Supported: css, scss, tailwind, style-dictionary, tokens-studio, json, ts
---
Additional Resources
references/design-philosophy.md- Detailed anti-slop aesthetics guidereferences/token-formats.md- Design token format specificationsreferences/framework-templates.md- Framework-specific patternsassets/*.json- JSON schema files for specifications
---
Related Skills
web-search- Research design inspiration and trendsresearch-workflow- Deep dive into design systems
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Component Specification",
"description": "Schema for component template generation",
"type": "object",
"required": ["name", "framework"],
"properties": {
"name": {
"type": "string",
"description": "Component name (PascalCase)",
"pattern": "^[A-Z][a-zA-Z0-9]*$",
"minLength": 1,
"maxLength": 64
},
"type": {
"type": "string",
"description": "Component type for template selection",
"enum": ["button", "card", "input", "modal", "navigation", "hero", "custom"],
"default": "custom"
},
"framework": {
"type": "string",
"description": "Target framework",
"enum": ["react", "vue", "svelte", "html"]
},
"styling": {
"type": "string",
"description": "Styling approach",
"enum": ["css", "tailwind", "css-modules", "styled-components", "emotion"],
"default": "css"
},
"aesthetic": {
"type": "object",
"description": "Aesthetic configuration",
"properties": {
"style": {
"type": "string",
"description": "Visual style",
"enum": ["minimal", "bold", "organic", "brutalist", "glassmorphism", "neumorphism"]
},
"animation": {
"type": "string",
"description": "Animation level",
"enum": ["none", "subtle", "expressive"],
"default": "subtle"
},
"darkMode": {
"type": "boolean",
"description": "Include dark mode support",
"default": false
}
}
},
"tokens": {
"type": "string",
"description": "Path to design tokens file for styling"
},
"props": {
"type": "object",
"description": "Component props definition",
"additionalProperties": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "TypeScript type"
},
"default": {
"type": "string",
"description": "Default value"
},
"required": {
"type": "boolean",
"description": "Whether prop is required",
"default": false
}
},
"required": ["type"]
}
},
"slots": {
"type": "array",
"description": "Named slots (for Vue/Svelte) or children patterns (for React)",
"items": { "type": "string" }
},
"variants": {
"type": "array",
"description": "Style variants to generate",
"items": { "type": "string" }
}
},
"examples": [
{
"name": "Button",
"type": "button",
"framework": "react",
"styling": "tailwind",
"aesthetic": {
"style": "bold",
"animation": "subtle",
"darkMode": true
},
"props": {
"variant": {
"type": "'primary' | 'secondary' | 'ghost'",
"default": "'primary'"
},
"size": {
"type": "'sm' | 'md' | 'lg'",
"default": "'md'"
},
"disabled": {
"type": "boolean",
"default": "false"
}
},
"variants": ["primary", "secondary", "ghost"]
},
{
"name": "Card",
"type": "card",
"framework": "vue",
"styling": "css-modules",
"aesthetic": {
"style": "glassmorphism",
"animation": "expressive"
},
"slots": ["header", "default", "footer"],
"props": {
"elevated": {
"type": "boolean",
"default": "false"
}
}
},
{
"name": "Input",
"type": "input",
"framework": "svelte",
"styling": "css",
"aesthetic": {
"style": "minimal"
},
"props": {
"label": {
"type": "string",
"required": true
},
"placeholder": {
"type": "string"
},
"error": {
"type": "string"
}
}
}
]
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Palette Specification",
"description": "Schema for color palette generation specifications",
"type": "object",
"properties": {
"seedColors": {
"type": "object",
"description": "Seed colors to build palette from",
"properties": {
"primary": {
"type": "string",
"description": "Primary brand color (hex format)",
"pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
},
"secondary": {
"type": "string",
"description": "Secondary color (hex format)",
"pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
},
"accent": {
"type": "string",
"description": "Accent color (hex format)",
"pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
}
}
},
"theme": {
"type": "string",
"description": "Overall theme direction",
"enum": ["warm", "cool", "neutral", "vibrant", "muted", "dark", "light"]
},
"style": {
"type": "string",
"description": "Design style influence",
"enum": ["minimalist", "bold", "organic", "corporate", "playful"]
},
"generateShades": {
"type": "boolean",
"description": "Generate 50-950 shade scale for each color",
"default": false
},
"generateSemantics": {
"type": "boolean",
"description": "Generate semantic colors (success, warning, error, info)",
"default": false
},
"contrastTarget": {
"type": "string",
"description": "WCAG contrast level to target",
"enum": ["AA", "AAA"],
"default": "AA"
},
"outputFormat": {
"type": "string",
"description": "Output format for the palette",
"enum": ["css", "scss", "tailwind", "tokens", "json"],
"default": "css"
}
},
"examples": [
{
"seedColors": {
"primary": "#2563eb",
"accent": "#f59e0b"
},
"theme": "cool",
"generateShades": true,
"generateSemantics": true,
"contrastTarget": "AA",
"outputFormat": "css"
},
{
"seedColors": {
"primary": "#8b5cf6"
},
"theme": "vibrant",
"style": "playful",
"generateShades": true,
"outputFormat": "tailwind"
}
]
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Design Token Specification",
"description": "Schema for comprehensive design token generation",
"type": "object",
"required": ["name", "tokens", "outputFormats"],
"properties": {
"name": {
"type": "string",
"description": "Design system name",
"minLength": 1,
"maxLength": 64
},
"description": {
"type": "string",
"description": "Description of the design system"
},
"tokens": {
"type": "object",
"description": "Token definitions by category",
"properties": {
"color": {
"type": "object",
"description": "Color tokens",
"additionalProperties": {
"oneOf": [
{ "type": "string" },
{ "type": "object", "additionalProperties": { "type": "string" } }
]
}
},
"spacing": {
"type": "object",
"description": "Spacing scale tokens",
"additionalProperties": { "type": "string" }
},
"typography": {
"type": "object",
"properties": {
"fontFamilies": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"fontSizes": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"fontWeights": {
"type": "object",
"additionalProperties": { "type": "number" }
},
"lineHeights": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"letterSpacing": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
},
"shadow": {
"type": "object",
"description": "Box shadow tokens",
"additionalProperties": { "type": "string" }
},
"border": {
"type": "object",
"properties": {
"radius": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"width": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
},
"animation": {
"type": "object",
"properties": {
"duration": {
"type": "object",
"additionalProperties": { "type": "string" }
},
"easing": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
},
"zIndex": {
"type": "object",
"description": "Z-index scale",
"additionalProperties": { "type": "number" }
}
}
},
"outputFormats": {
"type": "array",
"description": "Output formats to generate",
"items": {
"type": "string",
"enum": ["css", "scss", "json", "js", "ts", "tailwind", "style-dictionary"]
},
"minItems": 1
}
},
"examples": [
{
"name": "my-design-system",
"description": "A modern design system for web applications",
"tokens": {
"color": {
"primary": "#2563eb",
"primary-light": "#3b82f6",
"primary-dark": "#1d4ed8",
"secondary": "#64748b",
"background": "#ffffff",
"surface": "#f8fafc",
"text": "#1e293b",
"text-muted": "#64748b"
},
"spacing": {
"xs": "0.25rem",
"sm": "0.5rem",
"md": "1rem",
"lg": "1.5rem",
"xl": "2rem",
"2xl": "3rem"
},
"typography": {
"fontFamilies": {
"display": "Playfair Display, serif",
"body": "Inter, sans-serif",
"mono": "JetBrains Mono, monospace"
},
"fontSizes": {
"xs": "0.75rem",
"sm": "0.875rem",
"base": "1rem",
"lg": "1.125rem",
"xl": "1.25rem",
"2xl": "1.5rem",
"3xl": "1.875rem",
"4xl": "2.25rem"
},
"fontWeights": {
"normal": 400,
"medium": 500,
"semibold": 600,
"bold": 700
},
"lineHeights": {
"tight": "1.25",
"normal": "1.5",
"relaxed": "1.75"
}
},
"shadow": {
"sm": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
"md": "0 4px 6px -1px rgb(0 0 0 / 0.1)",
"lg": "0 10px 15px -3px rgb(0 0 0 / 0.1)",
"xl": "0 20px 25px -5px rgb(0 0 0 / 0.1)"
},
"border": {
"radius": {
"sm": "0.25rem",
"md": "0.5rem",
"lg": "1rem",
"full": "9999px"
},
"width": {
"thin": "1px",
"medium": "2px",
"thick": "4px"
}
},
"animation": {
"duration": {
"fast": "150ms",
"normal": "300ms",
"slow": "500ms"
},
"easing": {
"ease-in": "cubic-bezier(0.4, 0, 1, 1)",
"ease-out": "cubic-bezier(0, 0, 0.2, 1)",
"ease-in-out": "cubic-bezier(0.4, 0, 0.2, 1)"
}
},
"zIndex": {
"dropdown": 1000,
"sticky": 1020,
"modal": 1030,
"popover": 1040,
"tooltip": 1050
}
},
"outputFormats": ["css", "scss", "tailwind", "ts"]
}
]
}
Design Philosophy: Avoiding AI Slop
This document provides detailed guidance on creating distinctive, production-grade frontend interfaces that avoid generic "AI-generated" aesthetics.
Core Principle
Choose a clear aesthetic direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Design Thinking Process
Before writing any code, understand the context and commit to a BOLD aesthetic direction:
1. Purpose
- What problem does this interface solve?
- Who uses it and in what context?
- What emotions should it evoke?
2. Aesthetic Tone
Pick a distinctive direction. Here are examples for inspiration:
| Tone | Characteristics |
|---|---|
| Brutally Minimal | Stark contrast, maximum whitespace, single accent color |
| Maximalist Chaos | Dense information, layered textures, vibrant palettes |
| Retro-Futuristic | Neon accents, dark backgrounds, geometric shapes |
| Organic/Natural | Soft curves, earthy tones, flowing animations |
| Luxury/Refined | Subtle typography, muted palettes, premium spacing |
| Playful/Toy-like | Rounded corners, bright colors, bouncy animations |
| Editorial/Magazine | Strong typography hierarchy, asymmetric layouts |
| Brutalist/Raw | Exposed structure, stark fonts, visible grids |
| Art Deco/Geometric | Bold patterns, metallic accents, symmetry |
| Soft/Pastel | Gentle gradients, light shadows, calming colors |
| Industrial/Utilitarian | Monospace fonts, muted colors, functional density |
3. Differentiation
What makes this interface UNFORGETTABLE? What's the one thing someone will remember?
---
The Five Pillars of Distinctive Design
1. Typography
Do:
- Choose fonts that are beautiful, unique, and characterful
- Pair a distinctive display font with a refined body font
- Use unexpected, personality-rich choices
- Consider the context and audience
Don't:
- Default to Arial, Inter, Roboto, or system fonts
- Use the same fonts across every project
- Ignore font pairing harmony
- Over-rely on weight for hierarchy
Distinctive Font Examples:
| Category | Options |
|---|---|
| Display | Playfair Display, Fraunces, Libre Baskerville, Cormorant |
| Sans-Serif | Space Grotesk, DM Sans, Plus Jakarta Sans, Outfit |
| Mono | JetBrains Mono, Fira Code, IBM Plex Mono |
| Unique | Anybody, Instrument Serif, Cabinet Grotesk |
2. Color & Theme
Do:
- Commit to a cohesive aesthetic
- Use CSS variables for consistency
- Choose dominant colors with sharp accents
- Consider context (brand, mood, accessibility)
Don't:
- Default to purple gradients on white
- Use timid, evenly-distributed palettes
- Ignore contrast requirements
- Copy trending palettes without adaptation
Palette Strategies:
- Monochromatic: Single hue with varied saturation/lightness
- Complementary: Opposite colors for high contrast
- Analogous: Adjacent colors for harmony
- Split-Complementary: Main color + two adjacent to its complement
- Triadic: Three evenly-spaced colors
3. Motion
Do:
- Focus on high-impact moments
- Create orchestrated page load reveals with staggered animation-delay
- Use scroll-triggered animations that surprise
- Implement hover states with personality
Don't:
- Add motion for motion's sake
- Create scattered, uncoordinated micro-interactions
- Ignore reduced-motion preferences
- Use jarring or disorienting effects
Motion Hierarchy: 1. Page transitions: Most impactful, highest attention 2. Scroll reveals: Content appearing as user explores 3. Hover states: Feedback and delight 4. Micro-interactions: Subtle polish (buttons, toggles)
CSS-First Approach:
/* Staggered reveal */
.item { animation: fadeIn 0.5s ease-out forwards; opacity: 0; }
.item:nth-child(1) { animation-delay: 0.1s; }
.item:nth-child(2) { animation-delay: 0.2s; }
.item:nth-child(3) { animation-delay: 0.3s; }
/* Respect preferences */
@media (prefers-reduced-motion: reduce) {
.item { animation: none; opacity: 1; }
}4. Spatial Composition
Do:
- Create unexpected layouts
- Use asymmetry intentionally
- Allow elements to overlap
- Employ diagonal flow and grid-breaking elements
- Balance generous negative space OR controlled density
Don't:
- Default to symmetric card grids
- Treat every element equally
- Fear whitespace (or density, if that's your direction)
- Ignore visual rhythm
Layout Techniques:
- Broken grid: Elements that break the expected grid
- Overlap: Layered elements creating depth
- Scale contrast: Large elements next to small
- Negative space: Breathing room as a design element
- Asymmetric balance: Visual weight distributed unevenly
5. Backgrounds & Visual Details
Do:
- Create atmosphere and depth
- Use contextual effects that match the aesthetic
- Layer transparencies for dimension
- Add texture where appropriate
Don't:
- Default to solid white backgrounds
- Use generic gradient presets
- Forget about dark mode considerations
- Over-apply effects
Visual Effects Palette:
- Gradient meshes
- Noise and grain textures
- Geometric patterns
- Layered transparencies
- Dramatic shadows (soft or hard)
- Decorative borders
- Custom cursors
- Glassmorphism (used intentionally)
- Neumorphism (used sparingly)
---
What to Avoid: The "AI Slop" Checklist
Typography Anti-Patterns
- [ ] Inter everywhere
- [ ] Roboto as the only choice
- [ ] Arial or system fonts
- [ ] Identical font usage across projects
Color Anti-Patterns
- [ ] Purple-to-blue gradient on white
- [ ] Generic "modern" teal
- [ ] Desaturated "safe" palettes
- [ ] Unrelated accent colors
Layout Anti-Patterns
- [ ] 3-column card grids
- [ ] Identical padding everywhere
- [ ] Symmetric hero sections
- [ ] Predictable section patterns
Effect Anti-Patterns
- [ ] Subtle gray shadows on everything
- [ ] Generic rounded corners (8px everywhere)
- [ ] Smooth but meaningless transitions
- [ ] Glass effects without purpose
---
Implementation Complexity Matching
Critical: Match implementation complexity to aesthetic vision.
Maximalist Designs
- Elaborate code with extensive animations
- Multiple layered effects
- Complex state management for interactions
- Rich color variations
Minimalist Designs
- Restraint and precision
- Careful attention to spacing
- Perfect typography
- Subtle, meaningful details
- "Less" requires more care, not less code
---
Quality Checklist
Before considering a design complete:
- [ ] Typography is distinctive and intentional
- [ ] Colors support the aesthetic direction
- [ ] Motion is orchestrated, not scattered
- [ ] Layout has visual interest and hierarchy
- [ ] Details add atmosphere without clutter
- [ ] Accessibility is preserved (contrast, motion preferences)
- [ ] The design is memorable - someone would recognize it
---
Remember
"Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision."
No two designs should look the same. Vary themes, fonts, aesthetics. The goal is intentional, memorable, production-grade interfaces.
Framework Templates Reference
This document provides framework-specific patterns and best practices for generating components with the frontend-design skill.
React
File Structure
components/
├── Button/
│ ├── Button.tsx # Component
│ ├── Button.css # Styles (if using CSS)
│ ├── Button.module.css # Styles (if using CSS Modules)
│ ├── Button.test.tsx # Tests
│ └── index.ts # Re-exportComponent Pattern
import React from 'react';
export interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
}
export function Button({
children,
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}: ButtonProps) {
return (
<button
className={`btn btn--${variant} btn--${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
export default Button;Styling Options
CSS Modules:
import styles from './Button.module.css';
<button className={styles.button} />Tailwind:
<button className="px-4 py-2 bg-blue-500 text-white rounded-lg" />Styled Components:
import styled from 'styled-components';
const StyledButton = styled.button`
padding: 0.5rem 1rem;
background: var(--color-primary);
`;Emotion:
import { css } from '@emotion/react';
const buttonStyle = css`
padding: 0.5rem 1rem;
background: var(--color-primary);
`;---
Vue 3
File Structure
components/
├── Button.vue # Single File Component
└── __tests__/
└── Button.spec.ts # TestsComponent Pattern (Composition API)
<template>
<button
:class="[
'btn',
`btn--${variant}`,
`btn--${size}`,
{ 'btn--disabled': disabled }
]"
:disabled="disabled"
@click="emit('click')"
>
<slot />
</button>
</template>
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
disabled: false,
});
const emit = defineEmits<{
click: [];
}>();
</script>
<style scoped>
.btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn--primary {
background: var(--color-primary);
color: white;
}
.btn--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>Styling Options
Scoped CSS (default):
<style scoped>
.button { /* styles */ }
</style>CSS Modules:
<style module>
.button { /* styles */ }
</style>
<template>
<button :class="$style.button" />
</template>Tailwind:
<template>
<button class="px-4 py-2 bg-blue-500 text-white rounded-lg" />
</template>---
Svelte
File Structure
components/
├── Button.svelte # Component
└── Button.test.ts # TestsComponent Pattern
<script lang="ts">
export let variant: 'primary' | 'secondary' | 'ghost' = 'primary';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let disabled = false;
</script>
<button
class="btn btn--{variant} btn--{size}"
class:btn--disabled={disabled}
{disabled}
on:click
>
<slot />
</button>
<style>
.btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn--primary {
background: var(--color-primary);
color: white;
}
.btn--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>Styling Options
Component Styles (default):
<style>
/* Scoped by default */
.button { /* styles */ }
</style>Global Styles:
<style global>
.button { /* global styles */ }
</style>Tailwind:
<button class="px-4 py-2 bg-blue-500 text-white rounded-lg" />---
HTML (Vanilla)
File Structure
components/
├── button.html # Component HTML
├── button.css # Component CSS
└── button.js # Optional JavaScriptComponent Pattern
<!-- button.html -->
<button class="btn btn--primary btn--md">
Button Text
</button>/* button.css */
.btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.btn--primary {
background: var(--color-primary, #2563eb);
color: white;
}
.btn--primary:hover {
background: var(--color-primary-dark, #1d4ed8);
}
.btn--secondary {
background: var(--color-gray-100, #f3f4f6);
color: var(--color-gray-900, #111827);
}
.btn--ghost {
background: transparent;
color: var(--color-gray-700, #374151);
}
.btn--sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.btn--md {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.btn--lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.btn:disabled,
.btn--disabled {
opacity: 0.5;
cursor: not-allowed;
}---
Common Patterns
Variant Classes
All frameworks use a consistent variant pattern:
.component--variant
.component--size
.component--stateExamples:
.btn--primary,.btn--secondary,.btn--ghost.btn--sm,.btn--md,.btn--lg.btn--disabled,.btn--loading
Prop/Attribute Naming
| Concept | React | Vue | Svelte |
|---|---|---|---|
| Style variant | variant | variant | variant |
| Size | size | size | size |
| Disabled | disabled | disabled | disabled |
| Click handler | onClick | @click | on:click |
| Children | children | <slot /> | <slot /> |
Accessibility
All generated components include:
1. Proper semantics - Use native elements (<button>, <input>) 2. Keyboard support - Focus styles, keyboard handlers 3. ARIA attributes - When native semantics insufficient 4. Focus management - Visible focus indicators
Dark Mode Support
When --dark-mode is enabled:
/* Light mode (default) */
.btn {
background: white;
color: #111827;
}
/* Dark mode */
.dark .btn,
[data-theme="dark"] .btn {
background: #1f2937;
color: white;
}
/* Or using media query */
@media (prefers-color-scheme: dark) {
.btn {
background: #1f2937;
color: white;
}
}Animation Levels
| Level | Description | Usage |
|---|---|---|
none | No animations | Accessibility, performance |
subtle | Micro-interactions | Most components |
expressive | Bold animations | Hero sections, CTAs |
/* Subtle */
.btn {
transition: all 0.2s ease-out;
}
/* Expressive */
.btn {
transition: all 0.3s ease-out;
}
.btn:hover {
transform: scale(1.05);
}
.btn:active {
transform: scale(0.95);
}---
Best Practices
1. Use semantic HTML - <button> for buttons, <a> for links 2. Include TypeScript types - For React and Vue 3. Support variants - Primary, secondary, ghost at minimum 4. Include sizes - sm, md, lg 5. Handle disabled state - Visual and functional 6. Add focus styles - Visible focus indicators 7. Respect motion preferences - prefers-reduced-motion 8. Use CSS custom properties - For theming flexibility
Design Token Formats Reference
This document describes the various design token output formats supported by the frontend-design skill tools.
Overview
Design tokens are the atomic values of a design system - colors, typography, spacing, etc. stored in a format that can be consumed by different platforms and tools.
Supported Formats
1. CSS Custom Properties
File extension: .css
Standard CSS variables that work in any modern browser.
:root {
/* Colors */
--color-primary-500: #2563eb;
--color-primary-600: #1d4ed8;
/* Spacing */
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
/* Typography */
--font-body: Inter, sans-serif;
--text-base: 1rem;
}Usage:
.button {
background: var(--color-primary-500);
padding: var(--spacing-sm) var(--spacing-md);
font-family: var(--font-body);
}Pros:
- Native browser support
- Runtime theming (dark mode, etc.)
- No build step required
Cons:
- No type safety
- Limited IDE support
---
2. SCSS Variables
File extension: .scss
SCSS/Sass variables with maps for iteration.
// Colors
$color-primary-500: #2563eb;
$color-primary-600: #1d4ed8;
// Spacing
$spacing-sm: 0.5rem;
$spacing-md: 1rem;
// Maps for iteration
$colors: (
'primary-500': #2563eb,
'primary-600': #1d4ed8,
);
$spacing: (
'sm': 0.5rem,
'md': 1rem,
);Usage:
.button {
background: $color-primary-500;
padding: $spacing-sm $spacing-md;
}
// Generate utility classes
@each $name, $value in $spacing {
.p-#{$name} { padding: $value; }
}Pros:
- Compile-time optimization
- Programmatic generation with loops
- Mature ecosystem
Cons:
- Requires build step
- No runtime theming without CSS vars
---
3. Tailwind Config
File extension: .tailwind.js
Extension object for Tailwind CSS configuration.
module.exports = {
colors: {
primary: {
500: '#2563eb',
600: '#1d4ed8',
},
},
spacing: {
sm: '0.5rem',
md: '1rem',
},
fontFamily: {
body: ['Inter', 'sans-serif'],
},
};Usage (in tailwind.config.js):
const tokens = require('./tokens.tailwind.js');
module.exports = {
theme: {
extend: tokens,
},
};Pros:
- Direct Tailwind integration
- Utility class generation
- PurgeCSS optimization
Cons:
- Tailwind-specific
- Requires Tailwind setup
---
4. JSON
File extension: .json
Raw JSON format for maximum portability.
{
"name": "my-design-system",
"tokens": {
"color": {
"primary": {
"500": "#2563eb",
"600": "#1d4ed8"
}
},
"spacing": {
"sm": "0.5rem",
"md": "1rem"
}
}
}Usage:
- Import in any language
- Transform with custom tools
- Use as source of truth
Pros:
- Universal format
- Easy to parse
- Platform agnostic
Cons:
- Requires transformation for use
- No direct runtime usage
---
5. JavaScript Module
File extension: .js
ES module with exports.
export const tokens = {
color: {
primary: {
500: '#2563eb',
600: '#1d4ed8',
},
},
spacing: {
sm: '0.5rem',
md: '1rem',
},
};
export const flatTokens = {
'color-primary-500': '#2563eb',
'color-primary-600': '#1d4ed8',
'spacing-sm': '0.5rem',
'spacing-md': '1rem',
};
export default tokens;Usage:
import { tokens, flatTokens } from './tokens.js';
const primaryColor = tokens.color.primary[500];Pros:
- Direct JavaScript import
- Tree-shakeable
- Works with any bundler
Cons:
- JavaScript only
- No type information
---
6. TypeScript Module
File extension: .ts
TypeScript module with full type definitions.
export interface DesignTokens {
color: {
primary: Record<string, string>;
};
spacing: Record<string, string>;
}
export const tokens: DesignTokens = {
color: {
primary: {
500: '#2563eb',
600: '#1d4ed8',
},
},
spacing: {
sm: '0.5rem',
md: '1rem',
},
} as const;
export type FlatToken = string | number;
export const flatTokens: Record<string, FlatToken> = {
'color-primary-500': '#2563eb',
'spacing-sm': '0.5rem',
};
export default tokens;Usage:
import tokens, { DesignTokens } from './tokens';
// Full type safety
const color: string = tokens.color.primary[500];Pros:
- Full type safety
- IDE autocomplete
- Compile-time validation
Cons:
- TypeScript only
- Requires compilation
---
7. Style Dictionary
File extension: .tokens.json
Format compatible with Amazon's Style Dictionary tool.
{
"color": {
"primary": {
"500": { "value": "#2563eb" },
"600": { "value": "#1d4ed8" }
}
},
"spacing": {
"sm": { "value": "0.5rem" },
"md": { "value": "1rem" }
}
}Usage: Use with Style Dictionary to generate platform-specific outputs (iOS, Android, CSS, etc.).
Pros:
- Multi-platform support
- Powerful transformation system
- Industry standard
Cons:
- Requires Style Dictionary setup
- Additional tooling
---
Format Selection Guide
| Use Case | Recommended Format |
|---|---|
| Web-only, runtime theming | CSS |
| Sass/SCSS project | SCSS |
| Tailwind CSS project | Tailwind |
| Multi-platform design system | Style Dictionary |
| React/JS application | TypeScript |
| Universal source of truth | JSON |
| Quick prototyping | CSS |
Token Categories
All formats support these token categories:
| Category | Examples |
|---|---|
color | Primary, secondary, semantic colors |
spacing | Margins, paddings, gaps |
typography | Font families, sizes, weights, line heights |
shadow | Box shadows, text shadows |
border | Radius, width |
animation | Duration, easing functions |
zIndex | Stacking layers |
Best Practices
1. Use semantic names - color-primary not color-blue 2. Maintain consistency - Same structure across all formats 3. Include metadata - Name, description, version 4. Version control - Track token changes 5. Single source of truth - Generate all formats from one spec
#!/usr/bin/env -S deno run --allow-read
/**
* analyze-accessibility.ts - Design Accessibility Auditor
*
* Checks HTML/component files for design-related accessibility issues
* including color contrast, focus states, touch targets, and motion.
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { basename, extname, resolve } from "jsr:@std/path@1.0.8";
import { walk } from "jsr:@std/fs@1.0.8/walk";
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "analyze-accessibility";
// WCAG contrast ratios
const CONTRAST_AA_NORMAL = 4.5;
const CONTRAST_AA_LARGE = 3.0;
const CONTRAST_AAA_NORMAL = 7.0;
const CONTRAST_AAA_LARGE = 4.5;
// === Types ===
export interface A11yIssue {
severity: "error" | "warning" | "info";
category: "contrast" | "focus" | "motion" | "touch-target" | "semantics" | "images";
element: string;
line?: number;
description: string;
wcagCriteria: string;
suggestion: string;
}
export interface A11yCheck {
category: string;
description: string;
passed: boolean;
}
export interface AccessibilityAuditResult {
filename: string;
issues: A11yIssue[];
warnings: A11yIssue[];
passed: A11yCheck[];
score: number;
}
interface AnalyzeOptions {
input: string;
level: "AA" | "AAA";
format: "json" | "summary";
pretty: boolean;
}
// === Color Utilities ===
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) {
// Try short hex
const short = /^#?([a-f\d])([a-f\d])([a-f\d])$/i.exec(hex);
if (short) {
return {
r: parseInt(short[1] + short[1], 16),
g: parseInt(short[2] + short[2], 16),
b: parseInt(short[3] + short[3], 16),
};
}
return null;
}
return {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
};
}
function getLuminance(r: number, g: number, b: number): number {
const [rs, gs, bs] = [r, g, b].map((c) => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
function getContrastRatio(color1: string, color2: string): number | null {
const rgb1 = hexToRgb(color1);
const rgb2 = hexToRgb(color2);
if (!rgb1 || !rgb2) return null;
const l1 = getLuminance(rgb1.r, rgb1.g, rgb1.b);
const l2 = getLuminance(rgb2.r, rgb2.g, rgb2.b);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// === Analysis Patterns ===
interface AnalysisPattern {
pattern: RegExp;
check: (match: RegExpMatchArray, content: string, lineNum: number) => A11yIssue | null;
}
const HTML_PATTERNS: AnalysisPattern[] = [
// Missing alt text on images
{
pattern: /<img[^>]*(?!alt=)[^>]*>/gi,
check: (match, _content, lineNum) => {
if (!match[0].includes("alt=")) {
return {
severity: "error",
category: "images",
element: match[0].substring(0, 50) + "...",
line: lineNum,
description: "Image missing alt attribute",
wcagCriteria: "1.1.1 Non-text Content",
suggestion: 'Add alt="" for decorative images or descriptive alt text for informative images',
};
}
return null;
},
},
// Empty alt text should be intentional (for decorative images)
{
pattern: /<img[^>]*alt=""[^>]*>/gi,
check: (match, _content, lineNum) => {
// Check if it has role="presentation" or aria-hidden
if (!match[0].includes('role="presentation"') && !match[0].includes("aria-hidden")) {
return {
severity: "info",
category: "images",
element: match[0].substring(0, 50) + "...",
line: lineNum,
description: "Image has empty alt - ensure this is decorative",
wcagCriteria: "1.1.1 Non-text Content",
suggestion: 'For decorative images, also add role="presentation" or aria-hidden="true"',
};
}
return null;
},
},
// Links without accessible names
{
pattern: /<a[^>]*>[\s]*<\/a>/gi,
check: (match, _content, lineNum) => ({
severity: "error",
category: "semantics",
element: match[0],
line: lineNum,
description: "Empty link - no accessible name",
wcagCriteria: "2.4.4 Link Purpose",
suggestion: "Add text content or aria-label to the link",
}),
},
// Buttons without accessible names
{
pattern: /<button[^>]*>[\s]*<\/button>/gi,
check: (match, _content, lineNum) => ({
severity: "error",
category: "semantics",
element: match[0],
line: lineNum,
description: "Empty button - no accessible name",
wcagCriteria: "4.1.2 Name, Role, Value",
suggestion: "Add text content or aria-label to the button",
}),
},
// Form inputs without labels
{
pattern: /<input[^>]*type=["'](?:text|email|password|tel|url|search|number)[^>]*>/gi,
check: (match, content, lineNum) => {
const idMatch = match[0].match(/id=["']([^"']+)["']/);
if (idMatch) {
const labelPattern = new RegExp(`for=["']${idMatch[1]}["']`);
if (labelPattern.test(content)) {
return null;
}
}
if (match[0].includes("aria-label") || match[0].includes("aria-labelledby")) {
return null;
}
return {
severity: "error",
category: "semantics",
element: match[0].substring(0, 50) + "...",
line: lineNum,
description: "Form input missing associated label",
wcagCriteria: "1.3.1 Info and Relationships",
suggestion: "Add a <label> with matching for attribute, or use aria-label",
};
},
},
// Check for outline:none without alternative focus styles
{
pattern: /outline:\s*(?:none|0);?/gi,
check: (match, _content, lineNum) => ({
severity: "warning",
category: "focus",
element: "CSS rule",
line: lineNum,
description: "outline:none detected - ensure alternative focus indicator exists",
wcagCriteria: "2.4.7 Focus Visible",
suggestion: "Replace with visible focus styles (box-shadow, border, etc.)",
}),
},
// Check for :focus without styles
{
pattern: /:focus\s*\{[^}]*\}/gi,
check: (match, _content, lineNum) => {
// Check if focus rule has meaningful styles
const rule = match[0];
if (
rule.includes("outline") ||
rule.includes("box-shadow") ||
rule.includes("border") ||
rule.includes("background")
) {
return null; // Has focus styling
}
return {
severity: "info",
category: "focus",
element: ":focus rule",
line: lineNum,
description: "Focus rule may lack visible indicator",
wcagCriteria: "2.4.7 Focus Visible",
suggestion: "Ensure :focus includes visible changes (outline, shadow, border)",
};
},
},
// Check for animations without reduced motion support
{
pattern: /@keyframes\s+[\w-]+/gi,
check: (match, content, _lineNum) => {
if (!content.includes("prefers-reduced-motion")) {
return {
severity: "warning",
category: "motion",
element: match[0],
line: undefined,
description: "Animation detected without prefers-reduced-motion support",
wcagCriteria: "2.3.3 Animation from Interactions",
suggestion:
"Add @media (prefers-reduced-motion: reduce) to disable/reduce animations",
};
}
return null;
},
},
// Check for small touch targets
{
pattern: /(?:width|height|min-width|min-height):\s*(\d+)(px|rem)/gi,
check: (match, _content, lineNum) => {
const value = parseInt(match[1]);
const unit = match[2];
// Convert to px for comparison
let pxValue = value;
if (unit === "rem") {
pxValue = value * 16;
}
// Only flag if it looks like a button/link target
if (pxValue < 44) {
return {
severity: "info",
category: "touch-target",
element: `size: ${match[0]}`,
line: lineNum,
description: `Small dimension (${pxValue}px) - verify touch target is at least 44x44px`,
wcagCriteria: "2.5.5 Target Size",
suggestion: "Ensure interactive elements have at least 44x44px touch target",
};
}
return null;
},
},
// Check for text in SVGs without title/desc
{
pattern: /<svg[^>]*>[\s\S]*?<\/svg>/gi,
check: (match, _content, lineNum) => {
const svg = match[0];
if (svg.includes("<text") && !svg.includes("<title") && !svg.includes("aria-label")) {
return {
severity: "warning",
category: "images",
element: "SVG with text",
line: lineNum,
description: "SVG with text content but no accessible name",
wcagCriteria: "1.1.1 Non-text Content",
suggestion: "Add <title> element or aria-label to SVG",
};
}
return null;
},
},
// Check for autoplay on video/audio
{
pattern: /<(?:video|audio)[^>]*autoplay[^>]*>/gi,
check: (match, _content, lineNum) => {
if (!match[0].includes("muted")) {
return {
severity: "error",
category: "motion",
element: match[0].substring(0, 50) + "...",
line: lineNum,
description: "Autoplay media without muted attribute",
wcagCriteria: "1.4.2 Audio Control",
suggestion: 'Add muted attribute or provide pause controls',
};
}
return null;
},
},
];
// === Analysis Functions ===
function getLineNumber(content: string, index: number): number {
return content.substring(0, index).split("\n").length;
}
function analyzeContent(content: string, filename: string): A11yIssue[] {
const issues: A11yIssue[] = [];
for (const { pattern, check } of HTML_PATTERNS) {
// Reset regex
pattern.lastIndex = 0;
let match;
while ((match = pattern.exec(content)) !== null) {
const lineNum = getLineNumber(content, match.index);
const issue = check(match, content, lineNum);
if (issue) {
issues.push(issue);
}
}
}
return issues;
}
function analyzeColorContrast(
content: string,
level: "AA" | "AAA"
): A11yIssue[] {
const issues: A11yIssue[] = [];
// Extract color pairs from inline styles
const colorPairs: Array<{ fg: string; bg: string; context: string }> = [];
// Look for color and background-color in same rule
const ruleRegex = /\{([^}]+)\}/g;
let match;
while ((match = ruleRegex.exec(content)) !== null) {
const rule = match[1];
const colorMatch = rule.match(/(?:^|;)\s*color:\s*(#[a-f0-9]{3,6})/i);
const bgMatch = rule.match(/background(?:-color)?:\s*(#[a-f0-9]{3,6})/i);
if (colorMatch && bgMatch) {
colorPairs.push({
fg: colorMatch[1],
bg: bgMatch[1],
context: match[0].substring(0, 50),
});
}
}
// Check contrast for each pair
const minRatio = level === "AAA" ? CONTRAST_AAA_NORMAL : CONTRAST_AA_NORMAL;
for (const pair of colorPairs) {
const ratio = getContrastRatio(pair.fg, pair.bg);
if (ratio && ratio < minRatio) {
issues.push({
severity: "error",
category: "contrast",
element: pair.context,
description: `Insufficient contrast ratio: ${ratio.toFixed(2)}:1 (requires ${minRatio}:1 for ${level})`,
wcagCriteria: "1.4.3 Contrast (Minimum)",
suggestion: `Increase contrast between ${pair.fg} and ${pair.bg}`,
});
}
}
return issues;
}
function generatePassedChecks(issues: A11yIssue[]): A11yCheck[] {
const checks: A11yCheck[] = [];
const categories = ["contrast", "focus", "motion", "touch-target", "semantics", "images"];
for (const category of categories) {
const categoryIssues = issues.filter((i) => i.category === category);
const hasErrors = categoryIssues.some((i) => i.severity === "error");
if (!hasErrors) {
checks.push({
category,
description: `No ${category} errors detected`,
passed: true,
});
}
}
return checks;
}
function calculateScore(issues: A11yIssue[]): number {
// Start with 100, deduct points for issues
let score = 100;
for (const issue of issues) {
switch (issue.severity) {
case "error":
score -= 15;
break;
case "warning":
score -= 5;
break;
case "info":
score -= 1;
break;
}
}
return Math.max(0, Math.min(100, score));
}
// === Core Analysis Function ===
export async function analyzeAccessibility(
options: AnalyzeOptions
): Promise<AccessibilityAuditResult[]> {
const results: AccessibilityAuditResult[] = [];
const inputPath = resolve(options.input);
// Supported extensions
const exts = [".html", ".htm", ".tsx", ".jsx", ".vue", ".svelte", ".css"];
// Collect files
const files: string[] = [];
try {
const stat = await Deno.stat(inputPath);
if (stat.isDirectory) {
for await (const entry of walk(inputPath, {
exts: exts.map((e) => e.slice(1)),
includeDirs: false,
})) {
files.push(entry.path);
}
} else if (stat.isFile && exts.includes(extname(inputPath))) {
files.push(inputPath);
} else {
throw new Error(`Input must be a supported file or directory: ${inputPath}`);
}
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
throw new Error(`File or directory not found: ${inputPath}`);
}
throw error;
}
if (files.length === 0) {
throw new Error(`No supported files found in: ${inputPath}`);
}
// Analyze each file
for (const file of files) {
const content = await Deno.readTextFile(file);
const issues = [
...analyzeContent(content, basename(file)),
...analyzeColorContrast(content, options.level),
];
const errors = issues.filter((i) => i.severity === "error");
const warnings = issues.filter((i) => i.severity === "warning" || i.severity === "info");
const passed = generatePassedChecks(issues);
const score = calculateScore(issues);
results.push({
filename: basename(file),
issues: errors,
warnings,
passed,
score,
});
}
return results;
}
// === Output Formatting ===
function formatSummary(results: AccessibilityAuditResult[]): string {
const lines: string[] = [];
for (const result of results) {
lines.push(`\n${"=".repeat(60)}`);
lines.push(`FILE: ${result.filename}`);
lines.push(`SCORE: ${result.score}/100`);
lines.push("=".repeat(60));
if (result.issues.length > 0) {
lines.push("\nERRORS:");
for (const issue of result.issues) {
lines.push(` [${issue.category.toUpperCase()}] ${issue.description}`);
if (issue.line) lines.push(` Line: ${issue.line}`);
lines.push(` WCAG: ${issue.wcagCriteria}`);
lines.push(` Fix: ${issue.suggestion}`);
}
}
if (result.warnings.length > 0) {
lines.push("\nWARNINGS:");
for (const warning of result.warnings) {
lines.push(` [${warning.category.toUpperCase()}] ${warning.description}`);
if (warning.line) lines.push(` Line: ${warning.line}`);
lines.push(` Fix: ${warning.suggestion}`);
}
}
if (result.passed.length > 0) {
lines.push("\nPASSED:");
for (const check of result.passed) {
lines.push(` [OK] ${check.description}`);
}
}
}
// Summary
const totalErrors = results.reduce((sum, r) => sum + r.issues.length, 0);
const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0);
const avgScore = results.reduce((sum, r) => sum + r.score, 0) / results.length;
lines.push(`\n${"=".repeat(60)}`);
lines.push("SUMMARY");
lines.push("=".repeat(60));
lines.push(`Files analyzed: ${results.length}`);
lines.push(`Total errors: ${totalErrors}`);
lines.push(`Total warnings: ${totalWarnings}`);
lines.push(`Average score: ${avgScore.toFixed(1)}/100`);
return lines.join("\n");
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Design Accessibility Auditor
Checks HTML/component files for design-related accessibility issues
including color contrast, focus states, touch targets, and motion.
Usage:
deno run --allow-read scripts/${SCRIPT_NAME}.ts <input> [options]
Arguments:
<input> HTML/component file or directory
Options:
-h, --help Show this help message
-v, --version Show version
--level <AA|AAA> WCAG conformance level (default: AA)
--format <type> Output format: json (default), summary
--pretty Pretty-print JSON output
Supported Files:
.html, .htm, .tsx, .jsx, .vue, .svelte, .css
Checks Performed:
- Color contrast ratios
- Focus indicator presence
- Touch target sizes
- Motion and animation accessibility
- Image alt text
- Form label associations
- Semantic structure
Examples:
# Audit a component file
deno run --allow-read scripts/${SCRIPT_NAME}.ts Button.tsx
# Audit all components with AAA level
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src/components --level AAA
# Get a human-readable summary
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src --format summary
`);
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "version", "pretty"],
string: ["level", "format"],
alias: { h: "help", v: "version" },
default: { level: "AA", format: "json", pretty: false },
});
if (parsed.help) {
printHelp();
Deno.exit(0);
}
if (parsed.version) {
console.log(`${SCRIPT_NAME} v${VERSION}`);
Deno.exit(0);
}
const input = parsed._[0] as string;
if (!input) {
console.error("Error: Input file or directory required");
console.error("Use --help for usage information");
Deno.exit(1);
}
if (!["AA", "AAA"].includes(parsed.level)) {
console.error(`Error: Invalid level '${parsed.level}'. Use AA or AAA.`);
Deno.exit(1);
}
const options: AnalyzeOptions = {
input,
level: parsed.level as "AA" | "AAA",
format: parsed.format as "json" | "summary",
pretty: parsed.pretty,
};
try {
const results = await analyzeAccessibility(options);
if (options.format === "summary") {
console.log(formatSummary(results));
} else {
const output = options.pretty
? JSON.stringify(results, null, 2)
: JSON.stringify(results);
console.log(output);
}
// Exit with error code if there are issues
const hasErrors = results.some((r) => r.issues.length > 0);
if (hasErrors) {
Deno.exit(1);
}
} catch (error) {
console.error(`Error: ${(error as Error).message}`);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read
/**
* analyze-styles.ts - CSS Style Auditor
*
* Analyzes CSS files to extract color, typography, spacing patterns
* and identify design inconsistencies.
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { basename, extname, resolve } from "jsr:@std/path@1.0.8";
import { walk } from "jsr:@std/fs@1.0.8/walk";
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "analyze-styles";
// === Types ===
export interface ColorUsage {
value: string;
normalizedHex: string;
count: number;
locations: string[];
suggestedTokenName?: string;
}
export interface TypographyUsage {
property: "font-family" | "font-size" | "font-weight" | "line-height";
value: string;
count: number;
locations: string[];
}
export interface SpacingUsage {
property: string;
value: string;
count: number;
locations: string[];
}
export interface StyleInconsistency {
type: "color" | "font" | "spacing" | "z-index";
description: string;
values: string[];
suggestion: string;
}
export interface StyleAuditResult {
filename: string;
summary: {
totalSelectors: number;
totalDeclarations: number;
uniqueColors: number;
uniqueFonts: number;
uniqueSpacingValues: number;
};
colors: ColorUsage[];
typography: TypographyUsage[];
spacing: SpacingUsage[];
inconsistencies: StyleInconsistency[];
recommendations: string[];
}
interface AnalyzeOptions {
input: string;
tokensFile?: string;
pretty: boolean;
format: "json" | "summary";
}
// === Color Utilities ===
const COLOR_REGEX =
/#(?:[0-9a-fA-F]{3}){1,2}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|hsla\([^)]+\)|(?:transparent|currentColor|inherit)\b/gi;
const NAMED_COLORS: Record<string, string> = {
black: "#000000",
white: "#ffffff",
red: "#ff0000",
green: "#008000",
blue: "#0000ff",
yellow: "#ffff00",
cyan: "#00ffff",
magenta: "#ff00ff",
gray: "#808080",
grey: "#808080",
};
function normalizeColor(color: string): string {
const lower = color.toLowerCase().trim();
// Handle named colors
if (NAMED_COLORS[lower]) {
return NAMED_COLORS[lower];
}
// Handle hex shorthand
if (/^#[0-9a-f]{3}$/i.test(lower)) {
const r = lower[1];
const g = lower[2];
const b = lower[3];
return `#${r}${r}${g}${g}${b}${b}`;
}
// Handle rgb/rgba
const rgbMatch = lower.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (rgbMatch) {
const r = parseInt(rgbMatch[1]).toString(16).padStart(2, "0");
const g = parseInt(rgbMatch[2]).toString(16).padStart(2, "0");
const b = parseInt(rgbMatch[3]).toString(16).padStart(2, "0");
return `#${r}${g}${b}`;
}
// Already normalized hex or other
return lower;
}
function suggestTokenName(hex: string, index: number): string {
// Simple heuristic based on luminance
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
if (luminance > 0.9) return `color-light-${index}`;
if (luminance < 0.1) return `color-dark-${index}`;
if (r > g && r > b) return `color-red-${index}`;
if (g > r && g > b) return `color-green-${index}`;
if (b > r && b > g) return `color-blue-${index}`;
return `color-neutral-${index}`;
}
// === CSS Parsing ===
interface Declaration {
property: string;
value: string;
selector: string;
}
function parseCSS(css: string): Declaration[] {
const declarations: Declaration[] = [];
let currentSelector = "";
// Remove comments
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, "");
// Simple regex-based parser (handles most common cases)
const ruleRegex = /([^{]+)\{([^}]+)\}/g;
let match;
while ((match = ruleRegex.exec(cleaned)) !== null) {
currentSelector = match[1].trim();
const body = match[2];
// Parse declarations
const declRegex = /([a-z-]+)\s*:\s*([^;]+);?/gi;
let declMatch;
while ((declMatch = declRegex.exec(body)) !== null) {
declarations.push({
property: declMatch[1].toLowerCase().trim(),
value: declMatch[2].trim(),
selector: currentSelector,
});
}
}
return declarations;
}
// === Analysis Functions ===
function analyzeColors(declarations: Declaration[]): ColorUsage[] {
const colorMap = new Map<string, ColorUsage>();
const colorProperties = [
"color",
"background-color",
"background",
"border-color",
"border",
"border-top-color",
"border-right-color",
"border-bottom-color",
"border-left-color",
"outline-color",
"box-shadow",
"text-shadow",
"fill",
"stroke",
];
for (const decl of declarations) {
if (colorProperties.some((p) => decl.property.includes(p))) {
const colors = decl.value.match(COLOR_REGEX) || [];
for (const color of colors) {
if (color === "transparent" || color === "currentColor" || color === "inherit") {
continue;
}
const normalized = normalizeColor(color);
const location = `${decl.selector}:${decl.property}`;
if (colorMap.has(normalized)) {
const existing = colorMap.get(normalized)!;
existing.count++;
if (!existing.locations.includes(location)) {
existing.locations.push(location);
}
} else {
colorMap.set(normalized, {
value: color,
normalizedHex: normalized,
count: 1,
locations: [location],
});
}
}
}
}
// Add suggested token names
const colors = Array.from(colorMap.values()).sort((a, b) => b.count - a.count);
colors.forEach((c, i) => {
c.suggestedTokenName = suggestTokenName(c.normalizedHex, i + 1);
});
return colors;
}
function analyzeTypography(declarations: Declaration[]): TypographyUsage[] {
const typoMap = new Map<string, TypographyUsage>();
const typoProperties = ["font-family", "font-size", "font-weight", "line-height"];
for (const decl of declarations) {
if (typoProperties.includes(decl.property)) {
const key = `${decl.property}:${decl.value}`;
const location = `${decl.selector}:${decl.property}`;
if (typoMap.has(key)) {
const existing = typoMap.get(key)!;
existing.count++;
if (!existing.locations.includes(location)) {
existing.locations.push(location);
}
} else {
typoMap.set(key, {
property: decl.property as TypographyUsage["property"],
value: decl.value,
count: 1,
locations: [location],
});
}
}
}
return Array.from(typoMap.values()).sort((a, b) => b.count - a.count);
}
function analyzeSpacing(declarations: Declaration[]): SpacingUsage[] {
const spacingMap = new Map<string, SpacingUsage>();
const spacingProperties = [
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"gap",
"row-gap",
"column-gap",
];
for (const decl of declarations) {
if (spacingProperties.includes(decl.property)) {
const key = `${decl.property}:${decl.value}`;
const location = `${decl.selector}:${decl.property}`;
if (spacingMap.has(key)) {
const existing = spacingMap.get(key)!;
existing.count++;
if (!existing.locations.includes(location)) {
existing.locations.push(location);
}
} else {
spacingMap.set(key, {
property: decl.property,
value: decl.value,
count: 1,
locations: [location],
});
}
}
}
return Array.from(spacingMap.values()).sort((a, b) => b.count - a.count);
}
function findInconsistencies(
colors: ColorUsage[],
typography: TypographyUsage[],
spacing: SpacingUsage[]
): StyleInconsistency[] {
const inconsistencies: StyleInconsistency[] = [];
// Check for similar but different colors
const hexColors = colors.map((c) => c.normalizedHex);
for (let i = 0; i < hexColors.length; i++) {
for (let j = i + 1; j < hexColors.length; j++) {
const c1 = hexColors[i];
const c2 = hexColors[j];
if (c1 && c2 && areColorsSimilar(c1, c2)) {
inconsistencies.push({
type: "color",
description: `Similar colors found that could be consolidated`,
values: [c1, c2],
suggestion: `Consider using a single color token for these similar values`,
});
}
}
}
// Check for too many unique font families
const fontFamilies = typography.filter((t) => t.property === "font-family");
if (fontFamilies.length > 3) {
inconsistencies.push({
type: "font",
description: `Too many font families (${fontFamilies.length}) - consider limiting to 2-3`,
values: fontFamilies.map((f) => f.value),
suggestion: "Use a display font, body font, and optionally a monospace font",
});
}
// Check for inconsistent spacing values
const spacingValues = [...new Set(spacing.map((s) => s.value))];
if (spacingValues.length > 10) {
inconsistencies.push({
type: "spacing",
description: `Many unique spacing values (${spacingValues.length}) - consider a consistent scale`,
values: spacingValues.slice(0, 10),
suggestion: "Use a spacing scale (4px, 8px, 12px, 16px, 24px, 32px, etc.)",
});
}
return inconsistencies;
}
function areColorsSimilar(hex1: string, hex2: string): boolean {
if (!hex1.startsWith("#") || !hex2.startsWith("#")) return false;
const r1 = parseInt(hex1.slice(1, 3), 16);
const g1 = parseInt(hex1.slice(3, 5), 16);
const b1 = parseInt(hex1.slice(5, 7), 16);
const r2 = parseInt(hex2.slice(1, 3), 16);
const g2 = parseInt(hex2.slice(3, 5), 16);
const b2 = parseInt(hex2.slice(5, 7), 16);
const distance = Math.sqrt(
Math.pow(r1 - r2, 2) + Math.pow(g1 - g2, 2) + Math.pow(b1 - b2, 2)
);
// Colors within distance of 30 are considered similar
return distance > 0 && distance < 30;
}
function generateRecommendations(
result: Omit<StyleAuditResult, "recommendations">
): string[] {
const recommendations: string[] = [];
// Color recommendations
if (result.colors.length > 15) {
recommendations.push(
`Consider reducing color palette from ${result.colors.length} to 10-15 colors with a design token system`
);
}
// Typography recommendations
const genericFonts = result.typography.filter(
(t) =>
t.property === "font-family" &&
/arial|helvetica|roboto|inter|system-ui/i.test(t.value)
);
if (genericFonts.length > 0) {
recommendations.push(
"Replace generic fonts (Arial, Helvetica, Roboto, Inter) with distinctive alternatives"
);
}
// Spacing recommendations
const pixelSpacing = result.spacing.filter((s) => /^\d+px$/.test(s.value));
if (pixelSpacing.length > 5) {
recommendations.push(
"Convert pixel-based spacing to rem units for better accessibility"
);
}
// Inconsistency-based recommendations
if (result.inconsistencies.length > 0) {
recommendations.push(
`Address ${result.inconsistencies.length} design inconsistencies identified in the audit`
);
}
return recommendations;
}
// === Core Analysis Function ===
export async function analyzeStyles(options: AnalyzeOptions): Promise<StyleAuditResult[]> {
const results: StyleAuditResult[] = [];
const inputPath = resolve(options.input);
// Collect CSS files
const cssFiles: string[] = [];
try {
const stat = await Deno.stat(inputPath);
if (stat.isDirectory) {
for await (const entry of walk(inputPath, {
exts: [".css"],
includeDirs: false,
})) {
cssFiles.push(entry.path);
}
} else if (stat.isFile && extname(inputPath) === ".css") {
cssFiles.push(inputPath);
} else {
throw new Error(`Input must be a CSS file or directory: ${inputPath}`);
}
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
throw new Error(`File or directory not found: ${inputPath}`);
}
throw error;
}
if (cssFiles.length === 0) {
throw new Error(`No CSS files found in: ${inputPath}`);
}
// Analyze each file
for (const file of cssFiles) {
const css = await Deno.readTextFile(file);
const declarations = parseCSS(css);
const colors = analyzeColors(declarations);
const typography = analyzeTypography(declarations);
const spacing = analyzeSpacing(declarations);
const inconsistencies = findInconsistencies(colors, typography, spacing);
const uniqueSelectors = new Set(declarations.map((d) => d.selector));
const partialResult = {
filename: basename(file),
summary: {
totalSelectors: uniqueSelectors.size,
totalDeclarations: declarations.length,
uniqueColors: colors.length,
uniqueFonts: typography.filter((t) => t.property === "font-family").length,
uniqueSpacingValues: spacing.length,
},
colors,
typography,
spacing,
inconsistencies,
};
results.push({
...partialResult,
recommendations: generateRecommendations(partialResult),
});
}
return results;
}
// === Output Formatting ===
function formatSummary(results: StyleAuditResult[]): string {
const lines: string[] = [];
for (const result of results) {
lines.push(`\n${"=".repeat(60)}`);
lines.push(`FILE: ${result.filename}`);
lines.push("=".repeat(60));
lines.push("\nSUMMARY:");
lines.push(` Selectors: ${result.summary.totalSelectors}`);
lines.push(` Declarations: ${result.summary.totalDeclarations}`);
lines.push(` Unique Colors: ${result.summary.uniqueColors}`);
lines.push(` Unique Fonts: ${result.summary.uniqueFonts}`);
lines.push(` Unique Spacing: ${result.summary.uniqueSpacingValues}`);
if (result.colors.length > 0) {
lines.push("\nTOP COLORS:");
result.colors.slice(0, 5).forEach((c) => {
lines.push(` ${c.normalizedHex} (${c.count}x) → ${c.suggestedTokenName}`);
});
}
if (result.typography.length > 0) {
lines.push("\nTYPOGRAPHY:");
result.typography
.filter((t) => t.property === "font-family")
.slice(0, 3)
.forEach((t) => {
lines.push(` ${t.value} (${t.count}x)`);
});
}
if (result.inconsistencies.length > 0) {
lines.push("\nINCONSISTENCIES:");
result.inconsistencies.forEach((i) => {
lines.push(` [${i.type.toUpperCase()}] ${i.description}`);
});
}
if (result.recommendations.length > 0) {
lines.push("\nRECOMMENDATIONS:");
result.recommendations.forEach((r) => {
lines.push(` • ${r}`);
});
}
}
return lines.join("\n");
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - CSS Style Auditor
Analyzes CSS files to extract color, typography, spacing patterns
and identify design inconsistencies.
Usage:
deno run --allow-read scripts/${SCRIPT_NAME}.ts <input> [options]
Arguments:
<input> CSS file or directory containing CSS files
Options:
-h, --help Show this help message
-v, --version Show version
--tokens <file> Compare against existing design tokens file
--pretty Pretty-print JSON output
--format <type> Output format: json (default), summary
Examples:
# Analyze a single CSS file
deno run --allow-read scripts/${SCRIPT_NAME}.ts styles.css
# Analyze all CSS in a directory with pretty output
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src --pretty
# Get a human-readable summary
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src --format summary
# Compare against existing tokens
deno run --allow-read scripts/${SCRIPT_NAME}.ts styles.css --tokens tokens.json
`);
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "version", "pretty"],
string: ["tokens", "format"],
alias: { h: "help", v: "version" },
default: { format: "json", pretty: false },
});
if (parsed.help) {
printHelp();
Deno.exit(0);
}
if (parsed.version) {
console.log(`${SCRIPT_NAME} v${VERSION}`);
Deno.exit(0);
}
const input = parsed._[0] as string;
if (!input) {
console.error("Error: Input file or directory required");
console.error("Use --help for usage information");
Deno.exit(1);
}
const options: AnalyzeOptions = {
input,
tokensFile: parsed.tokens,
pretty: parsed.pretty,
format: parsed.format as "json" | "summary",
};
try {
const results = await analyzeStyles(options);
if (options.format === "summary") {
console.log(formatSummary(results));
} else {
const output = options.pretty
? JSON.stringify(results, null, 2)
: JSON.stringify(results);
console.log(output);
}
} catch (error) {
console.error(`Error: ${(error as Error).message}`);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read
/**
* extract-tokens.ts - Design Token Extractor
*
* Extracts design tokens from CSS files containing CSS custom properties
* and outputs in various standardized formats.
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { basename, extname, resolve } from "jsr:@std/path@1.0.8";
import { walk } from "jsr:@std/fs@1.0.8/walk";
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "extract-tokens";
type OutputFormat = "css" | "scss" | "tailwind" | "style-dictionary" | "tokens-studio" | "json";
// === Types ===
export interface TokenValue {
value: string;
type: string;
description?: string;
usageCount?: number;
}
export interface ExtractedTokens {
source: string;
format: OutputFormat;
tokens: {
color: Record<string, TokenValue>;
typography: Record<string, TokenValue>;
spacing: Record<string, TokenValue>;
shadow: Record<string, TokenValue>;
border: Record<string, TokenValue>;
animation: Record<string, TokenValue>;
other: Record<string, TokenValue>;
};
unmapped: Array<{
property: string;
value: string;
count: number;
suggestion: string;
}>;
}
interface ExtractOptions {
input: string;
format: OutputFormat;
outputCss: boolean;
pretty: boolean;
}
// === Token Classification ===
const TOKEN_PATTERNS: Record<string, RegExp[]> = {
color: [
/color/i,
/background/i,
/border-color/i,
/fill/i,
/stroke/i,
/shadow/i,
/bg/i,
/fg/i,
/text(?!-)/i,
],
typography: [
/font/i,
/text-/i,
/line-height/i,
/letter-spacing/i,
/word-spacing/i,
],
spacing: [
/space/i,
/gap/i,
/margin/i,
/padding/i,
/inset/i,
],
shadow: [
/shadow/i,
/elevation/i,
],
border: [
/border/i,
/radius/i,
/outline/i,
],
animation: [
/duration/i,
/timing/i,
/easing/i,
/delay/i,
/transition/i,
/animation/i,
],
};
function classifyToken(name: string, value: string): string {
// Check name patterns
for (const [category, patterns] of Object.entries(TOKEN_PATTERNS)) {
if (patterns.some((p) => p.test(name))) {
return category;
}
}
// Check value patterns
if (/^#|^rgb|^hsl|^oklch|transparent|currentColor/i.test(value)) {
return "color";
}
if (/^\d+(\.\d+)?(px|rem|em|%)$/.test(value)) {
if (/radius/i.test(name)) return "border";
return "spacing";
}
if (/^\d+(\.\d+)?(ms|s)$/.test(value)) {
return "animation";
}
if (/^(sans-serif|serif|monospace|cursive|fantasy|system-ui)/i.test(value)) {
return "typography";
}
return "other";
}
function inferTokenType(category: string, value: string): string {
if (category === "color") return "color";
if (category === "typography") {
if (/^\d/.test(value)) return "dimension";
return "fontFamily";
}
if (category === "spacing") return "dimension";
if (category === "shadow") return "shadow";
if (category === "border") return "dimension";
if (category === "animation") return "duration";
return "other";
}
// === CSS Parsing ===
interface CSSVariable {
name: string;
value: string;
count: number;
}
function extractCSSVariables(css: string): CSSVariable[] {
const variables = new Map<string, CSSVariable>();
// Remove comments
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, "");
// Find variable definitions (--name: value)
const varDefRegex = /(--[\w-]+)\s*:\s*([^;]+);/g;
let match;
while ((match = varDefRegex.exec(cleaned)) !== null) {
const name = match[1].trim();
const value = match[2].trim();
if (variables.has(name)) {
variables.get(name)!.count++;
} else {
variables.set(name, { name, value, count: 1 });
}
}
return Array.from(variables.values());
}
function extractRepeatedValues(css: string): Map<string, number> {
const valueCount = new Map<string, number>();
// Remove comments
const cleaned = css.replace(/\/\*[\s\S]*?\*\//g, "");
// Find all property values
const declRegex = /[a-z-]+\s*:\s*([^;{}]+);/gi;
let match;
while ((match = declRegex.exec(cleaned)) !== null) {
const value = match[1].trim();
// Skip var() references and very short values
if (!value.startsWith("var(") && value.length > 2) {
valueCount.set(value, (valueCount.get(value) || 0) + 1);
}
}
return valueCount;
}
// === Token Extraction ===
export async function extractTokens(options: ExtractOptions): Promise<ExtractedTokens> {
const inputPath = resolve(options.input);
// Collect CSS files
const cssContents: string[] = [];
let sourceName = "";
try {
const stat = await Deno.stat(inputPath);
if (stat.isDirectory) {
sourceName = basename(inputPath);
for await (const entry of walk(inputPath, {
exts: [".css"],
includeDirs: false,
})) {
cssContents.push(await Deno.readTextFile(entry.path));
}
} else if (stat.isFile && extname(inputPath) === ".css") {
sourceName = basename(inputPath);
cssContents.push(await Deno.readTextFile(inputPath));
} else {
throw new Error(`Input must be a CSS file or directory: ${inputPath}`);
}
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
throw new Error(`File or directory not found: ${inputPath}`);
}
throw error;
}
if (cssContents.length === 0) {
throw new Error(`No CSS files found in: ${inputPath}`);
}
const combinedCSS = cssContents.join("\n");
// Extract CSS variables
const cssVariables = extractCSSVariables(combinedCSS);
// Extract repeated values that could become tokens
const repeatedValues = extractRepeatedValues(combinedCSS);
// Classify tokens
const tokens: ExtractedTokens["tokens"] = {
color: {},
typography: {},
spacing: {},
shadow: {},
border: {},
animation: {},
other: {},
};
for (const variable of cssVariables) {
const category = classifyToken(variable.name, variable.value);
const tokenName = variable.name.replace(/^--/, "");
tokens[category as keyof typeof tokens][tokenName] = {
value: variable.value,
type: inferTokenType(category, variable.value),
usageCount: variable.count,
};
}
// Find unmapped repeated values
const unmapped: ExtractedTokens["unmapped"] = [];
for (const [value, count] of repeatedValues) {
if (count >= 3) {
// Value used 3+ times
const category = classifyToken("", value);
const suggestion = `--${category}-${unmapped.length + 1}`;
// Check if this value is already a token
const isTokenized = Object.values(tokens).some((cat) =>
Object.values(cat).some((t) => t.value === value)
);
if (!isTokenized) {
unmapped.push({
property: category,
value,
count,
suggestion,
});
}
}
}
return {
source: sourceName,
format: options.format,
tokens,
unmapped: unmapped.sort((a, b) => b.count - a.count).slice(0, 20),
};
}
// === Output Formatting ===
function formatAsCSS(tokens: ExtractedTokens): string {
const lines: string[] = ["/* Design Tokens - Generated by extract-tokens */", "", ":root {"];
for (const [category, categoryTokens] of Object.entries(tokens.tokens)) {
const entries = Object.entries(categoryTokens);
if (entries.length === 0) continue;
lines.push(` /* ${category} */`);
for (const [name, token] of entries) {
lines.push(` --${name}: ${token.value};`);
}
lines.push("");
}
lines.push("}");
return lines.join("\n");
}
function formatAsSCSS(tokens: ExtractedTokens): string {
const lines: string[] = ["// Design Tokens - Generated by extract-tokens", ""];
for (const [category, categoryTokens] of Object.entries(tokens.tokens)) {
const entries = Object.entries(categoryTokens);
if (entries.length === 0) continue;
lines.push(`// ${category}`);
for (const [name, token] of entries) {
const scssName = name.replace(/-/g, "_");
lines.push(`$${scssName}: ${token.value};`);
}
lines.push("");
}
return lines.join("\n");
}
function formatAsTailwind(tokens: ExtractedTokens): string {
const config: Record<string, Record<string, string>> = {
colors: {},
spacing: {},
fontFamily: {},
fontSize: {},
boxShadow: {},
borderRadius: {},
transitionDuration: {},
};
for (const [name, token] of Object.entries(tokens.tokens.color)) {
config.colors[name] = token.value;
}
for (const [name, token] of Object.entries(tokens.tokens.spacing)) {
config.spacing[name] = token.value;
}
for (const [name, token] of Object.entries(tokens.tokens.typography)) {
if (token.type === "fontFamily") {
config.fontFamily[name] = token.value;
} else {
config.fontSize[name] = token.value;
}
}
for (const [name, token] of Object.entries(tokens.tokens.shadow)) {
config.boxShadow[name] = token.value;
}
for (const [name, token] of Object.entries(tokens.tokens.border)) {
config.borderRadius[name] = token.value;
}
for (const [name, token] of Object.entries(tokens.tokens.animation)) {
config.transitionDuration[name] = token.value;
}
// Remove empty categories
for (const key of Object.keys(config)) {
if (Object.keys(config[key]).length === 0) {
delete config[key];
}
}
return `// Tailwind Config Extension - Generated by extract-tokens
// Add this to your tailwind.config.js theme.extend
module.exports = {
theme: {
extend: ${JSON.stringify(config, null, 6).replace(/"([^"]+)":/g, "$1:")}
}
};`;
}
function formatAsStyleDictionary(tokens: ExtractedTokens): string {
const sdTokens: Record<string, unknown> = {};
for (const [category, categoryTokens] of Object.entries(tokens.tokens)) {
if (Object.keys(categoryTokens).length === 0) continue;
sdTokens[category] = {};
for (const [name, token] of Object.entries(categoryTokens)) {
(sdTokens[category] as Record<string, unknown>)[name] = {
value: token.value,
type: token.type,
};
}
}
return JSON.stringify(sdTokens, null, 2);
}
function formatAsTokensStudio(tokens: ExtractedTokens): string {
const tsTokens: Record<string, unknown> = {};
for (const [category, categoryTokens] of Object.entries(tokens.tokens)) {
if (Object.keys(categoryTokens).length === 0) continue;
tsTokens[category] = {};
for (const [name, token] of Object.entries(categoryTokens)) {
(tsTokens[category] as Record<string, unknown>)[name] = {
value: token.value,
type: token.type,
$extensions: {
"studio.tokens": {
modify: {},
},
},
};
}
}
return JSON.stringify(tsTokens, null, 2);
}
function formatOutput(tokens: ExtractedTokens, options: ExtractOptions): string {
switch (options.format) {
case "css":
return formatAsCSS(tokens);
case "scss":
return formatAsSCSS(tokens);
case "tailwind":
return formatAsTailwind(tokens);
case "style-dictionary":
return formatAsStyleDictionary(tokens);
case "tokens-studio":
return formatAsTokensStudio(tokens);
case "json":
default:
return options.pretty
? JSON.stringify(tokens, null, 2)
: JSON.stringify(tokens);
}
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Design Token Extractor
Extracts design tokens from CSS files containing CSS custom properties
and outputs in various standardized formats.
Usage:
deno run --allow-read scripts/${SCRIPT_NAME}.ts <input> [options]
Arguments:
<input> CSS file or directory containing CSS files
Options:
-h, --help Show this help message
-v, --version Show version
--format <type> Output format: css, scss, tailwind, style-dictionary,
tokens-studio, json (default: json)
--output-css Also output CSS variables (with json format)
--pretty Pretty-print JSON output
Formats:
css CSS custom properties (:root { --token: value; })
scss SCSS variables ($token: value;)
tailwind Tailwind config extension object
style-dictionary Style Dictionary JSON format
tokens-studio Tokens Studio JSON format
json Raw JSON with all metadata
Examples:
# Extract tokens as CSS variables
deno run --allow-read scripts/${SCRIPT_NAME}.ts styles.css --format css
# Extract to Tailwind config
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src --format tailwind
# Extract to Style Dictionary format
deno run --allow-read scripts/${SCRIPT_NAME}.ts ./src --format style-dictionary
# Get raw JSON with unmapped values
deno run --allow-read scripts/${SCRIPT_NAME}.ts styles.css --pretty
`);
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "version", "pretty", "output-css"],
string: ["format"],
alias: { h: "help", v: "version" },
default: { format: "json", pretty: false },
});
if (parsed.help) {
printHelp();
Deno.exit(0);
}
if (parsed.version) {
console.log(`${SCRIPT_NAME} v${VERSION}`);
Deno.exit(0);
}
const input = parsed._[0] as string;
if (!input) {
console.error("Error: Input file or directory required");
console.error("Use --help for usage information");
Deno.exit(1);
}
const validFormats: OutputFormat[] = [
"css",
"scss",
"tailwind",
"style-dictionary",
"tokens-studio",
"json",
];
if (!validFormats.includes(parsed.format as OutputFormat)) {
console.error(`Error: Invalid format '${parsed.format}'`);
console.error(`Valid formats: ${validFormats.join(", ")}`);
Deno.exit(1);
}
const options: ExtractOptions = {
input,
format: parsed.format as OutputFormat,
outputCss: parsed["output-css"],
pretty: parsed.pretty,
};
try {
const tokens = await extractTokens(options);
console.log(formatOutput(tokens, options));
} catch (error) {
console.error(`Error: ${(error as Error).message}`);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
#!/usr/bin/env -S deno run --allow-read --allow-write
/**
* generate-component.ts - Component Template Generator
*
* Generates component templates with design-focused styling
* for multiple frameworks (React, Vue, Svelte, HTML).
*/
import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args";
import { resolve, join } from "jsr:@std/path@1.0.8";
import { ensureDir } from "jsr:@std/fs@1.0.8/ensure-dir";
// === Constants ===
const VERSION = "1.0.0";
const SCRIPT_NAME = "generate-component";
type Framework = "react" | "vue" | "svelte" | "html";
type Styling = "css" | "tailwind" | "css-modules" | "styled-components" | "emotion";
type Aesthetic = "minimal" | "bold" | "organic" | "brutalist" | "glassmorphism" | "neumorphism";
type Animation = "none" | "subtle" | "expressive";
type ComponentType = "button" | "card" | "input" | "modal" | "navigation" | "hero" | "custom";
// === Types ===
export interface ComponentSpec {
name: string;
type: ComponentType;
framework: Framework;
styling: Styling;
aesthetic?: {
style?: Aesthetic;
animation?: Animation;
darkMode?: boolean;
};
tokens?: string;
props?: Record<string, { type: string; default?: string; required?: boolean }>;
slots?: string[];
variants?: string[];
}
interface GenerateOptions {
specFile?: string;
name: string;
type: ComponentType;
framework: Framework;
styling: Styling;
aesthetic?: Aesthetic;
animation: Animation;
darkMode: boolean;
tokens?: string;
outputDir: string;
}
// === Style Templates ===
const AESTHETIC_STYLES: Record<Aesthetic, Record<string, string>> = {
minimal: {
base: "border border-gray-200 bg-white text-gray-900",
hover: "hover:border-gray-300",
focus: "focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2",
active: "active:bg-gray-50",
transition: "transition-colors duration-150",
},
bold: {
base: "bg-gradient-to-r from-purple-600 to-pink-600 text-white font-bold",
hover: "hover:from-purple-700 hover:to-pink-700 hover:scale-105",
focus: "focus:outline-none focus:ring-4 focus:ring-purple-300",
active: "active:scale-95",
transition: "transition-all duration-200 transform",
},
organic: {
base: "bg-amber-50 text-amber-900 border-2 border-amber-200 rounded-3xl",
hover: "hover:bg-amber-100 hover:border-amber-300",
focus: "focus:outline-none focus:ring-2 focus:ring-amber-400",
active: "active:bg-amber-200",
transition: "transition-all duration-300 ease-out",
},
brutalist: {
base: "bg-black text-white border-4 border-black font-mono uppercase tracking-wider",
hover: "hover:bg-white hover:text-black",
focus: "focus:outline-none focus:ring-0 focus:border-yellow-400",
active: "active:translate-x-1 active:translate-y-1",
transition: "transition-all duration-100",
},
glassmorphism: {
base: "bg-white/20 backdrop-blur-lg border border-white/30 text-white shadow-xl",
hover: "hover:bg-white/30",
focus: "focus:outline-none focus:ring-2 focus:ring-white/50",
active: "active:bg-white/40",
transition: "transition-all duration-200",
},
neumorphism: {
base: "bg-gray-100 text-gray-700 rounded-xl shadow-[8px_8px_16px_#d1d1d1,-8px_-8px_16px_#ffffff]",
hover: "hover:shadow-[4px_4px_8px_#d1d1d1,-4px_-4px_8px_#ffffff]",
focus: "focus:outline-none focus:shadow-[inset_4px_4px_8px_#d1d1d1,inset_-4px_-4px_8px_#ffffff]",
active: "active:shadow-[inset_4px_4px_8px_#d1d1d1,inset_-4px_-4px_8px_#ffffff]",
transition: "transition-shadow duration-200",
},
};
const ANIMATION_STYLES: Record<Animation, string> = {
none: "",
subtle: "transition-all duration-200 ease-out",
expressive: "transition-all duration-300 ease-out hover:scale-105 active:scale-95",
};
// === Component Templates ===
function getButtonTemplate(options: GenerateOptions): { styles: string; component: string } {
const aesthetic = options.aesthetic || "minimal";
const styles = AESTHETIC_STYLES[aesthetic];
const animation = ANIMATION_STYLES[options.animation];
const cssClasses = [
"px-4 py-2 rounded-lg font-medium",
styles.base,
styles.hover,
styles.focus,
styles.active,
animation || styles.transition,
].join(" ");
if (options.styling === "tailwind") {
return {
styles: "",
component: cssClasses,
};
}
// CSS version
const cssStyles = `
.btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
${aesthetic === "minimal" ? `
border: 1px solid #e5e7eb;
background: white;
color: #111827;
` : ""}
${aesthetic === "bold" ? `
background: linear-gradient(to right, #9333ea, #ec4899);
color: white;
font-weight: 700;
border: none;
` : ""}
transition: all 0.2s ease-out;
}
.btn:hover {
transform: translateY(-1px);
}
.btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
}
.btn:active {
transform: translateY(0);
}
${options.darkMode ? `
.dark .btn {
background: #1f2937;
color: white;
border-color: #374151;
}
` : ""}`;
return { styles: cssStyles, component: "btn" };
}
function getCardTemplate(options: GenerateOptions): { styles: string; component: string } {
const aesthetic = options.aesthetic || "minimal";
if (options.styling === "tailwind") {
const classes: Record<Aesthetic, string> = {
minimal: "bg-white border border-gray-200 rounded-lg shadow-sm p-6",
bold: "bg-gradient-to-br from-indigo-500 to-purple-600 text-white rounded-2xl p-8 shadow-2xl",
organic: "bg-amber-50 border-2 border-amber-200 rounded-3xl p-8",
brutalist: "bg-white border-4 border-black p-6",
glassmorphism: "bg-white/20 backdrop-blur-lg border border-white/30 rounded-2xl p-6 shadow-xl",
neumorphism: "bg-gray-100 rounded-2xl p-6 shadow-[8px_8px_16px_#d1d1d1,-8px_-8px_16px_#ffffff]",
};
return { styles: "", component: classes[aesthetic] };
}
const cssStyles = `
.card {
padding: 1.5rem;
border-radius: 0.5rem;
background: white;
border: 1px solid #e5e7eb;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.card-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.card-body {
color: #374151;
}
.card-footer {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
}
${options.darkMode ? `
.dark .card {
background: #1f2937;
border-color: #374151;
color: white;
}
` : ""}`;
return { styles: cssStyles, component: "card" };
}
function getInputTemplate(options: GenerateOptions): { styles: string; component: string } {
if (options.styling === "tailwind") {
return {
styles: "",
component: "w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent",
};
}
const cssStyles = `
.input-wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.input-label {
font-size: 0.875rem;
font-weight: 500;
color: #374151;
}
.input {
width: 100%;
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.input-error {
border-color: #ef4444;
}
.input-error-message {
font-size: 0.875rem;
color: #ef4444;
}
${options.darkMode ? `
.dark .input {
background: #1f2937;
border-color: #4b5563;
color: white;
}
` : ""}`;
return { styles: cssStyles, component: "input" };
}
// === Framework Generators ===
function generateReact(options: GenerateOptions, template: { styles: string; component: string }): string {
const { name } = options;
const propsInterface = `export interface ${name}Props {
children?: React.ReactNode;
className?: string;
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
}`;
if (options.styling === "tailwind") {
return `import React from 'react';
${propsInterface}
const variants = {
primary: '${template.component}',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
ghost: 'bg-transparent hover:bg-gray-100',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
export function ${name}({
children,
className = '',
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}: ${name}Props) {
return (
<button
className={\`\${variants[variant]} \${sizes[size]} \${className} \${disabled ? 'opacity-50 cursor-not-allowed' : ''}\`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
export default ${name};
`;
}
if (options.styling === "css-modules") {
return `import React from 'react';
import styles from './${name}.module.css';
${propsInterface}
export function ${name}({
children,
className = '',
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}: ${name}Props) {
const classes = [
styles.${template.component},
styles[variant],
styles[size],
disabled ? styles.disabled : '',
className,
].filter(Boolean).join(' ');
return (
<button className={classes} disabled={disabled} onClick={onClick}>
{children}
</button>
);
}
export default ${name};
`;
}
// Default CSS
return `import React from 'react';
import './${name}.css';
${propsInterface}
export function ${name}({
children,
className = '',
variant = 'primary',
size = 'md',
disabled = false,
onClick,
}: ${name}Props) {
const classes = [
'${template.component}',
\`${template.component}--\${variant}\`,
\`${template.component}--\${size}\`,
disabled ? '${template.component}--disabled' : '',
className,
].filter(Boolean).join(' ');
return (
<button className={classes} disabled={disabled} onClick={onClick}>
{children}
</button>
);
}
export default ${name};
`;
}
function generateVue(options: GenerateOptions, template: { styles: string; component: string }): string {
const { name } = options;
if (options.styling === "tailwind") {
return `<template>
<button
:class="[
'${template.component}',
variants[variant],
sizes[size],
{ 'opacity-50 cursor-not-allowed': disabled }
]"
:disabled="disabled"
@click="$emit('click')"
>
<slot />
</button>
</template>
<script setup lang="ts">
defineProps<{
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}>();
defineEmits<{
click: [];
}>();
const variants = {
primary: '',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
ghost: 'bg-transparent hover:bg-gray-100',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
</script>
`;
}
return `<template>
<button
:class="[
'${template.component}',
'${template.component}--' + variant,
'${template.component}--' + size,
{ '${template.component}--disabled': disabled }
]"
:disabled="disabled"
@click="$emit('click')"
>
<slot />
</button>
</template>
<script setup lang="ts">
defineProps<{
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}>();
defineEmits<{
click: [];
}>();
</script>
<style scoped>
${template.styles}
.${template.component}--secondary {
background: #f3f4f6;
color: #111827;
}
.${template.component}--ghost {
background: transparent;
border: none;
}
.${template.component}--sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.${template.component}--lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.${template.component}--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
`;
}
function generateSvelte(options: GenerateOptions, template: { styles: string; component: string }): string {
const { name: _name } = options;
if (options.styling === "tailwind") {
return `<script lang="ts">
export let variant: 'primary' | 'secondary' | 'ghost' = 'primary';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let disabled = false;
const variants = {
primary: '${template.component}',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
ghost: 'bg-transparent hover:bg-gray-100',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
</script>
<button
class="{variants[variant]} {sizes[size]} {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
{disabled}
on:click
>
<slot />
</button>
`;
}
return `<script lang="ts">
export let variant: 'primary' | 'secondary' | 'ghost' = 'primary';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let disabled = false;
</script>
<button
class="${template.component} ${template.component}--{variant} ${template.component}--{size}"
class:${template.component}--disabled={disabled}
{disabled}
on:click
>
<slot />
</button>
<style>
${template.styles}
.${template.component}--secondary {
background: #f3f4f6;
color: #111827;
}
.${template.component}--ghost {
background: transparent;
border: none;
}
.${template.component}--sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.${template.component}--lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.${template.component}--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
`;
}
function generateHTML(options: GenerateOptions, template: { styles: string; component: string }): string {
const { name } = options;
if (options.styling === "tailwind") {
return `<!-- ${name} Component -->
<!-- Add Tailwind CSS to your project for these styles to work -->
<button class="${template.component}">
Button Text
</button>
<!-- Variants -->
<button class="${template.component}">Primary</button>
<button class="bg-gray-100 text-gray-900 hover:bg-gray-200 px-4 py-2 rounded-lg">Secondary</button>
<button class="bg-transparent hover:bg-gray-100 px-4 py-2 rounded-lg">Ghost</button>
`;
}
return `<!-- ${name} Component -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${name} Component</title>
<style>
${template.styles}
.${template.component}--secondary {
background: #f3f4f6;
color: #111827;
}
.${template.component}--ghost {
background: transparent;
border: none;
}
.${template.component}--sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.${template.component}--lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.${template.component}--disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<button class="${template.component}">Primary Button</button>
<button class="${template.component} ${template.component}--secondary">Secondary Button</button>
<button class="${template.component} ${template.component}--ghost">Ghost Button</button>
</body>
</html>
`;
}
// === File Extension Mapping ===
function getFileExtension(framework: Framework, styling: Styling): string {
switch (framework) {
case "react":
return styling === "styled-components" || styling === "emotion" ? "tsx" : "tsx";
case "vue":
return "vue";
case "svelte":
return "svelte";
case "html":
return "html";
default:
return "txt";
}
}
// === Core Generation Function ===
export async function generateComponent(options: GenerateOptions): Promise<Map<string, string>> {
const outputs = new Map<string, string>();
// Get template based on component type
let template: { styles: string; component: string };
switch (options.type) {
case "button":
template = getButtonTemplate(options);
break;
case "card":
template = getCardTemplate(options);
break;
case "input":
template = getInputTemplate(options);
break;
default:
template = getButtonTemplate(options); // Default to button
}
// Generate component based on framework
let componentCode: string;
switch (options.framework) {
case "react":
componentCode = generateReact(options, template);
break;
case "vue":
componentCode = generateVue(options, template);
break;
case "svelte":
componentCode = generateSvelte(options, template);
break;
case "html":
componentCode = generateHTML(options, template);
break;
default:
throw new Error(`Unknown framework: ${options.framework}`);
}
const ext = getFileExtension(options.framework, options.styling);
const fileName = `${options.name}.${ext}`;
outputs.set(fileName, componentCode);
// Generate separate CSS file if needed
if (options.styling === "css" && template.styles && options.framework !== "html") {
outputs.set(`${options.name}.css`, template.styles);
}
if (options.styling === "css-modules" && template.styles) {
outputs.set(`${options.name}.module.css`, template.styles);
}
return outputs;
}
// === Help Text ===
function printHelp(): void {
console.log(`
${SCRIPT_NAME} v${VERSION} - Component Template Generator
Generates component templates with design-focused styling
for multiple frameworks (React, Vue, Svelte, HTML).
Usage:
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts [options] <output-dir>
Arguments:
<output-dir> Directory for output files
Options:
-h, --help Show this help message
-v, --version Show version
--spec <file> JSON specification file
--name <name> Component name (PascalCase)
--type <type> Component type: button, card, input, modal, navigation, hero
--framework <fw> Framework: react, vue, svelte, html
--styling <type> Styling: css, tailwind, css-modules, styled-components, emotion
--aesthetic <type> Aesthetic: minimal, bold, organic, brutalist, glassmorphism, neumorphism
--animation <level> Animation: none, subtle, expressive
--dark-mode Include dark mode support
--tokens <file> Use design tokens file
Examples:
# Generate React button with Tailwind
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
--name Button --framework react --styling tailwind --aesthetic bold ./components/
# Generate Vue card with CSS modules
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
--name Card --type card --framework vue --styling css-modules ./components/
# Generate Svelte input with minimal aesthetic
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
--name Input --type input --framework svelte --aesthetic minimal ./components/
# Generate from spec file
deno run --allow-read --allow-write scripts/${SCRIPT_NAME}.ts \\
--spec component-spec.json ./components/
`);
}
// === Main CLI Handler ===
async function main(args: string[]): Promise<void> {
const parsed = parseArgs(args, {
boolean: ["help", "version", "dark-mode"],
string: ["spec", "name", "type", "framework", "styling", "aesthetic", "animation", "tokens"],
alias: { h: "help", v: "version" },
default: {
type: "button",
framework: "react",
styling: "css",
animation: "subtle",
"dark-mode": false,
},
});
if (parsed.help) {
printHelp();
Deno.exit(0);
}
if (parsed.version) {
console.log(`${SCRIPT_NAME} v${VERSION}`);
Deno.exit(0);
}
const outputDir = parsed._[0] as string;
if (!outputDir) {
console.error("Error: Output directory required");
console.error("Use --help for usage information");
Deno.exit(1);
}
if (!parsed.spec && !parsed.name) {
console.error("Error: Component name required (--name or --spec)");
console.error("Use --help for usage information");
Deno.exit(1);
}
const validFrameworks: Framework[] = ["react", "vue", "svelte", "html"];
const validStyling: Styling[] = ["css", "tailwind", "css-modules", "styled-components", "emotion"];
const validAesthetics: Aesthetic[] = ["minimal", "bold", "organic", "brutalist", "glassmorphism", "neumorphism"];
const validTypes: ComponentType[] = ["button", "card", "input", "modal", "navigation", "hero", "custom"];
const validAnimations: Animation[] = ["none", "subtle", "expressive"];
if (!validFrameworks.includes(parsed.framework as Framework)) {
console.error(`Error: Invalid framework '${parsed.framework}'`);
console.error(`Valid frameworks: ${validFrameworks.join(", ")}`);
Deno.exit(1);
}
if (!validStyling.includes(parsed.styling as Styling)) {
console.error(`Error: Invalid styling '${parsed.styling}'`);
console.error(`Valid styling options: ${validStyling.join(", ")}`);
Deno.exit(1);
}
if (parsed.aesthetic && !validAesthetics.includes(parsed.aesthetic as Aesthetic)) {
console.error(`Error: Invalid aesthetic '${parsed.aesthetic}'`);
console.error(`Valid aesthetics: ${validAesthetics.join(", ")}`);
Deno.exit(1);
}
if (!validTypes.includes(parsed.type as ComponentType)) {
console.error(`Error: Invalid type '${parsed.type}'`);
console.error(`Valid types: ${validTypes.join(", ")}`);
Deno.exit(1);
}
if (!validAnimations.includes(parsed.animation as Animation)) {
console.error(`Error: Invalid animation '${parsed.animation}'`);
console.error(`Valid animations: ${validAnimations.join(", ")}`);
Deno.exit(1);
}
const options: GenerateOptions = {
specFile: parsed.spec,
name: parsed.name || "Component",
type: parsed.type as ComponentType,
framework: parsed.framework as Framework,
styling: parsed.styling as Styling,
aesthetic: parsed.aesthetic as Aesthetic | undefined,
animation: parsed.animation as Animation,
darkMode: parsed["dark-mode"],
tokens: parsed.tokens,
outputDir,
};
try {
const outputs = await generateComponent(options);
const outPath = resolve(outputDir);
await ensureDir(outPath);
for (const [fileName, content] of outputs) {
const filePath = join(outPath, fileName);
await Deno.writeTextFile(filePath, content);
console.log(`Generated: ${filePath}`);
}
console.log(`\nSuccessfully generated ${outputs.size} file(s)`);
} catch (error) {
console.error(`Error: ${(error as Error).message}`);
Deno.exit(1);
}
}
// === Entry Point ===
if (import.meta.main) {
main(Deno.args);
}
Related skills
How it compares
Pick this over generic frontend-design aesthetics skills when you have a JSON schema and need repeatable component file generation per framework.
FAQ
What runtime is required?
Deno with --allow-read and --allow-write for analyze-styles, extract-tokens, generate-palette, and generate-component scripts.
Which frameworks are supported for components?
React, Vue, Svelte, and vanilla HTML with css, tailwind, css-modules, styled-components, or emotion styling.
Should I analyze before generating?
Analysis is optional but recommended to understand existing patterns before creating new tokens or components.
Is Frontend Design safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.