npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@easemate/web-kit

v0.3.5

Published

UI kit of web components for easemate - a typescript animation library

Downloads

1,702

Readme

A modern, framework-agnostic UI kit of web components for building animation control panels.

npm version npm package minimized gzipped size npm downloads license

Table of Contents


Features

  • Rich Component Library - Sliders, toggles, color pickers, dropdowns, curve editors, and more
  • Dark Theme by Default - Beautiful dark UI with OKLAB color palette
  • Framework Agnostic - Works with vanilla JS, React, Vue, Svelte, or any framework
  • React/Next.js Ready - First-class React integration with hooks and SSR support
  • Tree-Shakeable - Import only what you need
  • TypeScript First - Full type definitions included
  • Accessible - ARIA attributes and keyboard navigation
  • Customizable - CSS custom properties and ::part selectors for styling
  • State Aggregation - Control panel state management with <ease-state>
  • Flexible Layout - Separate <ease-panel> and <ease-state> for maximum flexibility
  • No CSS Import Required - initWebKit() handles everything programmatically

Installation

npm install @easemate/web-kit
# or
pnpm add @easemate/web-kit
# or
yarn add @easemate/web-kit

Quick Start

Basic Usage

import { initWebKit } from '@easemate/web-kit';

// Minimal - just register components
initWebKit();

// Full setup with theme, styles, and fonts
const kit = initWebKit({
  theme: 'default',
  styles: 'main',
  fonts: 'default'
});

// Components are now registered and ready to use!

This single call:

  • Registers all custom elements
  • Applies the dark theme variables
  • Injects CSS reset and base styles
  • Loads the default fonts (Instrument Sans, Geist Mono)

Selective Loading

import { initWebKit } from '@easemate/web-kit';

// Only register specific components
initWebKit({
  include: ['ease-button', 'ease-slider', 'ease-toggle'],
  theme: 'default'
});

// Or exclude components you don't need
initWebKit({
  exclude: ['ease-curve', 'ease-code'],
  theme: 'default'
});

Theme Switching

import { initWebKit, registerTheme } from '@easemate/web-kit';

// Register a custom light theme
registerTheme('light', {
  base: null,
  config: {
    colors: {
      gray: { 900: 'oklab(98% 0 0)', 0: 'oklab(20% 0 0)' },
      foreground: 'var(--color-gray-0)'
    },
    vars: {
      '--ease-panel-background': 'white',
      '--ease-panel-border-color': 'color-mix(in oklab, black 10%, transparent)'
    }
  }
});

// Initialize with system theme detection
const kit = initWebKit({
  theme: {
    mode: 'system', // 'light', 'dark', or 'system'
    light: 'light',
    dark: 'default'
  },
  styles: 'main',
  fonts: 'default'
});

// Switch themes at runtime
kit.theme?.mode('dark');
kit.theme?.set('light');

React & Next.js

The library provides first-class React integration via @easemate/web-kit/react.

JSX Types

Importing @easemate/web-kit/react automatically adds JSX types for all ease-* custom elements, including:

  • All control components (ease-slider, ease-toggle, ease-input, etc.)
  • Layout components (ease-panel, ease-state, ease-field, etc.)
  • Advanced components (ease-curve, ease-code, ease-monitor-fps, etc.)
  • All icon components (ease-icon-settings, ease-icon-bezier, etc.)
import '@easemate/web-kit/react';

// Now ease-* elements are typed in JSX
<ease-panel>
  <ease-slider name="volume" value={50} />
</ease-panel>

You can also import just the JSX types separately (useful for type-only imports):

import '@easemate/web-kit/react/jsx';

The types include proper ref types for accessing component methods:

import type { StateElement, PanelElement } from '@easemate/web-kit/react';

const stateRef = useRef<StateElement>(null);
const panelRef = useRef<PanelElement>(null);

// Access typed methods
stateRef.current?.get('volume');
stateRef.current?.reset();
panelRef.current?.setActiveTab(1);

Basic Setup

// app/providers.tsx
'use client';

import { useEffect, useState, useRef } from 'react';
import { initWebKit, type WebKitController } from '@easemate/web-kit';

export function Providers({ children }: { children: React.ReactNode }) {
  const [ready, setReady] = useState(false);
  const controllerRef = useRef<WebKitController | null>(null);

  useEffect(() => {
    const controller = initWebKit({
      theme: 'default',
      styles: 'main',
      fonts: 'default'
    });
    controllerRef.current = controller;
    controller.ready.then(() => setReady(true));

    return () => controller.dispose();
  }, []);

  return <>{children}</>;
}

Next.js App Router

For Next.js 13+ with App Router, create a client component wrapper:

// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

useWebKit Hook

The useWebKit hook initializes the web kit and tracks readiness:

'use client';

import { useState, useEffect, useRef } from 'react';
import { useWebKit } from '@easemate/web-kit/react';

function App() {
  const { ready, theme, dispose } = useWebKit(
    {
      theme: 'default',
      styles: 'main',
      fonts: 'default',
      skip: false // Optional: skip initialization
    },
    { useState, useEffect, useRef }
  );

  if (!ready) return <div>Loading...</div>;

  return (
    <ease-panel>
      <ease-slider name="value" value="0.5" />
    </ease-panel>
  );
}

The hook manages a singleton controller internally, so multiple components using useWebKit will share the same initialization.

useEaseState Hook

The useEaseState hook provides reactive state management for controls:

'use client';

import { useState, useCallback, useRef } from 'react';
import { useEaseState } from '@easemate/web-kit/react';

interface AnimationState {
  duration: number;
  easing: string;
  loop: boolean;
}

function AnimationControls() {
  const {
    stateRef,
    panelRef,
    state,
    get,
    set,
    reset,
    setTab,
    activeTab
  } = useEaseState<AnimationState>(
    {
      initialState: { duration: 1, easing: 'ease-out', loop: false },
      onChange: ({ name, value }) => {
        console.log(`${name} changed to ${value}`);
      },
      onTabChange: ({ index }) => {
        console.log(`Switched to tab ${index}`);
      }
    },
    { useState, useCallback, useRef }
  );

  return (
    <ease-panel ref={panelRef} headline="Animation">
      <ease-state ref={stateRef}>
        <ease-field label="Duration">
          <ease-slider name="duration" value="1" min="0" max="5" step="0.1" />
        </ease-field>
        <ease-field label="Easing">
          <ease-dropdown name="easing" value="ease-out">
            <button slot="content" value="linear">Linear</button>
            <button slot="content" value="ease-out">Ease Out</button>
          </ease-dropdown>
        </ease-field>
        <ease-field label="Loop">
          <ease-toggle name="loop" />
        </ease-field>
      </ease-state>
      <div slot="footer">
        <ease-button onClick={() => reset()}>Reset</ease-button>
      </div>
    </ease-panel>
  );
}

useEaseState Return Values

| Property | Type | Description | |----------|------|-------------| | stateRef | (element: EaseStateRef \| null) => void | Ref callback for <ease-state> | | panelRef | (element: EasePanelRef \| null) => void | Ref callback for <ease-panel> | | state | T | Current state values (reactive) | | get | (name: keyof T) => T[keyof T] | Get a specific control value | | set | (name: keyof T, value: T[keyof T]) => void | Set a control value | | reset | () => void | Reset all controls to initial values | | setTab | (index: number) => void | Switch panel tab | | activeTab | number | Current active tab index |

WebKit Provider

For apps needing shared context, use createWebKitProvider:

// providers.tsx
'use client';

import * as React from 'react';
import { createWebKitProvider } from '@easemate/web-kit/react';

const { WebKitProvider, useWebKitContext } = createWebKitProvider(React);

export { WebKitProvider, useWebKitContext };
// layout.tsx
import { WebKitProvider } from './providers';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <WebKitProvider
      options={{ theme: 'default', styles: 'main', fonts: 'default' }}
      immediate={true} // Render children before ready (default: true)
    >
      {children}
    </WebKitProvider>
  );
}
// component.tsx
import { useWebKitContext } from './providers';

function MyComponent() {
  const { ready, theme } = useWebKitContext();

  if (!ready) return <div>Loading...</div>;

  return <ease-slider name="value" value="0.5" />;
}

Event Utilities

The React module exports typed event creators:

import { createEventHandler, type ControlChangeEvent, type StateChangeEvent, type TabChangeEvent } from '@easemate/web-kit/react';

// Create typed event handlers
const handleChange = createEventHandler<ControlChangeEvent>((e) => {
  console.log(e.detail.name, e.detail.value);
});

Components

Controls

| Component | Tag | Description | |-----------|-----|-------------| | Slider | <ease-slider> | Range slider with min/max/step | | Toggle | <ease-toggle> | Boolean switch | | Checkbox | <ease-checkbox> | Checkbox input | | Input | <ease-input> | Text input | | NumberInput | <ease-number-input> | Numeric input with stepper | | ColorInput | <ease-color-input> | Color input with picker | | ColorPicker | <ease-color-picker> | Full color picker UI | | Dropdown | <ease-dropdown> | Select dropdown | | RadioGroup | <ease-radio-group> | Radio button group | | RadioInput | <ease-radio-input> | Individual radio option | | Origin | <ease-origin> | Transform origin picker |

Layout & Display

| Component | Tag | Description | |-----------|-----|-------------| | Panel | <ease-panel> | Visual container with tabs, header, and footer | | Folder | <ease-folder> | Collapsible container for grouping controls | | State | <ease-state> | State aggregator for controls (no visual styling) | | Field | <ease-field> | Label + control wrapper | | Button | <ease-button> | Action button | | Tooltip | <ease-tooltip> | Tooltip wrapper | | Popover | <ease-popover> | Floating content |

Advanced

| Component | Tag | Description | |-----------|-----|-------------| | Curve | <ease-curve> | Cubic bezier / linear easing editor | | Code | <ease-code> | Syntax highlighted code | | Monitor | <ease-monitor> | Value monitor display | | MonitorFps | <ease-monitor-fps> | FPS counter | | LogoLoader | <ease-logo-loader> | Animated logo with intro animations and loading state |

Icons

All icon components follow the pattern <ease-icon-*>. All icons are typed in JSX when importing @easemate/web-kit/react.

Interface Icons: | Tag | Description | |-----|-------------| | ease-icon-settings | Settings gear icon | | ease-icon-dots | Three dots / more menu icon | | ease-icon-plus | Plus / add icon | | ease-icon-minus | Minus / remove icon | | ease-icon-check | Checkmark icon | | ease-icon-code | Code brackets icon | | ease-icon-picker | Eyedropper picker icon | | ease-icon-mention | @ mention icon | | ease-icon-arrow-up | Up arrow icon | | ease-icon-arrows-vertical | Vertical arrows icon | | ease-icon-circle-arrow-left | Left arrow in circle | | ease-icon-circle-arrow-right | Right arrow in circle | | ease-icon-anchor-add | Add anchor point | | ease-icon-anchor-remove | Remove anchor point | | ease-icon-bezier | Bezier curve icon | | ease-icon-bezier-angle | Bezier angle tool | | ease-icon-bezier-distribute | Distribute bezier points | | ease-icon-bezier-length | Bezier length tool | | ease-icon-bezier-mirror | Mirror bezier handles |

Animation Icons: | Tag | Description | |-----|-------------| | ease-icon-chevron | Animated chevron (expands/collapses) | | ease-icon-clear | Animated clear/X icon | | ease-icon-folder | Animated folder open/close icon | | ease-icon-grid | Animated grid icon | | ease-icon-loading | Animated loading spinner | | ease-icon-snap | Animated snap indicator |


Usage Examples

Basic Controls

<ease-slider name="opacity" value="0.5" min="0" max="1" step="0.01"></ease-slider>
<ease-toggle name="visible" checked></ease-toggle>
<ease-color-input name="background" value="#3b82f6"></ease-color-input>
<ease-input name="label" value="Hello"></ease-input>
<ease-number-input name="count" value="42" min="0" max="100"></ease-number-input>

Panel Component

The <ease-panel> component provides the visual container with optional tabs, header actions, and footer. It does NOT manage state - use <ease-state> for that.

<!-- Simple panel with headline -->
<ease-panel headline="Settings">
  <div>
    <!-- Your content here -->
  </div>
</ease-panel>

<!-- Panel with tabs -->
<ease-panel headline="Animation" active-tab="0">
  <div slot="tab-general" data-tab-label="General">
    <!-- General settings -->
  </div>
  <div slot="tab-advanced" data-tab-label="Advanced">
    <!-- Advanced settings -->
  </div>
</ease-panel>

<!-- Panel with header actions -->
<ease-panel headline="Controls">
  <button slot="actions" title="Settings">
    <ease-icon-settings></ease-icon-settings>
  </button>
  <div>
    <!-- Content -->
  </div>
  <div slot="footer">
    <ease-button>Save</ease-button>
  </div>
</ease-panel>

<!-- Panel with max-height (scrollable) -->
<ease-panel headline="Scrollable Panel" max-height="250px">
  <!-- Content will scroll when it exceeds 250px -->
</ease-panel>

Folder Component

The <ease-folder> component provides a collapsible container for grouping controls. Click the header to toggle open/closed.

<!-- Basic folder (closed by default) -->
<ease-folder headline="Transform">
  <ease-field label="X">
    <ease-number-input name="x" value="0"></ease-number-input>
  </ease-field>
  <ease-field label="Y">
    <ease-number-input name="y" value="0"></ease-number-input>
  </ease-field>
</ease-folder>

<!-- Open folder -->
<ease-folder open headline="Appearance">
  <ease-field label="Color">
    <ease-color-input name="color" value="#3b82f6"></ease-color-input>
  </ease-field>
</ease-folder>

<!-- Folder with max-height (scrollable with fade masks) -->
<ease-folder headline="Animation" max-height="150px">
  <!-- Many fields here will scroll with fade effect -->
</ease-folder>

State Component

The <ease-state> component aggregates state from child controls. It can be used standalone (no panel styling) or inside a panel.

<!-- Standalone state (no panel) - useful for embedding in your own UI -->
<ease-state>
  <ease-field label="Duration">
    <ease-slider name="duration" value="1" min="0" max="5" step="0.1"></ease-slider>
  </ease-field>
  <ease-field label="Easing">
    <ease-dropdown name="easing" value="ease-out">
      <button slot="content" value="linear">Linear</button>
      <button slot="content" value="ease-out">Ease Out</button>
    </ease-dropdown>
  </ease-field>
  <ease-field label="Loop">
    <ease-toggle name="loop"></ease-toggle>
  </ease-field>
</ease-state>

Combined Panel + State

For a complete control panel experience, combine <ease-panel> with <ease-state>:

<ease-panel headline="Animation Controls">
  <button slot="actions" title="Reset">
    <ease-icon-minus></ease-icon-minus>
  </button>
  
  <ease-state>
    <ease-field label="Duration">
      <ease-slider name="duration" value="1" min="0" max="5" step="0.1"></ease-slider>
    </ease-field>
    <ease-field label="Easing">
      <ease-dropdown name="easing" value="ease-out">
        <button slot="content" value="linear">Linear</button>
        <button slot="content" value="ease-in">Ease In</button>
        <button slot="content" value="ease-out">Ease Out</button>
        <button slot="content" value="ease-in-out">Ease In-Out</button>
      </ease-dropdown>
    </ease-field>
    <ease-field label="Loop">
      <ease-toggle name="loop"></ease-toggle>
    </ease-field>
  </ease-state>
  
  <div slot="footer">
    <ease-button>Apply</ease-button>
    <ease-button variant="secondary">Cancel</ease-button>
  </div>
</ease-panel>

Panel with Tabs + State

When using tabs with state, place the <ease-state> inside each tab:

<ease-panel headline="Animation" active-tab="0">
  <button slot="actions" title="Reset">
    <ease-icon-minus></ease-icon-minus>
  </button>
  
  <div slot="tab-transform" data-tab-label="Transform">
    <ease-state>
      <ease-field label="X">
        <ease-number-input name="x" value="0"></ease-number-input>
      </ease-field>
      <ease-field label="Y">
        <ease-number-input name="y" value="0"></ease-number-input>
      </ease-field>
      <ease-field label="Rotation">
        <ease-slider name="rotation" value="0" min="0" max="360"></ease-slider>
      </ease-field>
    </ease-state>
  </div>
  
  <div slot="tab-style" data-tab-label="Style">
    <ease-state>
      <ease-field label="Opacity">
        <ease-slider name="opacity" value="1" min="0" max="1" step="0.01"></ease-slider>
      </ease-field>
      <ease-field label="Color">
        <ease-color-input name="color" value="#3b82f6"></ease-color-input>
      </ease-field>
    </ease-state>
  </div>
</ease-panel>

JavaScript Integration

// Working with ease-state
const state = document.querySelector('ease-state');

// Get current state
console.log(state.state); // { duration: 1, easing: 'ease-out', loop: false }

// Get individual value
const duration = state.get('duration');

// Set value programmatically
state.set('duration', 2.5);

// Subscribe to changes
const sub = state.subscribe((value, name) => {
  console.log(`${name} changed to:`, value);
});

// Subscribe to specific control
state.subscribe('duration', (value) => {
  myAnimation.duration = value;
});

// Reset to initial values
state.reset();

// Cleanup
sub.unsubscribe();
// Working with ease-panel
const panel = document.querySelector('ease-panel');

// Get current active tab index
console.log(panel.activeTab); // 0

// Switch to a specific tab programmatically
panel.setTab(1); // Switch to second tab (0-indexed)

// Or set directly via property
panel.activeTab = 2;

Logo Loader

The <ease-logo-loader> component displays an animated logo with intro animations and an optional loading state.

<!-- Basic usage - plays wave intro on load -->
<ease-logo-loader></ease-logo-loader>

<!-- With particle intro animation -->
<ease-logo-loader intro="particle"></ease-logo-loader>

<!-- With loading state -->
<ease-logo-loader loading></ease-logo-loader>

<!-- Custom size -->
<ease-logo-loader size="48"></ease-logo-loader>

Logo Loader Attributes

| Attribute | Type | Default | Description | |-----------|------|---------|-------------| | intro | 'wave' \| 'particle' | 'wave' | Intro animation variant played on mount | | loading | boolean | false | When true, plays continuous loading animation | | size | number | 36 | Size in pixels | | aria-label | string | - | Accessible label for the logo |

Intro Animations

  • Wave (default): Inner dots appear at half scale, then expand while outer dots fade in with a staggered wave effect
  • Particle: Dots fly in from outside with curved bezier paths, rotation, and shockwave effects on impact

Loading Animation

When the loading attribute is set:

  1. Inner dots scale down and pulse with a breathing effect
  2. Outer dots animate in a circular wave pattern
  3. Animation completes its current cycle before stopping when loading is removed

JavaScript API

const logo = document.querySelector('ease-logo-loader');

// Toggle loading state
logo.loading = true;
logo.loading = false;

// Replay intro animation
logo.playIntro();           // Uses current intro variant
logo.playIntro('wave');     // Force wave intro
logo.playIntro('particle'); // Force particle intro

Theming

The logo uses theme color tokens:

| CSS Variable | Default | Description | |--------------|---------|-------------| | --dot-dark | var(--color-gray-0) | Brightest dot color (inner dots) | | --dot-medium | var(--color-gray-600) | Medium dot color | | --dot-light | var(--color-gray-700) | Dimmest dot color (outer dots) | | --dot-accent | var(--color-blue-600) | Accent color for effects |

Event Handling

All controls dispatch standard events:

// Standard control-change event
slider.addEventListener('control-change', (e: CustomEvent) => {
  const { name, value, event } = e.detail;
  console.log(`${name}: ${value}`);
});

// State aggregator events
state.addEventListener('state-change', (e: CustomEvent) => {
  const { name, value, state } = e.detail;
  console.log('Full state:', state);
});

// Panel tab change event
panel.addEventListener('tab-change', (e: CustomEvent) => {
  const { index, id, event } = e.detail;
  console.log(`Switched to tab ${id} (index: ${index})`);
});

Event Types

| Event | Component | Detail | Description | |-------|-----------|--------|-------------| | control-change | Controls | { name, value, event } | Fired when value changes | | state-change | <ease-state> | { name, value, state, event } | Fired when any control changes | | tab-change | <ease-panel> | { index, id, event } | Fired when active tab changes |


Configuration

initWebKit Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | include | string[] | - | Only register these component tags | | exclude | string[] | - | Register all except these tags | | replace | Record<string, Constructor \| string> | - | Replace components with custom implementations | | theme | string \| ThemeRef \| ThemeConfig \| ThemeModeConfig | - | Theme to apply | | target | HTMLElement | document.documentElement | Element to scope theme vars to | | styles | false \| 'reset' \| 'base' \| 'main' | false | Inject global styles | | fonts | false \| 'default' \| FontConfig | false | Font loading configuration | | lazyLoad | boolean \| LazyLoadConfig | false | Enable lazy component loading | | cspNonce | string | - | CSP nonce for injected elements | | dev | { warnUnknownTags?: boolean; logLoads?: boolean } | - | Development options |

Theme Configuration

// Theme mode configuration for light/dark switching
interface ThemeModeConfig {
  mode: 'light' | 'dark' | 'system';
  light: ThemeInput;
  dark: ThemeInput;
  persist?: { key: string }; // Coming soon
}

Custom Theme Registration

Register custom themes using registerTheme(). No TypeScript declaration files needed - custom theme names are fully supported:

import { initWebKit, registerTheme } from '@easemate/web-kit';

// Register a custom theme - returns a typed theme ref
const brandTheme = registerTheme('brand', {
  base: 'default', // Inherit from built-in theme ('default' or 'dark')
  config: {
    typography: {
      fontFamily: '"Inter", system-ui, sans-serif'
    },
    vars: {
      '--ease-panel-radius': '16px',
      '--ease-panel-padding': '16px'
    }
  }
});

// Use the theme ref (type-safe)
initWebKit({ theme: brandTheme });

// Or use the string name directly (also works!)
initWebKit({ theme: 'brand' });

Theme Inheritance

Themes can extend other themes using the base option:

// Create a base brand theme
const brandBase = registerTheme('brand-base', {
  base: 'default',
  config: {
    typography: { fontFamily: '"Inter", sans-serif' },
    vars: { '--ease-panel-radius': '16px' }
  }
});

// Create variants that extend brand-base
const brandLight = registerTheme('brand-light', {
  base: brandBase, // Can use theme ref or string name
  config: {
    colors: {
      gray: { 900: 'oklab(98% 0 0)', 0: 'oklab(20% 0 0)' }
    },
    vars: { '--ease-panel-background': 'white' }
  }
});

const brandDark = registerTheme('brand-dark', {
  base: 'brand-base', // String name works too
  config: {
    vars: { '--ease-panel-background': 'var(--color-gray-1000)' }
  }
});

// Use with theme mode switching
initWebKit({
  theme: {
    mode: 'system',
    light: brandLight,
    dark: brandDark
  }
});

Creating a Theme from Scratch

Use base: null to create a theme without inheriting defaults:

const minimalTheme = registerTheme('minimal', {
  base: null, // Start fresh, no inheritance
  config: {
    colors: {
      gray: { 900: '#f5f5f5', 0: '#171717' },
      blue: { 500: '#3b82f6' }
    },
    vars: {
      '--ease-panel-background': 'white',
      '--ease-panel-border-color': '#e5e5e5'
    }
  }
});

Theme Utilities

import { 
  registerTheme, 
  getTheme, 
  hasTheme, 
  getThemeNames,
  themeRef 
} from '@easemate/web-kit';

// Check if a theme exists
if (hasTheme('brand')) {
  console.log('Brand theme is registered');
}

// Get all registered theme names
const themes = getThemeNames(); // ['default', 'dark', 'brand', ...]

// Get resolved theme config (with inheritance applied)
const config = getTheme('brand');

// Get a theme ref for an already-registered theme
const ref = themeRef('brand');

Font Configuration

// Use default fonts (Instrument Sans, Geist Mono)
fonts: 'default'

// Custom Google fonts
fonts: {
  'Inter': { source: 'google', family: 'Inter', css2: '[email protected]' },
  'JetBrains Mono': { source: 'google', family: 'JetBrains Mono' }
}

// Self-hosted fonts
fonts: {
  'Custom Font': { source: 'css', url: '/fonts/custom.css' }
}

Lazy Loading

initWebKit({
  lazyLoad: true, // Auto-define components when they appear in DOM
  theme: 'default'
});

// Advanced lazy loading
initWebKit({
  lazyLoad: {
    strategy: 'mutation',
    include: ['ease-slider', 'ease-toggle'],
    preload: ['ease-button'] // Load immediately
  }
});

Component Replacement

import { initWebKit } from '@easemate/web-kit';
import { CustomInput } from './custom-input';

initWebKit({
  replace: {
    'ease-input': CustomInput, // Your custom element class
    // or alias to another tag:
    // 'ease-input': 'my-custom-input'
  },
  theme: 'default'
});

Theming

CSS Custom Properties

All components use CSS custom properties. Override them at any scope:

:root {
  --ease-panel-padding: 16px;
  --ease-panel-radius: 14px;
  --ease-button-radius: 8px;
}

/* Or scope to specific containers */
.my-panel {
  --ease-panel-background: var(--my-bg);
}

Multiple themes using data-ease-theme:

:root[data-ease-theme='dark'] {
  --ease-panel-background: var(--color-gray-1000);
}

:root[data-ease-theme='light'] {
  --ease-panel-background: white;
}

JavaScript Theme API

import { applyTheme, createTheme, mergeTheme, setThemeName } from '@easemate/web-kit/theme';

// Merge with defaults
const theme = mergeTheme({
  vars: { '--ease-panel-padding': '16px' }
});

// Apply to document
applyTheme(theme, { name: 'custom', colorScheme: 'dark' });

// Generate CSS string
const css = createTheme(theme, ':root[data-ease-theme="custom"]');

// Switch theme name
setThemeName('light', { colorScheme: 'light' });

Token Reference

Global Tokens

| Category | Variables | |----------|-----------| | Colors | --color-gray-*, --color-blue-*, --color-green-*, --color-red-*, --color-orange-*, --color-yellow-* | | Radii | --radii-sm, --radii-md, --radii-lg, --radii-xl, --radii-full | | Spacing | --spacing-xs, --spacing-sm, --spacing-md, --spacing-lg, --spacing-xl | | Typography | --font-family, --font-mono, --font-size, --font-line-height |

UI Kit Tokens (--ease-*)

| Category | Variables | |----------|-----------| | Typography | --ease-font-family, --ease-font-mono, --ease-font-size, --ease-line-height | | Panel | --ease-panel-max-width, --ease-panel-padding, --ease-panel-radius, --ease-panel-background, --ease-panel-border-color, --ease-panel-shadow, --ease-panel-gap, --ease-panel-header-spacing, --ease-panel-fade-size | | Panel Title | --ease-panel-title-font-size, --ease-panel-title-font-weight, --ease-panel-title-color | | Panel Tabs | --ease-panel-tab-font-size, --ease-panel-tab-font-weight, --ease-panel-tab-color, --ease-panel-tab-color-hover, --ease-panel-tab-color-active, --ease-panel-tab-background-active, --ease-panel-tab-radius | | Panel Actions | --ease-panel-action-icon-size | | Panel Footer | --ease-panel-footer-padding | | Folder | --ease-folder-padding, --ease-folder-radius, --ease-folder-border-color, --ease-folder-background, --ease-folder-shadow, --ease-folder-gap, --ease-folder-title-font-size, --ease-folder-title-font-weight, --ease-folder-title-color, --ease-folder-icon-color, --ease-folder-chevron-color, --ease-folder-chevron-color-hover, --ease-folder-fade-size | | Field | --ease-field-label-width, --ease-field-column-gap, --ease-field-row-gap | | Controls | Each control exposes --ease-<component>-* tokens |


API Reference

Controller API

initWebKit() returns a controller object:

interface WebKitController {
  dispose: () => void;           // Cleanup all injected resources
  ready: Promise<void>;          // Resolves when components are loaded
  theme?: {
    set: (theme) => void;        // Set theme by name/ref/config
    mode?: (mode) => void;       // Set mode (light/dark/system)
  };
}

Package Exports

| Export | Description | |--------|-------------| | @easemate/web-kit | Main entry: initWebKit(), theme utilities, and all types | | @easemate/web-kit/react | React hooks (useWebKit, useEaseState), provider, event utilities, and JSX types | | @easemate/web-kit/react/jsx | JSX type augmentation only (for TypeScript) | | @easemate/web-kit/register | Side-effect import that registers all components (SSR-safe) | | @easemate/web-kit/elements | Individual element classes (Button, Slider, Panel, etc.) | | @easemate/web-kit/decorators | @Component, @Prop, @Watch, @Listen, @Query decorators | | @easemate/web-kit/theme | Theme API: applyTheme, createTheme, mergeTheme, registerTheme | | @easemate/web-kit/utils | Template helpers: classMap, styleMap, when, repeat, etc. |

Panel API

The <ease-panel> component provides the visual container.

Properties

| Property | Type | Default | Description | |----------|------|---------|-------------| | headline | string \| null | null | Panel title text displayed in the header | | activeTab | number | 0 | Zero-based index of the active tab | | maxHeight | string \| null | null | Maximum height for scrollable content (e.g., "250px") |

Methods

| Method | Signature | Description | |--------|-----------|-------------| | setTab | (index: number) => void | Switch to a specific tab by index |

Slots

| Slot | Description | |------|-------------| | actions | Header action buttons, links, or dropdowns | | (default) | Main content area (used when no tabs) | | tab-{id} | Tab panel content (use data-tab-label for display name) | | footer | Footer content below all panels |

CSS Parts

| Part | Description | |------|-------------| | section | Outer container | | header | Header row containing headline/tabs and actions | | headline | Title element | | tabs | Tab button container | | tab | Individual tab button | | actions | Actions container | | content | Content wrapper (handles height animations) | | body | Inner body container (scrollable when max-height is set) | | items | Grid container for slotted content | | tab-panel | Individual tab panel | | footer | Footer container |

Events

| Event | Detail Type | Description | |-------|-------------|-------------| | tab-change | TabChangeEventDetail | Fired when the active tab changes |

interface TabChangeEventDetail {
  index: number;             // Tab index (0-based)
  id: string;                // Tab id from slot name
  event: Event;              // Original event
}

Folder API

The <ease-folder> component provides a collapsible container for grouping controls.

Properties

| Property | Type | Default | Description | |----------|------|---------|-------------| | headline | string \| null | null | Folder title text displayed in the header | | open | boolean | false | Whether the folder is expanded | | maxHeight | string \| null | null | Maximum height for scrollable content (e.g., "150px") |

Methods

| Method | Signature | Description | |--------|-----------|-------------| | toggle | () => void | Toggle the folder open/closed state |

Slots

| Slot | Description | |------|-------------| | (default) | Folder content (fields and controls) |

CSS Parts

| Part | Description | |------|-------------| | section | Outer container | | header | Clickable header row | | icon | Folder icon | | headline | Title element | | chevron | Chevron icon (toggle indicator) | | content | Content wrapper (handles height animations) | | body | Inner body container (scrollable when max-height is set) |

Events

| Event | Detail Type | Description | |-------|-------------|-------------| | folder-toggle | FolderToggleEventDetail | Fired when the folder is opened or closed |

interface FolderToggleEventDetail {
  open: boolean;             // Whether the folder is now open
  event: Event;              // Original event
}

Scrollable Content

When max-height is set, the folder content becomes scrollable with fade gradient masks at the top and bottom edges. The masks automatically appear/disappear based on scroll position using scroll-driven animations.

State API

The <ease-state> component provides state management for controls.

Properties

| Property | Type | Default | Description | |----------|------|---------|-------------| | value | string \| null | null | Reflects the last changed control's value | | state | Record<string, unknown> | {} | Read-only object containing all control values |

Methods

| Method | Signature | Description | |--------|-----------|-------------| | get | (name: string) => unknown | Get a specific control value by name | | set | (name: string, value: unknown) => void | Set a control value programmatically | | subscribe | (callback: (value, name) => void) => { unsubscribe } | Subscribe to all state changes | | subscribe | (name: string, callback: (value, name) => void) => { unsubscribe } | Subscribe to a specific control | | reset | () => void | Reset all controls to their initial values |

Slots

| Slot | Description | |------|-------------| | (default) | Controls to aggregate state from |

CSS Parts

| Part | Description | |------|-------------| | container | Inner container wrapping controls |

Events

| Event | Detail Type | Description | |-------|-------------|-------------| | state-change | StateChangeEventDetail | Fired when any control value changes |

interface StateChangeEventDetail {
  name: string;              // Control name
  value: unknown;            // New value
  state: Record<string, unknown>; // Complete state object
  event: Event;              // Original event
}

Accessibility

Components include:

  • ARIA attributes (role, aria-*)
  • Keyboard navigation (Tab, Arrow keys, Enter, Escape)
  • Focus management
  • Screen reader support
  • disabled state handling

SSR Support

The package is SSR-safe. initWebKit() is a no-op in server environments:

import { initWebKit } from '@easemate/web-kit';

// Safe on server - returns immediately without side effects
const kit = initWebKit({ theme: 'default' });
await kit.ready; // Resolves immediately on server

For React/Next.js, all hooks check for browser environment:

// This is safe to call on the server
const { ready, theme } = useWebKitContext();
// ready will be false on server, true after hydration

Links

Authors

License

MIT © Aaron Iker