Web Loom logoWeb.loom
Published PackagesDesign Core

Design Core

Framework-agnostic design tokens, CSS custom properties, dynamic theming, and a pre-built component library.

Design Core

@web-loom/design-core is a zero-dependency, framework-agnostic design token system. It provides a single source of truth for every visual decision in your application — colors, typography, spacing, shadows, motion, and more — with automatic CSS custom property generation, runtime theming, and an optional pre-built component library.

Overview

  • Design tokens — 15 categories of tokens following the Design Token Community Group format
  • CSS custom properties — all tokens are pre-generated as CSS variables, ready to import
  • Dynamic theming — runtime light / dark / high-contrast switching via data-theme
  • Pre-built component library — 20+ headless CSS components (button, input, modal, card, …)
  • TypeScript — every category is fully typed with generated type definitions
  • Zero dependencies — no runtime dependencies; pure CSS + async utilities

Installation

npm install @web-loom/design-core

Quick Start

Import the full design system

The fastest path: import a single CSS file that includes all token variables and base resets.

/* In your global CSS entry point */
@import '@web-loom/design-core/design-system';

Or in JavaScript:

import '@web-loom/design-core/design-system';

Import individual token categories

Import only what you need to keep bundle sizes small:

import '@web-loom/design-core/css/colors.css';
import '@web-loom/design-core/css/spacing.css';
import '@web-loom/design-core/css/typography.css';

Available categories: borders, breakpoints, colors, cursor-styles, focus-rings, gradients, opacity, radii, shadows, sizing, spacing, timing, transitions, typography, z-index.

Use tokens in CSS

Once imported, every token is available as a CSS custom property:

.card {
  background: var(--color-neutral-white);
  color: var(--color-neutral-gray-900);
  padding: var(--spacing-6);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  transition: box-shadow var(--transition-duration-medium)
    var(--transition-timing-function-ease-out);
}
 
[data-theme='dark'] .card {
  background: var(--color-themed-dark-background);
  color: var(--color-themed-dark-text);
}

Design Token Reference

All tokens follow the Design Token Community Group format: { value, type, description? }. References like {color.neutral.white.value} are resolved automatically at build time.

Colors

CSS variable prefix: --color-*

Base palette

  • primary--color-base-primary#1E40AF
  • secondary--color-base-secondary#64748B
  • success--color-base-success#10B981
  • warning--color-base-warning#F59E0B
  • danger--color-base-danger#EF4444
  • accent1--color-base-accent1#0CD4F3
  • accent2--color-base-accent2#F32B0C

Neutral scale

  • white--color-neutral-white#FFFFFF
  • gray-50--color-neutral-gray-50#F9FAFB
  • gray-100--color-neutral-gray-100#F3F4F6
  • gray-200--color-neutral-gray-200#E5E7EB
  • gray-300--color-neutral-gray-300#D1D5DB
  • gray-400--color-neutral-gray-400#9CA3AF
  • gray-500--color-neutral-gray-500#6B7280
  • gray-600--color-neutral-gray-600#4B5563
  • gray-700--color-neutral-gray-700#374151
  • gray-800--color-neutral-gray-800#1F2937
  • gray-900--color-neutral-gray-900#111827
  • black--color-neutral-black#000000

Themed (semantic) colors

These tokens carry a light / dark / high-contrast variant and respond to the data-theme attribute automatically.

  • --color-themed-light-background#FFFFFF
  • --color-themed-light-text#000000
  • --color-themed-dark-background#111827
  • --color-themed-dark-text#FFFFFF
  • --color-themed-high-contrast-background#000000
  • --color-themed-high-contrast-text#FFFFFF

Alpha colors

  • --color-alpha-primary-50rgba(30, 64, 175, 0.5)
  • --color-alpha-primary-100rgba(30, 64, 175, 1)
  • --color-alpha-black-50rgba(0, 0, 0, 0.5)

Spacing

CSS variable prefix: --spacing-*

A 4-point grid scale (4 px base unit) plus semantic aliases:

  • --spacing-00px
  • --spacing-14px
  • --spacing-28px
  • --spacing-312px
  • --spacing-416px
  • --spacing-520px
  • --spacing-624px
  • --spacing-728px
  • --spacing-832px
  • --spacing-936px
  • --spacing-1040px

Semantic aliases (resolve to the scale above):

  • --spacing-gutter16px (→ spacing.4)
  • --spacing-padding-sm8px / --spacing-padding-md16px / --spacing-padding-lg24px
  • --spacing-margin-sm8px / --spacing-margin-md16px / --spacing-margin-lg24px

Typography

CSS variable prefix: --typography-*

Font families

  • --typography-font-family-base'Inter', sans-serif
  • --typography-font-family-heading'Poppins', sans-serif

Font sizes

  • --typography-font-size-xs12px
  • --typography-font-size-sm14px
  • --typography-font-size-md16px
  • --typography-font-size-lg18px
  • --typography-font-size-xl20px
  • --typography-font-size-2xl24px
  • --typography-font-size-3xl30px

Font weights

  • --typography-font-weight-light300
  • --typography-font-weight-regular400
  • --typography-font-weight-medium500
  • --typography-font-weight-bold700
  • --typography-font-weight-extrabold800

Line heights

  • --typography-line-height-tight1.25
  • --typography-line-height-normal1.5
  • --typography-line-height-loose2

Letter spacing

  • --typography-letter-spacing-tight-0.05em
  • --typography-letter-spacing-normalnormal
  • --typography-letter-spacing-wide0.05em

Text transform

  • --typography-text-case-uppercaseuppercase
  • --typography-text-case-lowercaselowercase
  • --typography-text-case-capitalizecapitalize

Shadows

CSS variable prefix: --shadow-*

  • --shadow-xs0 1px 2px 0 rgba(0,0,0,0.05) — subtle card lift
  • --shadow-sm — dual-layer small shadow — default card
  • --shadow-md0 4px 6px -1px rgba(0,0,0,0.1), … — elevated panel
  • --shadow-lg0 10px 15px -3px rgba(0,0,0,0.1), … — floating element
  • --shadow-xl0 20px 25px -5px rgba(0,0,0,0.1), … — modal / dialog
  • --shadow-innerinset 0 2px 4px 0 rgba(0,0,0,0.06) — pressed / well state
  • --shadow-focus0 0 0 3px rgba(59,130,246,0.5) — focus ring
  • --shadow-md-top — upward-facing medium shadow — bottom sheets

Border Radius

CSS variable prefix: --radius-*

  • --radius-sm4px
  • --radius-md8px
  • --radius-lg16px
  • --radius-full9999px (pill shape)

Z-Index

CSS variable prefix: --z-index-*

  • --z-index-autoauto — browser default
  • --z-index-00 — reset stacking context
  • --z-index-1010 — base elements
  • --z-index-2020 — dropdowns
  • --z-index-3030 — fixed / sticky headers
  • --z-index-4040 — modals
  • --z-index-5050 — tooltips / popovers
  • --z-index-max9999 — highest layer (overlays, toasts)

Breakpoints

CSS variable prefix: --breakpoint-*

Mobile-first breakpoints matching common device widths:

  • --breakpoint-xs0px
  • --breakpoint-sm640px — tablets
  • --breakpoint-md768px — small laptops
  • --breakpoint-lg1024px — desktops
  • --breakpoint-xl1280px — large desktops
  • --breakpoint-2xl1536px — wide screens

Media query helpers available as token values:

@media (min-width: 768px) { /* breakpoint.md */ }
@media (orientation: portrait) { /* breakpoint.orientation.portrait */ }
@media (prefers-reduced-motion: reduce) { /* transition.motion.prefersReducedMotion */ }

Transitions

CSS variable prefix: --transition-*

Durations:

  • --transition-duration-fast150ms
  • --transition-duration-medium300ms
  • --transition-duration-slow500ms

Timing functions:

  • --transition-timing-function-linearlinear
  • --transition-timing-function-ease-incubic-bezier(0.4, 0, 1, 1)
  • --transition-timing-function-ease-outcubic-bezier(0, 0, 0.2, 1)
  • --transition-timing-function-ease-in-outcubic-bezier(0.4, 0, 0.2, 1)

Delays:

  • --transition-delay-none0ms
  • --transition-delay-short100ms
  • --transition-delay-long200ms

Always respect prefers-reduced-motion:

.animated {
  transition: transform var(--transition-duration-medium)
    var(--transition-timing-function-ease-out);
}
 
@media (prefers-reduced-motion: reduce) {
  .animated {
    transition: none;
  }
}

Opacity

CSS variable prefix: --opacity-*

  • --opacity-00
  • --opacity-250.25
  • --opacity-500.5
  • --opacity-750.75
  • --opacity-1001
  • --opacity-disabled0.5 — use on disabled UI elements
  • --opacity-muted0.75 — use on muted / secondary text
button:disabled {
  opacity: var(--opacity-disabled);
  cursor: not-allowed;
}

CSS Custom Properties API

The utility functions let you work with tokens programmatically when CSS alone isn't enough.

[object Object]

Converts a dot-path token name to a CSS custom property name.

import { pathToCssVar } from '@web-loom/design-core/utils';
 
pathToCssVar('color.base.primary');       // '--color-base-primary'
pathToCssVar('spacing.4');                // '--spacing-4'
pathToCssVar('typography.font.size.lg');  // '--typography-font-size-lg'

[object Object]

Returns the full var(--...) reference string, ready to use in inline styles or JS-driven CSS.

import { getTokenVar } from '@web-loom/design-core/utils';
 
getTokenVar('color.base.primary'); // 'var(--color-base-primary)'
getTokenVar('shadow.md');          // 'var(--shadow-md)'

[object Object]

Async version of getTokenVar — validates that the token exists before returning the reference. Returns undefined if the path is invalid.

import { getSafeTokenVar } from '@web-loom/design-core/utils';
 
const ref = await getSafeTokenVar('color.base.primary'); // 'var(--color-base-primary)'
const bad = await getSafeTokenVar('color.does.not.exist'); // undefined

[object Object]

Resolves a token to its raw value (async, cached after first call).

import { getTokenValue } from '@web-loom/design-core/utils';
 
const primary = await getTokenValue('color.base.primary'); // '#1E40AF'
const padding = await getTokenValue('spacing.4');          // '16px'
const shadow  = await getTokenValue('shadow.md');          // '0 4px 6px -1px ...'

[object Object]

Returns a flat Record<string, TokenValue> of every CSS variable and its resolved value. Useful for server-side rendering or generating custom stylesheets.

import { generateCssVariablesMap } from '@web-loom/design-core/utils';
 
const map = await generateCssVariablesMap();
// {
//   '--color-base-primary': '#1E40AF',
//   '--spacing-4': '16px',
//   ... 200+ entries
// }

[object Object]

Generates a complete CSS rule string. Defaults to :root.

import { generateCssVariablesString } from '@web-loom/design-core/utils';
 
const css = await generateCssVariablesString(':root');
// ':root { --color-base-primary: #1E40AF; --spacing-4: 16px; ... }'
 
// Inject into the document
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);

[object Object]

Returns the full resolved token tree (all 15 categories, deeply nested).

import { getAllTokens } from '@web-loom/design-core/utils';
 
const tokens = await getAllTokens();
console.log(tokens.color.base.primary); // '#1E40AF'

Theming

Built-in themes

The base CSS ships with three themes controlled by data-theme on the <html> element. Themed tokens (--color-themed-*-background, --color-themed-*-text) switch values automatically via CSS — no JavaScript required.

<html data-theme="light">          <!-- default -->
<html data-theme="dark">
<html data-theme="high-contrast">

[object Object]

Creates a theme object by merging token overrides onto the base set.

import { createTheme } from '@web-loom/design-core/utils';
 
const brandTheme = createTheme('brand', {
  color: {
    base: {
      primary:   { value: '#7C3AED' },
      secondary: { value: '#A78BFA' },
    },
  },
});

[object Object]

Generates and injects a <style> element containing the theme's CSS variables. By default the styles are scoped to [data-theme="<name>"]; pass true to override :root instead.

import { applyTheme } from '@web-loom/design-core/utils';
 
await applyTheme(brandTheme);        // scoped to [data-theme="brand"]
await applyTheme(brandTheme, true);  // overrides :root

[object Object]

Activates a theme by setting data-theme on <html>. The pre-injected CSS takes effect immediately — no re-render required.

import { setTheme } from '@web-loom/design-core/utils';
 
setTheme('dark');
setTheme('brand');
setTheme('light');

[object Object]

Returns the currently active theme name, or null if no data-theme attribute is set.

import { getCurrentTheme } from '@web-loom/design-core/utils';
 
getCurrentTheme(); // 'dark' | 'light' | 'brand' | null

Full theming workflow

import { createTheme, applyTheme, setTheme } from '@web-loom/design-core/utils';
 
// 1. Define custom themes at app startup
const darkTheme = createTheme('dark', {
  color: {
    themed: {
      dark: {
        background: { value: '#0a0a0a' },
        text:       { value: '#f5f5f5' },
      },
    },
  },
});
 
// 2. Inject their CSS once, early in the app lifecycle
await applyTheme(darkTheme);
 
// 3. Switch at runtime — instant, CSS-only
setTheme('dark');
setTheme('light');

Pre-built Component Library

Import @web-loom/design-core/design-system to get a lightweight CSS component library built entirely on top of the design tokens. All components use var(--...) references, so they automatically respond to theme changes.

@import '@web-loom/design-core/design-system';

Components

Formsbutton, checkbox, input, radio-group, select, switch, textarea

Displayavatar, badge, card, list, table

Layoutcontainer, footer, page-content, page-header

Navigationnavigation-bar, sidebar, tabs

Overlaysmodal, toast, tooltip

Utilityloader (spinner)

Usage examples

<!-- Buttons -->
<button class="btn btn-primary">Save</button>
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-danger">Delete</button>
<button class="btn btn-primary" disabled>Disabled</button>
 
<!-- Card -->
<div class="card">
  <div class="card-header">
    <h3 class="card-title">Task Details</h3>
  </div>
  <div class="card-body">
    <p>Card content here.</p>
  </div>
</div>
 
<!-- Badges -->
<span class="badge badge-success">Active</span>
<span class="badge badge-warning">Pending</span>
<span class="badge badge-danger">Failed</span>
 
<!-- Input -->
<div class="input-group">
  <label class="label" for="name">Full name</label>
  <input class="input" id="name" type="text" placeholder="Alice Smith" />
</div>
 
<!-- Loader -->
<div class="loader loader-md"></div>

Dark mode works automatically — just set data-theme="dark" on <html> and all component classes switch palettes with no extra class names needed.


Framework Integration

React

A minimal hook to read the current theme and expose a toggle:

import { useEffect, useState, useCallback } from 'react';
import {
  setTheme,
  getCurrentTheme,
  applyTheme,
  createTheme,
} from '@web-loom/design-core/utils';
 
const darkTheme = createTheme('dark', {});
 
export function useTheme() {
  const [theme, setThemeState] = useState<string>(
    () => getCurrentTheme() ?? 'light',
  );
 
  useEffect(() => {
    applyTheme(darkTheme);
  }, []);
 
  const toggle = useCallback(() => {
    const next = theme === 'light' ? 'dark' : 'light';
    setTheme(next);
    setThemeState(next);
  }, [theme]);
 
  return { theme, toggle };
}

Pairing with inline styles using token variables:

import { getTokenVar } from '@web-loom/design-core/utils';
 
function HighlightBox({ children }: { children: React.ReactNode }) {
  return (
    <div
      style={{
        background:   getTokenVar('color.base.primary'),
        color:        getTokenVar('color.neutral.white'),
        padding:      getTokenVar('spacing.4'),
        borderRadius: getTokenVar('radius.md'),
      }}
    >
      {children}
    </div>
  );
}

Vue 3

// composables/useTheme.ts
import { ref, onMounted } from 'vue';
import {
  setTheme,
  getCurrentTheme,
  applyTheme,
  createTheme,
} from '@web-loom/design-core/utils';
 
const darkTheme = createTheme('dark', {});
 
export function useTheme() {
  const theme = ref(getCurrentTheme() ?? 'light');
 
  onMounted(async () => {
    await applyTheme(darkTheme);
  });
 
  function toggle() {
    const next = theme.value === 'light' ? 'dark' : 'light';
    setTheme(next);
    theme.value = next;
  }
 
  return { theme, toggle };
}

Angular

import { Injectable, signal, effect } from '@angular/core';
import {
  setTheme,
  getCurrentTheme,
  applyTheme,
  createTheme,
} from '@web-loom/design-core/utils';
 
const darkTheme = createTheme('dark', {});
 
@Injectable({ providedIn: 'root' })
export class ThemeService {
  readonly theme = signal<string>(getCurrentTheme() ?? 'light');
 
  constructor() {
    applyTheme(darkTheme);
    effect(() => setTheme(this.theme()));
  }
 
  toggle() {
    this.theme.set(this.theme() === 'light' ? 'dark' : 'light');
  }
}

Vanilla JavaScript

import '@web-loom/design-core/design-system';
import { applyTheme, createTheme, setTheme } from '@web-loom/design-core/utils';
 
const darkTheme = createTheme('dark', {});
await applyTheme(darkTheme);
 
document.getElementById('theme-toggle').addEventListener('click', () => {
  const current = document.documentElement.getAttribute('data-theme');
  setTheme(current === 'dark' ? 'light' : 'dark');
});

Pairing with MVVM Core

ViewModel-driven theming

Keep theme state in a ViewModel rather than scattered across components:

import { signal, computed, effect } from '@web-loom/signals-core';
import {
  setTheme,
  getCurrentTheme,
  applyTheme,
  createTheme,
} from '@web-loom/design-core/utils';
 
const themes = {
  dark:  createTheme('dark', {}),
  light: createTheme('light', {}),
};
 
export class ThemeViewModel {
  private _theme = signal<'light' | 'dark'>(
    (getCurrentTheme() as 'light' | 'dark') ?? 'light',
  );
 
  readonly theme  = this._theme.asReadonly();
  readonly isDark = computed(() => this._theme.get() === 'dark');
 
  private _effectHandle = effect(() => setTheme(this._theme.get()));
 
  constructor() {
    Promise.all([applyTheme(themes.light), applyTheme(themes.dark)]);
  }
 
  setTheme(theme: 'light' | 'dark') {
    this._theme.set(theme);
  }
 
  toggle() {
    this._theme.update((t) => (t === 'light' ? 'dark' : 'light'));
  }
 
  dispose() {
    this._effectHandle.dispose();
  }
}

Model-layer token access

When building user-customizable branding, resolve tokens in the Model and expose them as readonly signals:

import { signal } from '@web-loom/signals-core';
import { getTokenValue } from '@web-loom/design-core/utils';
 
export class BrandingModel {
  private _primaryColor = signal('#1E40AF');
  readonly primaryColor = this._primaryColor.asReadonly();
 
  async loadDefaults() {
    const value = await getTokenValue('color.base.primary');
    if (value) this._primaryColor.set(String(value));
  }
 
  setPrimaryColor(hex: string) {
    this._primaryColor.set(hex);
  }
}

TypeScript

All token category types are exported from @web-loom/design-core/types:

import type {
  DesignTokenValue,
  ColorToken,
  SpacingToken,
  TypographyToken,
  ShadowToken,
  RadiusToken,
  BorderToken,
  BreakpointToken,
  ZIndexToken,
  OpacityToken,
  TimingToken,
  TransitionToken,
  GradientToken,
  FocusRingToken,
  CursorToken,
  SizingToken,
} from '@web-loom/design-core/types';

The root interface that every token follows:

interface DesignTokenValue<T = string> {
  value: T;
  type: string;
  description?: string;
}

Utility types from @web-loom/design-core/utils:

import type {
  TokenValue,
  TokenGroup,
  DesignTokens,
  Theme,
} from '@web-loom/design-core/utils';

Best Practices

  • Import once. Load @web-loom/design-core/design-system (or individual CSS files) at the application entry point — never inside components.
  • Prefer CSS variables over utility functions in CSS. var(--spacing-4) in a stylesheet is zero runtime cost. Only reach for getTokenVar / getTokenValue when you need token values in JavaScript logic.
  • Use semantic tokens for themed values. Prefer --color-themed-light-background over hardcoded hex values so theme switching works automatically without extra CSS.
  • Call applyTheme once on startup. Injecting the same theme multiple times creates duplicate <style> tags; call it during app initialization only.
  • Scope custom themes. When applyToRoot is false (the default) each theme only activates when data-theme="<name>" is set, so multiple themes can coexist in the stylesheet without conflict.
  • Respect motion preferences. Use a @media (prefers-reduced-motion: reduce) block to disable animations for users who prefer reduced motion.
  • Pair with Signals Core for reactive theming. Drive setTheme from a signal so the DOM, derived computed values, and framework views all stay in sync automatically.

See Also

Was this helpful?
Web Loom logoWeb.loom
Copyright © Web Loom. All rights reserved.