bdsg
v0.2.0
Published
Design system generation library with WCAG accessibility compliance
Maintainers
Readme
bdsg
Design System Generation library. Algorithms for generating design tokens programmatically with WCAG accessibility compliance.
Table of Contents
Installation
bun add bdsg
# or
npm install bdsgQuick Start
import {
generatePalette,
generateTypographyScale,
generateSpacingScale,
generateShadows,
adjustColorForContrast,
} from "bdsg";
// Generate a color palette from a base color
const palette = generatePalette("#3B82F6", "primary");
console.log(palette.shades[500].value); // "#3B82F6"
console.log(palette.shades[500].textColor); // "#FFFFFF"
// Adjust a color to meet WCAG AA contrast requirements
const result = adjustColorForContrast("#87CEEB", "#FFFFFF", "AA");
console.log(result.adjusted); // Darker blue that meets 4.5:1 ratio
console.log(result.ratio); // 4.52
// Generate typography scale using perfect-fourth ratio
const typography = generateTypographyScale({
base: 16,
ratio: "perfect-fourth",
stepsUp: 4,
stepsDown: 2,
});
console.log(typography.tokens);
// [{ name: "xs", fontSize: 9, lineHeight: 1.7, ... }, ...]
// Generate spacing scale with Fibonacci progression
const spacing = generateSpacingScale({ base: 8, method: "fibonacci" });
console.log(spacing.tokens.map(t => t.formatted));
// ["0", "0.25rem", "0.5rem", "0.75rem", "1rem", ...]
// Generate Material Design-style shadows
const shadows = generateShadows({ style: "material", levels: 5 });
console.log(shadows.cssVariables);
// "--shadow-none: none;\n--shadow-sm: 0px 1px 2px..."API Reference
Color Utilities
Convert colors between hex, RGB, and HSL color spaces.
import { hexToRgb, rgbToHex, hexToHsl, hslToHex } from "bdsg";
hexToRgb("#FF5733"); // { r: 255, g: 87, b: 51 }
hexToRgb("#F00"); // { r: 255, g: 0, b: 0 } (shorthand supported)
rgbToHex({ r: 0, g: 128, b: 255 }); // "#0080ff"
hexToHsl("#3B82F6"); // { h: 217, s: 91, l: 60 }
hslToHex({ h: 0, s: 100, l: 50 }); // "#ff0000"OKLCH Color Space - ^0.2.0
OKLCH is a perceptually uniform color space that provides better gradient interpolation and more intuitive color manipulation.
import { hexToOklch, oklchToHex, interpolateOklch } from "bdsg";
// Convert between HEX and OKLCH
const oklch = hexToOklch("#3B82F6");
// { l: 0.623, c: 0.185, h: 259.5 }
// l: lightness (0-1), c: chroma (0+), h: hue (0-360)
oklchToHex({ l: 0.623, c: 0.185, h: 259.5 }); // "#3b82f6"
// Interpolate colors without "muddy middle"
const red = hexToOklch("#FF0000");
const green = hexToOklch("#00FF00");
const middle = interpolateOklch(red, green, 0.5);
// Produces vibrant yellow, not muddy brown like RGB interpolationWhy OKLCH?
- Perceptually uniform: Equal lightness values look equally bright across all hues
- No muddy gradients: Interpolation stays vibrant, avoiding the brown/gray zone
- Intuitive: Hue, chroma, and lightness are independent
Gradients - ^0.2.0
Generate smooth color gradients using OKLCH interpolation with easing and hue direction control.
import {
generateGradient,
generateMultiStopGradient,
toCssGradient,
EASING
} from "bdsg";
// Simple two-color gradient
const gradient = generateGradient("#FF0000", "#0000FF", 5);
// ["#ff0000", "#c5007c", "#9100c9", "#5c00ed", "#0000ff"]
// With easing function
const smooth = generateGradient("#000000", "#FFFFFF", 5, {
easing: EASING.easeInOut
});
// Control hue direction (shorter or longer path around color wheel)
const rainbow = generateGradient("#FF0000", "#FF8800", 5, {
hueDirection: "longer" // Takes the long way through blue/purple
});
// Multi-stop gradient
const sunset = generateMultiStopGradient([
{ color: "#FF0000", position: 0 },
{ color: "#FFFF00", position: 0.3 },
{ color: "#00FF00", position: 1 }
], 10);
// Generate CSS gradient string
const colors = generateGradient("#FF0000", "#0000FF", 3);
toCssGradient("linear", colors, 45);
// "linear-gradient(45deg, #ff0000, #800080, #0000ff)"
toCssGradient("radial", colors);
// "radial-gradient(circle, #ff0000, #800080, #0000ff)"
toCssGradient("conic", colors, 90);
// "conic-gradient(from 90deg, #ff0000, #800080, #0000ff)"Available easing functions:
EASING.linear— Constant speedEASING.easeIn— Slow start, acceleratesEASING.easeOut— Fast start, deceleratesEASING.easeInOut— Slow start and end
Hue direction options:
"shorter"— Takes shortest path around color wheel (default)"longer"— Takes longer path for rainbow effects"increasing"— Always increases hue"decreasing"— Always decreases hue
Contrast
Calculate and validate WCAG 2.1 contrast ratios.
import {
calculateContrast,
getRelativeLuminance,
meetsWCAG,
getWCAGCompliance,
} from "bdsg";
// Calculate contrast ratio (1:1 to 21:1)
calculateContrast("#000000", "#FFFFFF"); // 21
calculateContrast("#767676", "#FFFFFF"); // 4.54
// Check WCAG compliance
meetsWCAG(4.5, "AA", "normal"); // true
meetsWCAG(4.5, "AAA", "normal"); // false (AAA requires 7.0)
meetsWCAG(3.0, "AA", "large"); // true (large text only needs 3.0)
// Get full compliance info
getWCAGCompliance(4.5, "normal");
// { ratio: 4.5, AA: true, AAA: false, level: "AA" }WCAG Requirements: | Level | Normal Text | Large Text | |-------|-------------|------------| | AA | 4.5:1 | 3.0:1 | | AAA | 7.0:1 | 4.5:1 |
Color Adjustment
Automatically adjust colors to meet accessibility requirements.
import { adjustColorForContrast, generateAccessibleVariations } from "bdsg";
// Adjust color to meet WCAG AA on white background
const result = adjustColorForContrast("#87CEEB", "#FFFFFF", "AA", "normal");
// {
// original: "#87CEEB",
// adjusted: "#1A6B8A", // Darkened to meet contrast
// ratio: 4.52,
// iterations: 8,
// strategy: "lightness"
// }
// Generate accessible color variations
const variations = generateAccessibleVariations("#3B82F6", "#FFFFFF");
// {
// base: "#2563EB", // Adjusted base
// light: "#60A5FA", // Lighter variant
// dark: "#1E40AF", // Darker variant
// text: "#FFFFFF" // Best text color on base
// }The algorithm:
- Preserves hue (brand identity)
- Adjusts lightness first (binary search)
- Reduces saturation if needed
- Falls back to black/white if impossible
Palette Generation
Generate 10-shade color palettes from a base color.
import { generatePalette, generatePaletteTokens } from "bdsg";
const palette = generatePalette("#3B82F6", "primary");
// Each shade includes value, text color, and contrast ratio
palette.shades[100];
// { value: "#DBEAFE", textColor: "#000000", contrastRatio: 17.4 }
palette.shades[900];
// { value: "#1E3A8A", textColor: "#FFFFFF", contrastRatio: 10.2 }
// Export as flat tokens
const tokens = generatePaletteTokens("#3B82F6", "primary");
// [
// { name: "primary-50", value: "#EFF6FF", textColor: "#000000", ... },
// { name: "primary-100", value: "#DBEAFE", ... },
// ...
// ]Shade lightness targets: | Shade | Lightness | |-------|-----------| | 50 | 97% | | 100 | 93% | | 200 | 85% | | 300 | 75% | | 400 | 60% | | 500 | 50% (base)| | 600 | 42% | | 700 | 35% | | 800 | 27% | | 900 | 20% |
Typography Scale
Generate font sizes using musical ratio progressions.
import { generateTypographyScale, TYPOGRAPHY_RATIOS } from "bdsg";
const scale = generateTypographyScale({
base: 16, // Base font size in px
ratio: "perfect-fourth", // 1.333 ratio
stepsUp: 6, // Sizes above base
stepsDown: 2, // Sizes below base
baseLineHeight: 1.5, // Line height for base
unit: "rem", // Output unit
});
// Each token includes calculated line-height and letter-spacing
scale.tokens[0];
// {
// name: "xs",
// fontSize: 9,
// lineHeight: 1.7, // Looser for small text
// letterSpacing: 0.02, // Positive tracking
// weight: 400
// }
scale.tokens[scale.tokens.length - 1];
// {
// name: "4xl",
// fontSize: 50,
// lineHeight: 1.1, // Tighter for display
// letterSpacing: -0.02, // Negative tracking
// weight: 700
// }
// Available ratios
TYPOGRAPHY_RATIOS["minor-second"]; // 1.067
TYPOGRAPHY_RATIOS["major-second"]; // 1.125
TYPOGRAPHY_RATIOS["minor-third"]; // 1.2
TYPOGRAPHY_RATIOS["major-third"]; // 1.25
TYPOGRAPHY_RATIOS["perfect-fourth"]; // 1.333
TYPOGRAPHY_RATIOS["augmented-fourth"]; // 1.414 (sqrt(2))
TYPOGRAPHY_RATIOS["perfect-fifth"]; // 1.5
TYPOGRAPHY_RATIOS["golden-ratio"]; // 1.618Spacing Scale
Generate spacing values using mathematical progressions.
import { generateSpacingScale, SPACING_PRESETS } from "bdsg";
// Fibonacci progression (organic, natural feel)
const fibonacci = generateSpacingScale({
base: 8,
method: "fibonacci",
steps: 10,
});
// Values: [0, 4, 8, 12, 16, 28, 44, 72, 116, 188]
// Linear progression (consistent increments)
const linear = generateSpacingScale({
base: 4,
method: "linear",
steps: 8,
});
// Values: [0, 4, 8, 12, 16, 20, 24, 28]
// Exponential progression (dramatic hierarchy)
const exponential = generateSpacingScale({
base: 4,
method: "exponential",
exponent: 2,
steps: 6,
});
// Values: [0, 4, 8, 16, 32, 64]
// T-shirt sizes (semantic naming)
const tshirt = generateSpacingScale({
base: 8,
method: "t-shirt",
steps: 8,
});
// Names: 3xs, 2xs, xs, sm, md, lg, xl, 2xl
// Presets
generateSpacingScale(SPACING_PRESETS.tailwind); // 4px linear
generateSpacingScale(SPACING_PRESETS.material); // 8px linear
generateSpacingScale(SPACING_PRESETS.natural); // 8px fibonacci
generateSpacingScale(SPACING_PRESETS.semantic); // 8px t-shirtShadow Generation
Generate layered shadows based on Material Design elevation.
import { generateShadows, SHADOW_PRESETS } from "bdsg";
const shadows = generateShadows({
color: "#000000",
baseOpacity: 0.1,
levels: 6,
style: "material", // material | soft | hard | inset
layered: true,
});
// Token structure
shadows.tokens[2];
// {
// name: "md",
// level: 2,
// layers: [
// { x: 0, y: 3, blur: 6, spread: 0, opacity: 0.12 },
// { x: 0, y: 2, blur: 4, spread: -1, opacity: 0.1 },
// { x: 0, y: 1, blur: 2, spread: 0, opacity: 0.06 }
// ],
// value: "0px 3px 6px 0px rgba(0,0,0,0.12), ..."
// }
// CSS variables output
shadows.cssVariables;
// "--shadow-none: none;
// --shadow-sm: 0px 1px 2px 0px rgba(0,0,0,0.08), ...
// --shadow-md: ..."
// Presets
generateShadows(SHADOW_PRESETS.material); // 3-layer realistic
generateShadows(SHADOW_PRESETS.soft); // High blur, single layer
generateShadows(SHADOW_PRESETS.brutalist); // Hard edges, no blur
generateShadows(SHADOW_PRESETS.neumorphism); // Inset + outerShadow styles:
- material: Three layers (umbra, penumbra, ambient) for realistic depth
- soft: Single layer with high blur for diffuse shadows
- hard: Minimal blur, high contrast for retro look
- inset: Inner shadows for pressed/recessed states
Relations Detection
Auto-detect relationships between design tokens using naming patterns.
import { detectRelations, wouldCreateDirectCycle } from "bdsg";
const existingNodes = [
{ id: "1", name: "primary", category: "color", type: "token", value: "#3B82F6" },
{ id: "2", name: "spacing-base", category: "spacing", type: "token", value: "8px" },
];
// Detect paired naming: "primary-text" uses "primary"
const newNode = { id: "3", name: "primary-text", category: "color", type: "token", value: "#FFF" };
detectRelations(newNode, existingNodes);
// [{
// fromNodeId: "3",
// toNodeId: "1",
// type: "uses",
// confidence: 0.9,
// reason: 'Token "primary-text" uses "primary" (paired naming pattern)'
// }]
// Detect hierarchical: "spacing-sm" depends on "spacing-base"
detectRelations(
{ id: "4", name: "spacing-sm", category: "spacing", type: "token", value: "4px" },
existingNodes
);
// [{ type: "depends_on", confidence: 0.7, ... }]
// Detect palette shades: "primary-500" uses "primary"
detectRelations(
{ id: "5", name: "primary-500", category: "color", type: "token", value: "#3B82F6" },
existingNodes
);
// [{ type: "uses", confidence: 0.8, ... }]
// Prevent cycles
wouldCreateDirectCycle("node-1", "node-1"); // true
wouldCreateDirectCycle("node-1", "node-2"); // falseDetection patterns:
| Pattern | Suffixes | Relation Type | Confidence |
|---------|----------|---------------|------------|
| Paired naming | -text, -bg, -border, -hover | uses | 0.9 |
| Hierarchical | -base, -default, -primary | depends_on | 0.7 |
| Palette shades | -50 to -900 | uses | 0.8 |
Validation
All generation functions validate input using Zod schemas. Invalid configurations throw descriptive errors:
generateTypographyScale({ base: 4 });
// Error: Invalid typography config: Number must be greater than or equal to 8
generateSpacingScale({ steps: 50 });
// Error: Invalid spacing config: Number must be less than or equal to 20
generatePalette("invalid");
// Error: Invalid base color: "invalid". Invalid hex color. Expected format: #RRGGBB or #RGBLicense
MIT
