$npx -y skills add jezweb/claude-skills --skill color-paletteGenerate complete, accessible colour palettes from a single brand hex. Produces 11-shade scale (50-950), semantic tokens, dark mode variants, Tailwind v4 CSS output, WCAG contrast checks. Use whenever the user supplies a brand hex and asks for a palette, mentions setting up a des
| 1 | # Colour Palette Generator |
| 2 | |
| 3 | Generate a complete, accessible colour system from a single brand hex. Produces Tailwind v4 CSS ready to paste into your project. |
| 4 | |
| 5 | ## Workflow |
| 6 | |
| 7 | ### Step 1: Get the Brand Hex |
| 8 | |
| 9 | Ask for the primary brand colour. A single hex like `#0D9488` is enough. |
| 10 | |
| 11 | ### Step 2: Generate 11-Shade Scale |
| 12 | |
| 13 | Convert hex to HSL, then generate shades by varying lightness while keeping hue constant. |
| 14 | |
| 15 | #### Hex to HSL Conversion |
| 16 | |
| 17 | ```javascript |
| 18 | function hexToHSL(hex) { |
| 19 | hex = hex.replace(/^#/, ''); |
| 20 | const r = parseInt(hex.substring(0, 2), 16) / 255; |
| 21 | const g = parseInt(hex.substring(2, 4), 16) / 255; |
| 22 | const b = parseInt(hex.substring(4, 6), 16) / 255; |
| 23 | |
| 24 | const max = Math.max(r, g, b); |
| 25 | const min = Math.min(r, g, b); |
| 26 | const diff = max - min; |
| 27 | |
| 28 | let l = (max + min) / 2; |
| 29 | let s = 0; |
| 30 | if (diff !== 0) { |
| 31 | s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min); |
| 32 | } |
| 33 | |
| 34 | let h = 0; |
| 35 | if (diff !== 0) { |
| 36 | if (max === r) h = ((g - b) / diff + (g < b ? 6 : 0)) / 6; |
| 37 | else if (max === g) h = ((b - r) / diff + 2) / 6; |
| 38 | else h = ((r - g) / diff + 4) / 6; |
| 39 | } |
| 40 | |
| 41 | return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }; |
| 42 | } |
| 43 | ``` |
| 44 | |
| 45 | #### Lightness and Saturation Values |
| 46 | |
| 47 | | Shade | Lightness | Saturation Mult | Use Case | |
| 48 | |-------|-----------|-----------------|----------| |
| 49 | | 50 | 97% | 0.80 | Subtle backgrounds | |
| 50 | | 100 | 94% | 0.80 | Hover states | |
| 51 | | 200 | 87% | 0.85 | Borders, dividers | |
| 52 | | 300 | 75% | 0.90 | Disabled states | |
| 53 | | 400 | 62% | 0.95 | Placeholder text | |
| 54 | | 500 | 48% | 1.00 | **Brand colour baseline** | |
| 55 | | 600 | 40% | 1.00 | Primary actions (often the brand colour) | |
| 56 | | 700 | 33% | 1.00 | Hover on primary | |
| 57 | | 800 | 27% | 1.00 | Active states | |
| 58 | | 900 | 20% | 1.00 | Text on light bg | |
| 59 | | 950 | 10% | 1.00 | Darkest accents | |
| 60 | |
| 61 | Reduce saturation for lighter shades (50-200 by 15-20%, 300-400 by 5-10%) to prevent overly vibrant pastels. Keep full saturation for 500-950. |
| 62 | |
| 63 | #### Complete Scale Generator |
| 64 | |
| 65 | ```javascript |
| 66 | function generateShadeScale(brandHex) { |
| 67 | const { h, s } = hexToHSL(brandHex); |
| 68 | const shades = { |
| 69 | 50: { l: 97, sMul: 0.8 }, 100: { l: 94, sMul: 0.8 }, |
| 70 | 200: { l: 87, sMul: 0.85 }, 300: { l: 75, sMul: 0.9 }, |
| 71 | 400: { l: 62, sMul: 0.95 }, 500: { l: 48, sMul: 1.0 }, |
| 72 | 600: { l: 40, sMul: 1.0 }, 700: { l: 33, sMul: 1.0 }, |
| 73 | 800: { l: 27, sMul: 1.0 }, 900: { l: 20, sMul: 1.0 }, |
| 74 | 950: { l: 10, sMul: 1.0 } |
| 75 | }; |
| 76 | const result = {}; |
| 77 | for (const [shade, { l, sMul }] of Object.entries(shades)) { |
| 78 | result[shade] = `hsl(${h}, ${Math.round(s * sMul)}%, ${l}%)`; |
| 79 | } |
| 80 | return result; |
| 81 | } |
| 82 | ``` |
| 83 | |
| 84 | #### HSL to Hex Conversion |
| 85 | |
| 86 | ```javascript |
| 87 | function hslToHex(h, s, l) { |
| 88 | s = s / 100; l = l / 100; |
| 89 | const c = (1 - Math.abs(2 * l - 1)) * s; |
| 90 | const x = c * (1 - Math.abs((h / 60) % 2 - 1)); |
| 91 | const m = l - c / 2; |
| 92 | let r = 0, g = 0, b = 0; |
| 93 | if (h < 60) { r = c; g = x; } |
| 94 | else if (h < 120) { r = x; g = c; } |
| 95 | else if (h < 180) { g = c; b = x; } |
| 96 | else if (h < 240) { g = x; b = c; } |
| 97 | else if (h < 300) { r = x; b = c; } |
| 98 | else { r = c; b = x; } |
| 99 | r = Math.round((r + m) * 255); |
| 100 | g = Math.round((g + m) * 255); |
| 101 | b = Math.round((b + m) * 255); |
| 102 | return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase(); |
| 103 | } |
| 104 | ``` |
| 105 | |
| 106 | #### Verification |
| 107 | |
| 108 | Generated shades should look like the same colour family with smooth progression. Light shades (50-300) usable for backgrounds, dark shades (700-950) usable for text. Brand colour recognisable in 500-700. |
| 109 | |
| 110 | --- |
| 111 | |
| 112 | ### Step 3: Map Semantic Tokens |
| 113 | |
| 114 | Every background token MUST have a paired foreground token. Never use a background without its pair or dark mode will break. |
| 115 | |
| 116 | #### Light Mode Tokens |
| 117 | |
| 118 | | Token | Shade | Use Case | |
| 119 | |-------|-------|----------| |
| 120 | | `background` | white | Page backgrounds | |
| 121 | | `foreground` | 950 | Body text | |
| 122 | | `card` | white | Card backgrounds | |
| 123 | | `card-foreground` | 900 | Card text | |
| 124 | | `popover` | white | Dropdown/tooltip backgrounds | |
| 125 | | `popover-foreground` | 950 | Dropdown text | |
| 126 | | `primary` | 600 | Primary buttons, links | |
| 127 | | `primary-foreground` | white | Text on primary buttons | |
| 128 | | `secondary` | 100 | Secondary buttons | |
| 129 | | `secondary-foreground` | 900 | Text on secondary buttons | |
| 130 | | `muted` | 50 | Disabled backgrounds, subtle sections | |
| 131 | | `muted-foreground` | 600 | Muted text, captions | |
| 132 | | `accent` | 100 | Hover states, subtle highlights | |
| 133 | | `accent-foreground` | 900 | Text on accent backgrounds | |
| 134 | | `destructive` | red-600 | Delete buttons, errors | |
| 135 | | `destructive-foreground` | white | Text on destructive buttons |