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

@afixt/lexic-a11y

v2.0.0

Published

A fully featured, accessible, and internationalized rich text editor built with React and Lexical

Downloads

905

Readme

lexic-a11y: An accessible Lexical Rich Text Editor Package

An accessible and internationalized rich text editor built with React and Lexical. This package provides a modular editor focused on accessibility that supports core formatting options. It is designed to be easily integrated into any React application and to serve as a reusable component for projects requiring high accessibility (WCAG compliant) text editing capabilities.

Overview

lexic-a11y is a self-contained, React-based editor that emphasizes accessibility, extensibility, and internationalization. It leverages the modern Lexical framework by Meta to provide a headless editing experience that can be easily extended and customized. Designed with WCAG-compliant practices in mind, it provides keyboard shortcuts and accessibility features that make rich text editing more accessible to all users.

What It Does

  • Rich Text Editing: Offers essential text formatting including bold, italic, underline, strikethrough, inline code, and a full range of headings (H1–H6).
  • Block Content: Blockquotes, fenced code blocks, horizontal rules, tables with a real header row, and images that require alt text (or an explicit "decorative" opt-out).
  • List Support: Create and manage ordered and unordered lists with proper semantic structure.
  • Link Management: Insert and edit hyperlinks with an accessible dialog interface, with URL scheme validation that rejects unsafe protocols.
  • Markdown Shortcuts: Type #, >, -, 1., **bold**, and friends to format as you go; content can also be serialized to Markdown.
  • Live Document Outline: Opt in with showOutline for a running list of the document's headings, with WCAG-aligned warnings for skipped heading levels and multiple H1s. It is a labelled list, so it contributes neither a heading nor a landmark to the host page's structure.
  • Word and Character Count: Debounced counts surfaced in a polite live region.
  • Paste Sanitization: Pasted markup from Word or Google Docs is cleaned to semantic HTML; Ctrl/Cmd+Shift+V pastes as plain text.
  • Keyboard Shortcuts: Implements a variety of keyboard shortcuts for quick formatting actions (e.g., Ctrl/Cmd+B for bold, Ctrl/Cmd+Alt+1 for Heading 1) and efficient navigation.
  • Documentation: Provides an overlay with available keyboard shortcuts and usage tips (Ctrl/Cmd+D).
  • Internationalization (i18n): Integrated with react-i18next to allow localization of toolbar labels and prompts, making it adaptable for multi-language projects.
  • Accessibility: Designed with accessibility in mind, including ARIA roles, keyboard navigability, and semantic output to ensure compliance with WCAG standards.

Features

  • Core Formatting Options:
    • Text Styling: Bold, Italic, Underline, Strikethrough, inline code.
    • Headings: H1 through H6 with both toolbar buttons and keyboard shortcuts.
  • List Formatting:
    • Ordered Lists: Create numbered lists with proper semantic structure.
    • Unordered Lists: Create bullet lists with proper semantic structure.
  • Content Elements:
    • Links: Insert and edit hyperlinks with an accessible dialog.
    • Images: Insert by URL, or via the optional onImageUpload handler with a drag-and-drop zone and file picker. Alt text is required before the image can be inserted, unless it is explicitly marked decorative (alt="").
    • Tables: Insert with a header row; exported with scope="col".
    • Blockquotes, fenced code blocks, and horizontal rules.
  • Output Formats:
    • Clean HTML (utility classes and Lexical's sizing markup stripped) or Markdown, selected with the outputFormat prop.
  • Internationalization (i18n):
    • Built-in support using react-i18next.
    • Easy to add new languages and localize toolbar and prompt texts.
  • Accessibility (WCAG Compliant):
    • ARIA roles and labels throughout the UI.
    • Fully keyboard accessible, including a roving-tabindex toolbar.
    • Semantic HTML output for screen readers and other assistive technologies.

Keyboard shortcuts

On macOS use Cmd wherever Ctrl is shown. The same list is available in-app from the help overlay (Ctrl+D).

| Shortcut | Action | | -------------------- | ------------------------- | | Ctrl+B / I / U | Bold / Italic / Underline | | Ctrl+Shift+X | Strikethrough | | Ctrl+E | Inline code | | Ctrl+Shift+E | Code block | | Ctrl+\ | Clear formatting | | Ctrl+Alt+[1–6] | Heading 1–6 | | Ctrl+Shift+7 | Ordered (numbered) list | | Ctrl+Shift+8 | Unordered (bullet) list | | Ctrl+Shift+Q | Blockquote | | Ctrl+K | Insert / edit link | | Ctrl+Shift+M | Insert image | | Ctrl+Shift+L | Insert table | | Ctrl+Shift+- | Horizontal rule | | Ctrl+Shift+V | Paste as plain text | | Ctrl+D | Toggle the help overlay | | Escape | Close the open dialog |

The toolbar is a single tab stop with arrow-key roving focus; Home and End jump to the first and last control.

Installation

Prerequisites

  • Node.js — package.json requires v20+, but the repo pins v22 in .nvmrc / .node-version (and CI runs lts/*). Use 22: the @afixt/a11y-assert test dependency declares engines.node >= 22.
  • npm v10+ (enforced via engine-strict=true in .npmrc)
  • React (v16.8+, v17.0.0+, or v18.0.0+ for Hooks support)
  • Homebrew (macOS/Linux) — used by the bootstrap script to install security binaries (trufflehog, osv-scanner, semgrep, lychee)

Steps

  1. Clone the Repository:
git clone https://github.com/AFixt/lexic-a11y.git
cd lexic-a11y
  1. Bootstrap (installs required Homebrew binaries and npm deps):
bash scripts/bootstrap.sh

Or, if you already have the security binaries installed, run the plain install:

npm install
  1. Build the Package:

If you plan to reuse this package in other projects, you can build it as a library:

npm run build

This will generate a bundled version of the editor that can be imported into your projects.

Usage

Integration in a React Project

1. Import the Editor Component

In your React application, import the main Editor component:

import React, { useState } from 'react';
import Editor from '@afixt/lexic-a11y';
import '@afixt/lexic-a11y/dist/styles.css'; // Import the styles

// Resolve an uploaded File to a hosted URL (POST to your backend in a real app).
async function uploadImage(file) {
  const url = await myBackend.upload(file);
  return url;
}

export default function App() {
  const [content, setContent] = useState('');

  return (
    <div>
      <h1>My Application with Lexical Editor</h1>
      <Editor
        onContentChange={setContent}
        outputFormat="html"
        onImageUpload={uploadImage}
        initialValue="<p>Pre-filled <strong>draft</strong> content.</p>"
        showOutline
      />
      <h2>Output HTML</h2>
      <pre>{content}</pre>
    </div>
  );
}

Editor props

| Prop | Type | Default | Description | | ----------------- | --------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | onContentChange | (content: string) => void | — | Called on every edit with the serialized content, in the format chosen by outputFormat. | | outputFormat | 'html' \| 'markdown' | 'html' | Format passed to onContentChange: cleaned HTML or Markdown. Nodes without a Markdown form (tables, images, horizontal rules, code blocks) are omitted from Markdown output. | | onImageUpload | (file: File) => Promise<string> | — | Optional. When provided, the Insert Image dialog gains a drag-and-drop zone and file picker; the handler receives the chosen File and must resolve to the URL to embed. | | initialValue | string | — | Optional trusted HTML used to seed the editor once, on mount (e.g. a saved draft or template). Images, tables, and code blocks are preserved. Later changes to this prop are ignored so user edits are never clobbered. | | showOutline | boolean | false | Whether to render the Document Outline panel below the editing surface. Off by default, which suits short-form embedded fields (a reply box, a ticket description). Pass true for long-form authoring. |

Upgrading

Coming from 1.1.x, the Document Outline panel changed in two ways that affect existing consumers:

  • It no longer renders by default. It used to always render; it is now opt-in. If you want it, pass showOutline:

    <Editor onContentChange={setContent} showOutline />
  • It no longer adds a heading or a landmark to the host page. The title is a <div> rather than an <h2>, the panel's <section> is unnamed (so it stays generic instead of becoming a region landmark), and the heading list is a labelled <ul> rather than a <nav>. An embedded editor therefore leaves the host page's heading hierarchy and landmark structure alone. Class names (.editor-outline, .editor-outline-title, .editor-outline-list) are unchanged, but CSS or queries that relied on the old elements — .editor-outline h2, or a nav inside the panel — need updating.

TypeScript

The package ships hand-maintained declarations at dist/index.d.ts (the source is JavaScript — see ADR 0002). No @types/... package is needed:

import Editor, { ToolbarPlugin, i18n } from '@afixt/lexic-a11y';
import type { EditorProps, OutputFormat } from '@afixt/lexic-a11y';

npm run types:check compiles the declarations under strict, and src/tests/public-api.test.js fails if they drift from the library's real exports.

2. i18n Setup

The package exports its configured i18next instance as a named export alongside the default Editor export. Wrap your application with an i18n provider:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { I18nextProvider } from 'react-i18next';
import { i18n } from '@afixt/lexic-a11y';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <I18nextProvider i18n={i18n}>
    <App />
  </I18nextProvider>,
);

3. Styling

The CSS is included when you import the styles as shown above. This provides styling for the toolbar and editor components.

Customizing the Editor

  • Theme: Lexical class names for each node type are defined in the theme object in src/components/Editor.js. It is not currently exposed as a prop — change it in source (or fork) to rename the classes.
  • Custom Styling: Customize colors, fonts, and spacing by overriding the CSS classes from dist/styles.css (built from src/styles/Editor.css) to match your project's design guidelines.

Extending the Editor

  • Keyboard Shortcuts: The shortcuts are registered in src/components/ToolbarPlugin.js with a document-level keydown listener. You can modify the key combinations or add new ones as needed. (Bold, italic, and underline are deliberately left to Lexical's RichTextPlugin — handling them again there applies the format twice.)
  • Internationalization: Update src/utils/i18n.js to add additional languages or modify existing translations. Use the useTranslation hook within any component to localize additional UI elements.
  • Adding Features: The editor ships with images (insert by URL or via the optional onImageUpload handler), horizontal rules, and tables built in. To add further formatting options, follow Lexical's modular architecture to create and register new nodes.

Development

Running the Editor Locally

To run the editor in a development environment:

  1. Start the Development Server:
npm start

This launches the Vite dev server (configured in vite.config.js) and opens http://localhost:4001 automatically.

  1. Making Changes:
  • Edit the source files in the /src directory.
  • The development server supports hot reloading, so your changes will appear automatically.
  1. Running the Example Demo:

The repository includes a feature-tour example in examples/ that imports the editor straight from src/ (no build step required) and seeds it with sample content exercising every block type — headings, blockquote, lists, a table with a header row, an image with alt text, inline and block code, and a horizontal rule. It also surfaces a "Try this" panel of keyboard shortcuts and Markdown triggers, and the live serialized output.

npm run example

This starts the Vite dev server and opens the example at http://localhost:4001/examples/index.html. Because it imports from src/, your edits to the source are reflected immediately with hot reloading — there is no stale pre-built bundle to rebuild. See examples/README.md for details.

Try it on CodeSandbox

You can also try the editor directly on CodeSandbox without installing anything locally:

Edit lexic-a11y

This opens the sandbox/ demo. Note that it is a deliberately simplified, standalone rebuild of the editor (bold/italic/underline/strikethrough, H1–H3, lists) rather than the packaged component — it exists so CodeSandbox can run without building the library. For the full feature set (tables, images, code blocks, horizontal rules, the link dialog, and every keyboard shortcut), run npm run example locally.

End-to-end tests

The E2E suite runs the demo app in a real browser with Playwright. A rich-text editor's correctness is largely real-browser behavior (keyboard handling, selection, contenteditable quirks, ARIA state), which Jest unit tests cannot exercise.

# One-time: install the Playwright browser
npx playwright install chromium

# Run the suite (starts the Vite dev server automatically)
npm run test:e2e

# Debug with a visible browser
npm run test:e2e:headed

The suite covers typing, formatting (toolbar and keyboard), lists, the link dialog, keyboard navigation, and ARIA state assertions. CI runs it on pull requests via .github/workflows/e2e.yml.

Built-bundle smoke tests

npm test imports from src/, so it cannot see a bug introduced by the build itself. npm run test:dist mounts the real dist/ artifacts — both the ESM and CJS bundles — in jsdom and asserts they render:

npm run build      # test:dist needs the artifacts
npm run test:dist

This guards the class of regression that shipped in v1.1.2, where the bundles referenced an unbound global React and threw React is not defined on mount while the source suite stayed green. npm run check:all runs it after build.

Building for Production

Once you are satisfied with your changes, build the package for production:

npm run build

The production-ready files will be output to the /dist directory.

Contributing

We welcome contributions from the community! If you'd like to contribute:

  • Fork & branch: Branch off develop (feature/<issue>-<slug>).
  • Before pushing: Run npm run check:all — this runs lint, tests, build, duplication check, bundle size, license compliance, npm audit, and trufflehog.
  • Commit style: Conventional Commits (enforced by commitlint via the commit-msg hook).
  • Open issues: Use the repository's issue tracker for bugs or feature requests.

Available scripts

| Script | Purpose | | ------------------------------ | ---------------------------------------------- | | npm start | Start Vite dev server (demo, port 4001) | | npm run example | Open the feature-tour example | | npm run build | Build the library (Rollup → dist/) | | npm run build:analyze | Build with bundle visualizer report | | npm test | Run the Jest test suite | | npm run test:dist | Smoke-test the built bundles (needs build) | | npm run test:e2e | Run the Playwright E2E suite | | npm run lint | Run ESLint | | npm run lint:css | Run Stylelint | | npm run lint:md | Run markdownlint-cli2 | | npm run types:check | Typecheck the published .d.ts under strict | | npm run format | Run Prettier (write mode) | | npm run format:check | Run Prettier in check mode | | npm run dupes | Run jscpd duplication check | | npm run size | Enforce size-limit budgets | | npm run links | Check markdown links with lychee | | npm run security | Run npm audit + OSV + Semgrep + trufflehog | | npm run license:check | Verify production dependency licenses | | npm run security:banned-deps | Fail if a banned package resolves | | npm run check | Lint + stylelint + markdown + format + types | | npm run check:all | Full local gate (used by the pre-push hook) |

Accessibility test tooling: npm test runs automated WCAG assertions via @afixt/a11y-assert, a first-party Afixt dev dependency. It declares engines.node >= 22, so run the test suite on Node 22+ (the version CI uses); Node 20 only emits an EBADENGINE warning. It is a devDependency and is not part of the published package or its production license:check.

axe-core is banned in this project, directly and transitively. A package.json overrides entry resolves it to an empty stub so it can never be installed, and npm run security:banned-deps fails the build naming the dependency that asked for it. Before adding a dependency, check it does not pull in axe-core (eslint-plugin-jsx-a11y, lighthouse, pa11y, jest-axe, and cypress-axe all do). Use @afixt/a11y-assert instead.

Architecture decisions

See docs/adr/ for architectural decision records. Use docs/templates/ADR.md to draft new ones.

License

This project is licensed under the MIT License. See the LICENSE file for more details.