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

@solopx/spx-ui-kit

v0.2.1

Published

Angular component library with layout container, stepper, and theming support

Readme

spx-ui-kit

A reusable Angular component library for consistent, accessible layouts.

Features

  • Layout Container with fixed header and scrollable content
  • Theming and responsive design
  • Accessibility support
  • Storybook documentation

Usage

Import the SpxLayoutContainerComponent and use the <spx-layout-container> selector in your templates.

Development

  • Run npm start to launch Storybook
  • Run npm test to execute unit tests

See CONTRIBUTING.md for more details.

Stepper Documentation

Overview

The SpxStepper is a powerful CDK-style Angular component for creating multi-step workflows with advanced validation, navigation control, and responsive design. It follows Angular's modern architecture using signals and standalone components.

Installation

npm install @solopx/spx-ui-kit

Basic Usage

Simple Stepper

import { Component, ViewChild } from '@angular/core';
import { SpxStepperComponent, SpxStepComponent, SpxStepPanelComponent } from '@solopx/spx-ui-kit';

@Component({
  selector: 'app-my-stepper',
  imports: [SpxStepperComponent, SpxStepComponent, SpxStepPanelComponent],
  template: `
    <spx-stepper #stepper>
      <spx-step value="step1" label="Basic Information">
        <spx-step-panel value="step1">
          <h3>Step 1: Enter your details</h3>
          <p>Content for step 1...</p>
          <button (click)="stepper.validateAndNext()">Next</button>
        </spx-step-panel>
      </spx-step>

      <spx-step value="step2" label="Confirmation">
        <spx-step-panel value="step2">
          <h3>Step 2: Confirm</h3>
          <p>Content for step 2...</p>
          <button (click)="stepper.validateAndPrevious()">Previous</button>
        </spx-step-panel>
      </spx-step>
    </spx-stepper>
  `,
})
export class MyStepperComponent {
  @ViewChild('stepper') stepper!: SpxStepperComponent;
}

Components

SpxStepperComponent

Main container component that manages step navigation and validation.

Inputs

| Input | Type | Default | Description | | ------------------- | --------- | ------- | ------------------------------------------------------- | | activeStep | string | '' | Initial active step value | | linear | boolean | false | Enable linear mode (users must complete steps in order) | | allowPreviewSteps | boolean | false | Allow clicking step headers to preview future steps |

Outputs

| Output | Type | Description | | ------------------ | ----------------------- | ---------------------------------------------------- | | activeStepChange | string | Emits when active step changes | | stepChange | StepChangeEvent | Emits detailed step change information | | beforeStepChange | BeforeStepChangeEvent | Emits before step change (can be cancelled) | | validateStep | ValidateStepEvent | Emits for step validation with prevention capability |

SpxStepComponent

Defines individual step configuration.

Inputs

| Input | Type | Default | Description | | ----------- | --------- | -------- | ------------------------------ | | value | string | Required | Unique identifier for the step | | label | string | '' | Display label for step header | | completed | boolean | false | Mark step as completed | | disabled | boolean | false | Disable step interaction |

SpxStepPanelComponent

Container for step content with automatic visibility management.

Inputs

| Input | Type | Description | | ------- | -------- | --------------------------------------------------------- | | value | string | Step value this panel belongs to (must match parent step) |

Advanced Usage

Linear Mode with Validation

@Component({
  template: `
    <spx-stepper
      #stepper
      [linear]="true"
      [allowPreviewSteps]="false"
      (validateStep)="onValidateStep($event)"
    >
      <spx-step value="personal" label="Personal Info">
        <spx-step-panel value="personal">
          <form #personalForm="ngForm">
            <input [(ngModel)]="personalData.name" name="name" required />
            <input [(ngModel)]="personalData.email" name="email" required />
          </form>
          <button [disabled]="!stepper.canProceedToNextStep()" (click)="stepper.validateAndNext()">
            Next Step
          </button>
        </spx-step-panel>
      </spx-step>

      <spx-step value="address" label="Address">
        <spx-step-panel value="address">
          <!-- Address form content -->
        </spx-step-panel>
      </spx-step>
    </spx-stepper>
  `,
})
export class LinearStepperComponent {
  personalData = { name: '', email: '' };

  onValidateStep(event: ValidateStepEvent): void {
    if (event.step === 'personal' && event.direction === 'next') {
      if (!this.personalData.name || !this.personalData.email) {
        event.preventDefault(); // Prevent navigation
        alert('Please fill in all required fields');
      }
    }
  }
}

Dynamic Workflow Steps

@Component({
  template: `
    <spx-stepper #stepper (validateStep)="onValidateStep($event)">
      <!-- Initial step -->
      <spx-step value="workflow-type" label="Choose Workflow">
        <spx-step-panel value="workflow-type">
          <label>
            <input
              type="radio"
              [(ngModel)]="workflowType"
              value="simple"
              (change)="onWorkflowChange()"
            />
            Simple Workflow
          </label>
          <label>
            <input
              type="radio"
              [(ngModel)]="workflowType"
              value="advanced"
              (change)="onWorkflowChange()"
            />
            Advanced Workflow
          </label>
        </spx-step-panel>
      </spx-step>

      <!-- Conditional steps based on workflow type -->
      @if (workflowType === 'simple') {
      <spx-step value="simple-config" label="Simple Configuration">
        <spx-step-panel value="simple-config">
          <!-- Simple workflow content -->
        </spx-step-panel>
      </spx-step>
      } @if (workflowType === 'advanced') {
      <spx-step value="advanced-config" label="Advanced Configuration">
        <spx-step-panel value="advanced-config">
          <!-- Advanced workflow content -->
        </spx-step-panel>
      </spx-step>

      <spx-step value="advanced-settings" label="Advanced Settings">
        <spx-step-panel value="advanced-settings">
          <!-- Additional settings -->
        </spx-step-panel>
      </spx-step>
      }
    </spx-stepper>
  `,
})
export class DynamicWorkflowComponent {
  workflowType: 'simple' | 'advanced' | null = null;

  onWorkflowChange(): void {
    // Invalidate steps after workflow selection when changing
    if (this.stepper) {
      this.stepper.invalidateStepsAfter('workflow-type');
    }
  }
}

Event System

ValidateStepEvent

Emitted when step navigation is attempted. Use preventDefault() to stop navigation.

interface ValidateStepEvent {
  step: string;           // Current step being validated
  direction: 'next' | 'previous'; // Navigation direction
  preventDefault(): void;  // Call to prevent navigation
}

onValidateStep(event: ValidateStepEvent): void {
  console.log(`Validating step: ${event.step}, direction: ${event.direction}`);

  // Custom validation logic
  if (event.step === 'payment' && !this.isPaymentValid()) {
    event.preventDefault();
    this.showPaymentError();
  }
}

StepChangeEvent

Emitted after successful step change.

interface StepChangeEvent {
  previousStep: string | null;
  currentStep: string;
  stepIndex: number;
}

onStepChange(event: StepChangeEvent): void {
  console.log(`Changed from ${event.previousStep} to ${event.currentStep}`);

  // Track analytics
  this.analytics.trackStepProgress(event.currentStep, event.stepIndex);
}

BeforeStepChangeEvent

Emitted before step change occurs. Can be cancelled.

interface BeforeStepChangeEvent {
  from: string | null;
  to: string;
  preventDefault(): void;
}

onBeforeStepChange(event: BeforeStepChangeEvent): void {
  // Show confirmation for certain transitions
  if (event.from === 'draft' && event.to === 'published') {
    const confirmed = confirm('Are you sure you want to publish?');
    if (!confirmed) {
      event.preventDefault();
    }
  }
}

Programmatic Navigation

ViewChild Access Pattern

export class MyComponent {
  @ViewChild('stepper') stepper!: SpxStepperComponent;

  // Navigation methods
  goToNextStep(): void {
    this.stepper.validateAndNext();
  }

  goToPreviousStep(): void {
    this.stepper.validateAndPrevious();
  }

  goToSpecificStep(stepValue: string): void {
    this.stepper.setActiveStep(stepValue);
  }

  // State checking
  canProceed(): boolean {
    return this.stepper.canProceedToNextStep();
  }

  getCurrentStep(): string {
    return this.stepper.activeStepValue();
  }

  isStepCompleted(stepValue: string): boolean {
    return this.stepper.isStepCompleted(stepValue);
  }

  // Step management
  markStepComplete(stepValue: string): void {
    this.stepper.markStepCompleted(stepValue);
  }

  resetStepper(): void {
    this.stepper.reset();
  }
}

Available Methods

| Method | Parameters | Return Type | Description | | ----------------------------- | ---------- | ----------- | -------------------------------------------------- | | validateAndNext() | - | boolean | Validate current step and move to next | | validateAndPrevious() | - | boolean | Validate current step and move to previous | | setActiveStep(value) | string | boolean | Set specific step as active | | reset() | - | void | Reset stepper to initial state | | canProceedToNextStep() | - | boolean | Check if next step navigation is allowed | | canNavigateToStep(value) | string | boolean | Check if navigation to specific step is allowed | | isStepCompleted(value) | string | boolean | Check if step is marked as completed | | markStepCompleted(value) | string | void | Mark step as completed | | markStepVisited(value) | string | void | Mark step as visited | | invalidateStepsAfter(value) | string | void | Reset completion status for steps after given step |

Computed Signals

| Signal | Type | Description | | ------------------- | -------------------- | ------------------------------ | | activeStepValue() | string | Currently active step value | | activeStepIndex() | number | Index of currently active step | | stepsArray() | SpxStepComponent[] | Array of all step components | | firstStepValue() | string | Value of first step | | lastStepValue() | string | Value of last step |

Styling & Theming

CSS Classes

The stepper uses BEM-style CSS classes for consistent styling:

// Stepper container
.stepper {
  display: flex;
  // ... stepper styles
}

// Individual step headers
.stepper__item {
  // Base step header styles

  &.active {
    // Active step styles
  }

  &.completed {
    // Completed step styles
  }

  &.disabled {
    // Disabled step styles
  }
}

// Step content panels
.spx-step-panel {
  // Panel container styles
}

Custom Theming

// Override stepper variables
:root {
  --stepper-primary-color: #007bff;
  --stepper-completed-color: #28a745;
  --stepper-disabled-color: #6c757d;
}

// Custom step header styling
.stepper__item {
  border-color: var(--stepper-primary-color);

  &.completed {
    background-color: var(--stepper-completed-color);
  }
}

Best Practices

1. Form Integration

// Use reactive forms for better validation
@Component({
  template: `
    <spx-stepper (validateStep)="onValidateStep($event)">
      <spx-step value="form-step" label="Form Data">
        <spx-step-panel value="form-step">
          <form [formGroup]="myForm">
            <input formControlName="email" type="email" />
            <input formControlName="password" type="password" />
          </form>
        </spx-step-panel>
      </spx-step>
    </spx-stepper>
  `,
})
export class FormStepperComponent {
  myForm = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
  });

  onValidateStep(event: ValidateStepEvent): void {
    if (event.step === 'form-step' && event.direction === 'next') {
      if (this.myForm.invalid) {
        event.preventDefault();
        this.myForm.markAllAsTouched();
      }
    }
  }
}

2. Async Operations

async onValidateStep(event: ValidateStepEvent): Promise<void> {
  if (event.step === 'save-step' && event.direction === 'next') {
    try {
      event.preventDefault(); // Prevent immediate navigation

      const result = await this.apiService.saveData(this.formData);

      if (result.success) {
        this.stepper.markStepCompleted('save-step');
        this.stepper.validateAndNext(); // Now proceed
      } else {
        alert('Save failed. Please try again.');
      }
    } catch (error) {
      console.error('Save error:', error);
    }
  }
}

3. State Management

// Use services for complex state management
@Injectable()
export class StepperStateService {
  private state = signal({
    currentStep: 'step1',
    formData: {},
    completedSteps: new Set<string>(),
  });

  readonly currentStep = computed(() => this.state().currentStep);
  readonly isStepCompleted = (step: string) =>
    computed(() => this.state().completedSteps.has(step));

  updateStep(step: string): void {
    this.state.update((state) => ({
      ...state,
      currentStep: step,
    }));
  }

  completeStep(step: string): void {
    this.state.update((state) => ({
      ...state,
      completedSteps: new Set([...state.completedSteps, step]),
    }));
  }
}

Accessibility

The stepper includes built-in accessibility features:

  • Keyboard navigation with arrow keys and Enter/Space
  • ARIA labels and roles for screen readers
  • Focus management during step transitions
  • High contrast support for step states

Additional ARIA Support

<spx-stepper role="tablist" aria-label="Multi-step form navigation">
  <spx-step value="step1" label="Personal Information" [attr.aria-describedby]="'step1-desc'">
    <spx-step-panel value="step1" role="tabpanel" [attr.aria-labelledby]="'step1-header'">
      <div id="step1-desc">Enter your personal details</div>
      <!-- Content -->
    </spx-step-panel>
  </spx-step>
</spx-stepper>

Testing

E2E Testing with Playwright

import { test, expect } from '@playwright/test';

test('stepper navigation works correctly', async ({ page }) => {
  await page.goto('/stepper-demo');

  // Check initial state
  await expect(page.locator('[data-testid="step-header-step1"]')).toHaveClass(/active/);

  // Navigate to next step
  await page.locator('[data-testid="step1-next-btn"]').click();
  await expect(page.locator('[data-testid="step-header-step2"]')).toHaveClass(/active/);

  // Test validation prevention
  await page.locator('[data-testid="step2-next-btn"]').click();
  // Should stay on step2 if validation fails
  await expect(page.locator('[data-testid="step-header-step2"]')).toHaveClass(/active/);
});

Unit Testing

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SpxStepperComponent } from '@solopx/spx-ui-kit';

describe('SpxStepperComponent', () => {
  let component: SpxStepperComponent;
  let fixture: ComponentFixture<SpxStepperComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [SpxStepperComponent],
    });
    fixture = TestBed.createComponent(SpxStepperComponent);
    component = fixture.componentInstance;
  });

  it('should navigate to next step when valid', () => {
    // Setup test steps
    component.setActiveStep('step1');

    // Test navigation
    const result = component.validateAndNext();
    expect(result).toBe(true);
    expect(component.activeStepValue()).toBe('step2');
  });
});

Troubleshooting

Common Issues

  1. Steps not appearing: Ensure step values are unique and step panels have matching values
  2. Navigation not working: Check that ViewChild reference is properly initialized
  3. Validation not preventing navigation: Make sure to call event.preventDefault() in validation handler
  4. Form state lost: Use proper form management and avoid removing elements from DOM

Debug Mode

Enable debug logging:

// Add to your component
ngAfterViewInit(): void {
  // Enable stepper debugging
  (this.stepper as any)._debug = true;
}

Migration Guide

From version 0.1.x to 0.2.x

  • ✅ No breaking changes
  • ✨ New features: Enhanced validation system, outlet directive
  • 🔧 Improved: Better form state management, accessibility features

For detailed examples and live demos, visit our Storybook documentation.