Procedural Design Systems: How Sailop Generates Infinite Palettes
Sailop uses seed-based procedural generation to create unique design systems. Here's how the HSL math, WCAG contrast, and reproducibility work.
Every Sailop design system is unique. Not hand-picked-from-a-list unique. Mathematically unique. Generated procedurally from a seed, with constraints that guarantee usability, accessibility, and visual distinction from AI defaults. Here's how it works under the hood.
Why Procedural Generation?
The traditional approach to design systems is curation. A designer picks colors, fonts, spacing values. This works beautifully but doesn't scale. You can't hand-curate a design system for every new project, especially when the goal is to break away from AI defaults.
Sailop takes a different approach: procedural generation with constraints. This is the technical foundation behind the complete guide to anti-AI design. Given a seed string, it produces a complete design system that is:
- Unique: Different seeds produce visually different systems
- Reproducible: Same seed always produces the same system
- Accessible: WCAG contrast ratios are guaranteed
- Non-AI: Every generated value falls outside the AI default ranges
This is the same principle behind procedural terrain generation in games: deterministic randomness within constraints.
The Seed System
Every design system starts with a seed -- any string:
sailop generate --seed "my-project-2026"
sailop generate --seed "client-acme-rebrand"
sailop generate --seed "hackathon-demo"The seed is hashed into a deterministic random number generator (using a variant of SFC32). This RNG feeds every decision in the generation pipeline. Same seed, same sequence of "random" numbers, same design system.
// Simplified seed-to-RNG pipeline
function seedToRng(seed: string): () => number {
let hash = cyrb128(seed);
return sfc32(hash[0], hash[1], hash[2], hash[3]);
}
const rng = seedToRng("my-project-2026");
rng(); // 0.7234... (always the same for this seed)
rng(); // 0.1891... (always the same)
rng(); // 0.5523... (always the same)Color Generation: HSL With Exclusion Zones
Colors are generated in HSL (Hue, Saturation, Lightness) space, never RGB. HSL makes it possible to reason about color relationships and enforce constraints:
Step 1: Primary Hue Selection
The RNG generates a hue between 0 and 360, but with an exclusion zone:
function generatePrimaryHue(rng: () => number): number {
// Exclude hue 200-290 (blue/indigo/violet AI band)
const AI_BAND_START = 200;
const AI_BAND_END = 290;
const AI_BAND_SIZE = AI_BAND_END - AI_BAND_START; // 90
// Generate in the allowed range (0-270 mapped to 0-360 minus exclusion)
const allowed = 360 - AI_BAND_SIZE; // 270 degrees available
const raw = Math.floor(rng() * allowed);
// Map back to full hue circle, skipping the AI band
return raw < AI_BAND_START ? raw : raw + AI_BAND_SIZE;
}This guarantees the primary hue is never in the blue-indigo-violet range that AI defaults to. For a full explanation of why AI converges on blue, see why every AI-generated website looks the same.
Step 2: Saturation and Lightness
With the hue chosen, saturation and lightness are generated within usable ranges:
function generateAccentColor(hue: number, rng: () => number) {
const saturation = 45 + rng() * 35; // 45-80% (vivid but not neon)
const lightness = 35 + rng() * 20; // 35-55% (readable on light bg)
return { h: hue, s: saturation, l: lightness };
}Step 3: Background Hue-Shifting
The background is never pure white (#fff). It's always tinted toward the primary hue:
function generateBackground(primaryHue: number, rng: () => number) {
const bgLightness = 93 + rng() * 4; // 93-97% (very light but not white)
const bgSaturation = 8 + rng() * 12; // 8-20% (subtle tint)
return { h: primaryHue, s: bgSaturation, l: bgLightness };
}
// Seed "my-project-2026" might produce: HSL(28, 12%, 95%) = #f5f0ebStep 4: Complementary Secondary
The secondary color uses color theory. Sailop generates either a complementary (180 degrees opposite) or split-complementary (150-210 degrees) hue:
function generateSecondary(primaryHue: number, rng: () => number) {
const strategy = rng() > 0.5 ? 'complementary' : 'split';
const offset = strategy === 'complementary'
? 180
: 150 + rng() * 60; // 150-210 degrees
const secondaryHue = (primaryHue + offset) % 360;
return generateAccentColor(secondaryHue, rng);
}Step 5: Neutral Palette
Neutrals (text, borders, muted elements) are generated from the primary hue with decreasing saturation:
function generateNeutrals(primaryHue: number) {
return {
fg: { h: primaryHue, s: 15, l: 10 }, // near-black, hue-tinted
fgBody: { h: primaryHue, s: 12, l: 28 }, // body text
fgMuted: { h: primaryHue, s: 10, l: 48 }, // secondary text
fgFaint: { h: primaryHue, s: 8, l: 68 }, // disabled/placeholder
border: { h: primaryHue, s: 10, l: 85 }, // default border
borderStrong: { h: primaryHue, s: 10, l: 78 }, // emphasized border
};
}WCAG Contrast Validation
Every generated color pair is checked against WCAG 2.1 contrast requirements:
function validateContrast(fg: HSL, bg: HSL): boolean {
const ratio = getContrastRatio(hslToRgb(fg), hslToRgb(bg));
return ratio >= 4.5; // AA standard for normal text
}
function validatePalette(palette: Palette): Palette {
// If body text fails contrast against background, darken it
while (!validateContrast(palette.fgBody, palette.bg)) {
palette.fgBody.l -= 2;
}
// If accent fails contrast, adjust saturation and lightness
while (!validateContrast(palette.accent, palette.bg)) {
palette.accent.l -= 2;
palette.accent.s += 1;
}
return palette;
}This loop guarantees that no generated palette produces inaccessible text. The contrast ratio is always at least 4.5:1 for body text and 3:1 for large text.
Typography Generation
Font pairing is also procedural. Sailop maintains a curated list of non-AI-default fonts, categorized by style:
const DISPLAY_FONTS = [
{ name: 'Bitter', category: 'serif', personality: 'warm' },
{ name: 'Playfair Display', category: 'serif', personality: 'elegant' },
{ name: 'Cormorant Garamond', category: 'serif', personality: 'refined' },
{ name: 'Bricolage Grotesque', category: 'sans', personality: 'playful' },
{ name: 'Space Mono', category: 'mono', personality: 'technical' },
// ... more options
];
const BODY_FONTS = [
{ name: 'Karla', category: 'sans', personality: 'humanist' },
{ name: 'DM Sans', category: 'sans', personality: 'geometric' },
{ name: 'Work Sans', category: 'sans', personality: 'neutral' },
{ name: 'Barlow', category: 'sans', personality: 'clean' },
// ... more options
];The RNG selects a display font and a body font, with a preference for cross-category pairing (serif display + sans body). For more on why these pairings matter, see why Inter is killing your brand:
function generateTypography(rng: () => number) {
const displayIdx = Math.floor(rng() * DISPLAY_FONTS.length);
const bodyIdx = Math.floor(rng() * BODY_FONTS.length);
return {
display: DISPLAY_FONTS[displayIdx],
body: BODY_FONTS[bodyIdx],
mono: { name: 'JetBrains Mono' }, // Always JetBrains Mono for code
};
}Spacing Generation
Spacing values intentionally break the 4px grid:
function generateSpacing(rng: () => number) {
// Generate 3 "off-grid" micro values (not multiples of 4)
const offGrid = [
2 + Math.floor(rng() * 4), // 2-5px
12 + Math.floor(rng() * 6), // 12-17px (includes 13, 14, 15)
20 + Math.floor(rng() * 6), // 20-25px (includes 21, 22, 23)
];
// Generate section paddings (varied, not uniform)
const heropad = 130 + Math.floor(rng() * 40); // 130-170px
const sectionA = 80 + Math.floor(rng() * 30); // 80-110px
const sectionB = 60 + Math.floor(rng() * 20); // 60-80px
const sectionC = 48 + Math.floor(rng() * 16); // 48-64px
return { offGrid, heropad, sectionA, sectionB, sectionC };
}The Full Pipeline
Putting it all together:
sailop generate --seed "acme-corp-2026"Output:
/* Generated by Sailop — seed: acme-corp-2026 */
:root {
--f-display: 'Playfair Display', Georgia, serif;
--f-body: 'DM Sans', system-ui, sans-serif;
--f-mono: 'JetBrains Mono', monospace;
--c-bg: hsl(42, 18%, 94%);
--c-bg-raised: hsl(42, 14%, 97%);
--c-fg: hsl(42, 20%, 9%);
--c-fg-body: hsl(42, 14%, 26%);
--c-accent: hsl(42, 72%, 44%);
--c-secondary: hsl(222, 48%, 38%);
--r-btn: 3px;
--r-card: 6px;
--r-container: 0px;
--s-micro: 3px;
--s-small: 13px;
--s-medium: 21px;
}Every value is different from the default. Every value is accessible. Every value is reproducible from the seed.
Infinity, Constrained
There are effectively infinite possible design systems. The constraint space (non-AI hues, WCAG contrast, serif pairing, off-grid spacing) is large enough that seed collisions producing visually similar output are statistically negligible.
This is the core philosophy: you don't need a designer for every project. You need a system that produces designed output every time. Procedural generation with the right constraints achieves this. See a real transformation in action in our Grade F to Grade A case study.
Try it: npm i -g sailop && sailop generate --seed "your-name-here". Every seed tells a different visual story. Find yours at sailop.com.
Try Sailop
Scan your frontend for AI patterns. Generate a unique design system. Ship code that looks intentional.