npm package: @pithagon/live-region
Description: A custom element for making announcements with live regions - Svelte 5
Author: Pithagon
Latest version: 0.1.0

# @pithagon/live-region

> A Svelte 5 component for making announcements with ARIA live regions

[![npm version](https://img.shields.io/npm/v/@pithagon/live-region.svg)](https://www.npmjs.com/package/@pithagon/live-region)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Overview

`@pithagon/live-region` provides an accessible way to announce dynamic content changes to screen reader users in Svelte 5 applications. It implements ARIA live regions following best practices for web accessibility.

## Features

- ✅ **Svelte 5 Native** - Built with Svelte 5 runes ($state, $effect)
- ✅ **Accessible** - Follows ARIA live region best practices
- ✅ **TypeScript** - Full type safety with TypeScript
- ✅ **Flexible** - Global announcements or component-level usage
- ✅ **Smart Queuing** - Priority-based message queue (assertive > polite)
- ✅ **Cancelable** - Cancel delayed announcements when needed
- ✅ **Zero Dependencies** - Minimal footprint

## Installation

```bash
# Using npm
npm install @pithagon/live-region

# Using bun
bun add @pithagon/live-region

# Using pnpm
pnpm add @pithagon/live-region
```

## Quick Start

### 1. Set up the global LiveRegion

Add the `LiveRegion` component to your root layout (e.g., `src/routes/+layout.svelte`):

```svelte
<script>
	import { LiveRegion, registerLiveRegion } from '@pithagon/live-region';

	let liveRegion;

	$effect(() => {
		if (liveRegion) {
			registerLiveRegion(liveRegion);
		}
	});
</script>

<LiveRegion bind:this={liveRegion} />

<slot />
```

### 2. Make announcements

Now you can use the global `announce()` function anywhere in your app:

```svelte
<script>
	import { announce } from '@pithagon/live-region';

	function handleSubmit() {
		// ... form submission logic
		announce('Form submitted successfully!');
	}
</script>

<button onclick={handleSubmit}>Submit</button>
```

## Usage

### Basic Announcements

```typescript
import { announce } from '@pithagon/live-region';

// Simple announcement (polite by default)
announce('Item added to cart');

// Polite announcement (doesn't interrupt)
announce('Form saved successfully', {
	politeness: 'polite'
});

// Assertive announcement (interrupts screen reader)
announce('Error: Connection lost', {
	politeness: 'assertive'
});
```

### Delayed Announcements

```typescript
import { announce } from '@pithagon/live-region';

// Announce after 5 seconds
announce('Your session will expire soon', {
	politeness: 'polite',
	delayMs: 5000
});
```

### Canceling Announcements

```typescript
import { announce } from '@pithagon/live-region';

// Create a cancelable announcement
const cancelable = announce('Saving changes...', {
	delayMs: 3000
});

// Cancel if needed (e.g., save completed early)
cancelable.cancel();
```

### Announcing from Elements

```typescript
import { announceFromElement } from '@pithagon/live-region';

const statusElement = document.getElementById('status');
// Uses aria-label if present, otherwise innerText/textContent
announceFromElement(statusElement);
```

### Clearing All Announcements

```typescript
import { clear } from '@pithagon/live-region';

// Clear all pending announcements (useful for route transitions)
clear();
```

### Component-Level Usage

If you prefer not to use global registration:

```svelte
<script>
	import { LiveRegion } from '@pithagon/live-region';

	let liveRegion;

	function handleClick() {
		liveRegion?.announce('Button clicked!');
	}
</script>

<LiveRegion bind:this={liveRegion} />
<button onclick={handleClick}>Click me</button>
```

## API Reference

### Components

#### `LiveRegion`

The main component that creates ARIA live region containers.

**Props:**

- `delay?: number` - Delay between announcements in milliseconds (default: 150)

**Methods:**

- `announce(message: string, options?: AnnounceOptions): Cancelable<Promise<void>>`
- `announceFromElement(element: HTMLElement, options?: AnnounceOptions): Cancelable<Promise<void>>`
- `clear(): void` - Clear all pending messages
- `getMessage(politeness?: 'polite' | 'assertive'): string` - Get current message

### Functions

#### `announce(message, options?)`

Announce a message using the global live region.

**Parameters:**

- `message: string` - The message to announce
- `options?: AnnounceOptions` - Announcement options

**Returns:** `Cancelable<Promise<void>>`

#### `announceFromElement(element, options?)`

Announce the text content of an element.

**Parameters:**

- `element: HTMLElement` - The element to announce from
- `options?: AnnounceOptions` - Announcement options

**Returns:** `Cancelable<Promise<void>>`

#### `registerLiveRegion(instance)`

Register a LiveRegion instance as the global live region.

**Parameters:**

- `instance: LiveRegion` - The LiveRegion component instance

#### `unregisterLiveRegion()`

Unregister the current global live region.

#### `clear()`

Clear all pending announcements from the global live region.

#### `getMessage(politeness?)`

Get the current message from the global live region.

**Parameters:**

- `politeness?: 'polite' | 'assertive'` - The politeness level (default: 'polite')

**Returns:** `string`

### Types

```typescript
type Politeness = 'polite' | 'assertive';

type AnnounceOptions =
	| {
			politeness?: 'assertive';
			delayMs?: never;
	  }
	| {
			politeness?: 'polite';
			delayMs?: number;
	  };

type Cancelable<T> = T & {
	cancel: () => void;
};
```

## Best Practices

### 1. Use One Global LiveRegion

Place the `<LiveRegion />` component in your root layout to ensure it's available across all pages:

```svelte
<!-- src/routes/+layout.svelte -->
<script>
	import { LiveRegion, registerLiveRegion } from '@pithagon/live-region';

	let liveRegion;

	$effect(() => {
		if (liveRegion) {
			registerLiveRegion(liveRegion);
		}
	});
</script>

<LiveRegion bind:this={liveRegion} />
<slot />
```

### 2. Choose Appropriate Politeness Levels

- **Use `polite` (default)** for most announcements:
  - Form submissions
  - Item added to cart
  - Content loaded
  - Search results updated

- **Use `assertive`** only for critical, time-sensitive messages:
  - Fatal errors
  - Connection lost
  - Security warnings
  - Session expiration

### 3. Keep Messages Concise

Screen readers will read the entire message. Keep announcements brief and clear:

```typescript
// Good ✅
announce('3 items added to cart');

// Avoid ❌
announce(
	'The following items have been successfully added to your shopping cart and you can now proceed to checkout if you wish: Item 1, Item 2, Item 3'
);
```

### 4. Clean Up on Route Changes

Clear pending announcements when navigating to prevent outdated messages:

```svelte
<!-- src/routes/+layout.svelte -->
<script>
	import { clear } from '@pithagon/live-region';
	import { beforeNavigate } from '$app/navigation';

	beforeNavigate(() => {
		clear();
	});
</script>
```

### 5. Test with Screen Readers

Always test your announcements with actual screen readers:

- **macOS**: VoiceOver
- **Windows**: NVDA or JAWS
- **Mobile**: VoiceOver (iOS) or TalkBack (Android)

## Accessibility

This component follows ARIA live region specifications:

- ✅ Uses `aria-live="polite"` and `aria-live="assertive"` attributes
- ✅ Uses `aria-atomic="true"` to ensure complete messages are read
- ✅ Visually hidden but accessible to assistive technologies
- ✅ Implements message queuing to prevent dropped announcements
- ✅ Priority-based queue (assertive messages take precedence)

## Examples

See [EXAMPLES.md](./EXAMPLES.md) for more detailed usage examples.

## Contributing

Contributions are welcome! Please read our [Contributing Guide](../../.github/CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.

## License

MIT © [Pithagon](https://github.com/Pithagon)

## Related Packages

- [@pithagon/components](../pithagon-components) - Accessible Svelte 5 components
- [@pithagon/behaviors](../pithagon-behaviors) - Behavioral utilities for Svelte
- [@pithagon/utils](../pithagon-utils) - Common utilities