Skip to content

BrianARuff/accretion_ui

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Accretion UI

Accretion UI is a cross-framework component system built from one core web component library and distributed as independent framework packages.

For product and design leadership, the core proof point is simple: one implementation can ship a consistent button experience across React, Angular 18-20, and Angular 21+ without duplicating component logic.

Start Here (2-Minute Demo)

If you only have a few minutes, open these first:

Framework Target Storybook Chromatic Project npm Package
React (@accretion_ui/react) View Storybook View Chromatic @accretion_ui/react
Angular 18 (@accretion_ui/angular_18) View Storybook View Chromatic @accretion_ui/angular_18
Angular 21 (@accretion_ui/angular_21) View Storybook View Chromatic @accretion_ui/angular_21

Core package (single source of truth): @accretion_ui/core

Figma Design Source

  • Accretion UI Figma library: Accretion UI (Figma)
  • Current status: the Figma library is currently empty.
  • Component styles are taken from the tokens folder files like:
    • /tokens/primitives.json
    • /tokens/semantic/button.json
    • /tokens/semantic/accordion.json

What to Look For in the Demos

  • Same component API across frameworks.
  • Same visual intent hierarchy (primary, secondary, tertiary).
  • Same behavior for hover, active, focus, and disabled states.
  • Same slotted content behavior and clickable interactions.
  • Framework-native state updates driven by the same underlying web component.

Table of Contents

Support Matrix

Area Oldest Supported Newest Supported
Angular wrappers Angular 18.x (@accretion_ui/angular_18) Angular 21.x (@accretion_ui/angular_21)
React wrapper React 18.2.0 React 19.x
Angular package split @accretion_ui/angular_18 for >=18 <21 @accretion_ui/angular_21 for >=21 <22

Packages

Package Purpose npm
@accretion_ui/core Source of truth (core Stencil web components) npm
@accretion_ui/react React wrapper (v18+) npm
@accretion_ui/angular_18 Angular wrapper (v18-v20) npm
@accretion_ui/angular_21 Angular wrapper (v21+, includes signals) npm

How It Works (Single Core, Multiple Frameworks)

Accretion UI uses Stencil to compile standards-based web components and generate framework wrappers.

Key Stencil references:

Flow:

  1. Components are authored once in components/core.
  2. Stencil builds @accretion_ui/core.
  3. Stencil output tooling generates React and Angular wrappers.
  4. Wrappers publish as standalone npm packages.
  5. App teams consume wrappers directly with normal package installs. No app-side code generation is required.

Install

React

npm install @accretion_ui/react

Angular 18-20

npm install @accretion_ui/angular_18 @accretion_ui/core

Angular 21+

npm install @accretion_ui/angular_21 @accretion_ui/core

SSR and SSG pre-style guidance

For server-rendered or statically generated applications, make sure this CSS is present in the initial server response so unresolved custom elements stay hidden until the browser defines them:

accretion-accordion:not(:defined),
accretion-accordion-item:not(:defined),
accretion-accordion-header:not(:defined),
accretion-accordion-trigger:not(:defined),
accretion-accordion-panel:not(:defined),
accretion-button:not(:defined) {
  visibility: hidden;
}

Apply that rule in a global stylesheet or root layout/document <head> that is loaded with the first HTML response. Do not rely on component-scoped CSS, CSS modules attached after hydration, or client-side runtime injection if you need to avoid the refresh-time flash in SSR/SSG apps.

Each package ships the same rule as predefine.css, so the simplest option is to import it from your top-level entry:

// React / Next.js top-level entry
import '@accretion_ui/react/predefine.css';
/* Angular 18-20 src/styles.css */
@import "@accretion_ui/angular_18/predefine.css";

/* Angular 21+ src/styles.css */
@import "@accretion_ui/angular_21/predefine.css";

/* Core-only usage */
@import "@accretion_ui/core/predefine.css";

The wrappers and core runtime also inject the same stylesheet once on the client as a fallback, but that runtime injection cannot prevent a first-paint flash if the CSS was not already present in the initial HTML/CSS payload.

Accretion UI is client-first today. The components can still participate in SSR and SSG, but only with caveats:

  • The server can emit the custom element tags, but the real behavior and styling arrive only after the browser defines and upgrades those elements on the client.
  • React and Angular hydrate before the web components finish upgrading, so framework hydration can observe markup, attributes, or nested structure that is still in a pre-upgrade state.
  • The wrappers proxy props and events onto custom elements rather than rendering native React or Angular component trees, which means strict framework-managed hydration parity is harder to guarantee.

In practice, SSR is useful here for faster first HTML delivery and progressive enhancement, but regions that require strict hydration parity should still prefer client-only rendering or a framework hydration opt-out until the web-component SSR path is more mature.

Framework Setup Examples

Angular 18 (npx @angular/cli@18 new my-app)

npx @angular/cli@18 new my-app
cd my-app
npm install @accretion_ui/angular_18 @accretion_ui/core

src/app/app.component.ts

import { Component } from '@angular/core';
import { AccretionButton } from '@accretion_ui/angular_18';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AccretionButton],
  template: `
    <p>Count: {{ count }}</p>
    <accretion-button variant="primary" (click)="increment()">Increment Count</accretion-button>
    <accretion-button variant="secondary" (click)="decrement()">Decrement Count</accretion-button>
    <accretion-button variant="tertiary" (click)="reset()">Reset Count</accretion-button>
  `
})
export class AppComponent {
  count = 0;

  increment() {
    this.count += 1;
  }

  decrement() {
    this.count -= 1;
  }

  reset() {
    this.count = 0;
  }
}

Latest Angular (ng new my-app)

ng new my-app
cd my-app
npm install @accretion_ui/angular_21 @accretion_ui/core

src/app/app.ts

import { Component, signal } from '@angular/core';
import { AccretionButton } from '@accretion_ui/angular_21';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AccretionButton],
  template: `
    <p>Count: {{ count() }}</p>
    <accretion-button variant="primary" (click)="increment()">Increment Count</accretion-button>
    <accretion-button variant="secondary" (click)="decrement()">Decrement Count</accretion-button>
    <accretion-button variant="tertiary" (click)="reset()">Reset Count</accretion-button>
  `
})
export class App {
  count = signal(0);

  increment() {
    this.count.update((value) => value + 1);
  }

  decrement() {
    this.count.update((value) => value - 1);
  }

  reset() {
    this.count.set(0);
  }
}

React + Vite

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm install @accretion_ui/react

src/App.tsx

import { useState } from 'react';
import { AccretionButton } from '@accretion_ui/react';

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <>
      <p>Count: {count}</p>
      <AccretionButton variant="primary" onClick={() => setCount((value) => value + 1)}>Increment Count</AccretionButton>
      <AccretionButton variant="secondary" onClick={() => setCount((value) => value - 1)}>Decrement Count</AccretionButton>
      <AccretionButton variant="tertiary" onClick={() => setCount(0)}>Reset Count</AccretionButton>
    </>
  );
}

React + CRA

npx create-react-app@latest my-app --template typescript
cd my-app
npm install @accretion_ui/react

src/App.tsx

import { useState } from 'react';
import { AccretionButton } from '@accretion_ui/react';

function App() {
  const [count, setCount] = useState(0);

  return (
    <>
      <p>Count: {count}</p>
      <AccretionButton variant="primary" onClick={() => setCount((value) => value + 1)}>Increment Count</AccretionButton>
      <AccretionButton variant="secondary" onClick={() => setCount((value) => value - 1)}>Decrement Count</AccretionButton>
      <AccretionButton variant="tertiary" onClick={() => setCount(0)}>Reset Count</AccretionButton>
    </>
  );
}

export default App;

React + Next.js

npx create-next-app@latest my-app --typescript
cd my-app
npm install @accretion_ui/react

app/page.tsx

'use client';

import { useState } from 'react';
import { AccretionButton } from '@accretion_ui/react';

export default function Page() {
  const [count, setCount] = useState(0);

  return (
    <>
      <p>Count: {count}</p>
      <AccretionButton variant="primary" onClick={() => setCount((value) => value + 1)}>Increment Count</AccretionButton>
      <AccretionButton variant="secondary" onClick={() => setCount((value) => value - 1)}>Decrement Count</AccretionButton>
      <AccretionButton variant="tertiary" onClick={() => setCount(0)}>Reset Count</AccretionButton>
    </>
  );
}

Testing Strategy and Setup

Fast summary (non-engineering)

Accretion UI uses multiple independent test layers so releases are not validated by a single signal:

  • Story-level behavior tests confirm stories behave the way they claim.
  • Accessibility scans catch WCAG-impacting markup/semantics issues.
  • Keyboard and zoom-focused checks validate interaction quality beyond static rules.
  • Smoke tests validate real app installs in React and Angular before and after publish.

What each test layer validates

Test layer Command Primary purpose Typical failure signal
Core package test runner npm --prefix components/core run test Runs Stencil spec/e2e suite (currently no committed spec files; command kept CI-safe). Build/test harness failure in components/core.
Storybook behavior + prop coverage + Playwright accessibility npm --prefix chromatic run test:accordion Validates Accordion stories, prop behavior, and framework parity across React, Angular 18, Angular 21. Story behavior regression, prop contract break, keyboard/interaction bug.
Storybook accessibility addon (@storybook/addon-a11y) Included in each Storybook preview/build Provides per-story a11y scans in Storybook UI and Chromatic context. WCAG rule violations flagged in Storybook addon panel.
USWDS-aligned keyboard/zoom checks (Playwright) Included in npm --prefix chromatic run test:accordion Covers keyboard interaction and zoom behavior that static axe scans do not fully cover. Focus order, keyboard toggle, zoom overflow/clipping regressions.
Local package smoke matrix npm --prefix testing run verify:local Verifies local tarballs install/build in React Vite, React CRA, React Next, Angular 18, Angular 21. Consumer import/build failures before publish.
npm package smoke matrix npm --prefix testing run verify:npm Verifies published npm artifacts install/build in the same framework matrix. Published package regression, bad manifest/exports/dependency issues.
npm runtime browser smoke matrix npm --prefix testing run verify:npm:browser Starts each npm smoke app in a real browser and validates button + accordion interactions with Playwright. Runtime hydration/interaction failures not caught by build-only smoke checks.

Why both Storybook a11y and Playwright a11y are used

  • addon-a11y and @axe-core/playwright both run axe rules, but in different execution contexts.
  • Axe does not fully cover keyboard workflow expectations (for example, open/close behavior on Enter/Space, tab sequencing through panel content), so dedicated keyboard tests remain required.
  • Accordion-specific keyboard/zoom tests in chromatic/tests/specs/accordion.uswds-a11y.spec.ts cover USWDS-style interaction expectations that are not purely rule-based.

How to run the full release-quality test gate

From repo root:

npm --prefix components/core run test
npm --prefix chromatic run test:accordion
npm --prefix testing run verify:local

After publish, run:

npm --prefix testing run verify:npm
npm --prefix testing run verify:npm:browser

How to extend the test setup for new component behavior

  1. Add or update Storybook stories first in each framework workspace (chromatic/react, chromatic/angular_18, chromatic/angular_21).
  2. Add story IDs to chromatic/tests/specs/helpers.ts and keep IDs framework-agnostic (for example, accretion-accordion--*).
  3. Add behavior and prop assertions in accordion.behavior.spec.ts and/or accordion.props.spec.ts.
  4. Add accessibility expectations in:
    • accordion.a11y.spec.ts for axe blocking violations.
    • accordion.uswds-a11y.spec.ts for keyboard/zoom/interaction expectations.
  5. Re-run npm --prefix chromatic run test:accordion and both smoke scripts before publish.

Smoke Testing (How It Works)

The testing/ workspace is the smoke-test harness that replaced committed demo apps (apps/ and live_apps/).

This keeps the repository slim while still verifying real install paths.

Commands

# 1) Verify local package changes before publish
npm --prefix testing run verify:local

# 2) Verify npm-published packages after publish
npm --prefix testing run verify:npm

# 3) Verify npm-published runtime behavior in a real browser
npm --prefix testing run verify:npm:browser

verify:local flow

  1. Builds local packages: core, react, angular_18, angular_21.
  2. Packs each into tarballs.
  3. Generates temporary apps under .tmp/smoke-local:
    • React Vite
    • React CRA
    • React Next.js
    • Angular 18
    • Angular 21
  4. Installs local tarballs into those apps.
  5. Runs framework builds to confirm imports and compilation.

verify:npm flow

  1. Generates temporary apps under .tmp/smoke-npm for the same framework matrix.
  2. Installs from npm registry instead of local tarballs.
  3. Runs framework builds to validate published artifacts.
  4. If a published wrapper version does not yet expose Accordion exports, the harness falls back to button-only smoke checks and logs that fallback explicitly.

verify:npm:browser flow

  1. Runs verify:npm first to regenerate npm smoke apps.
  2. Starts each generated app server sequentially.
  3. Runs Playwright Chromium checks against each framework target:
    • React Vite
    • React CRA
    • React Next.js
    • Angular 18
    • Angular 21
  4. Validates runtime behavior in browser (button interactions + Accordion interaction where exports exist).

Version pinning is supported:

ACCRETION_CORE_VERSION=<core_version> \
ACCRETION_REACT_VERSION=<react_version> \
ACCRETION_ANGULAR_18_VERSION=<angular_18_version> \
ACCRETION_ANGULAR_21_VERSION=<angular_21_version> \
npm --prefix testing run verify:npm

For Accordion release validation, always pin the just-published package versions so the smoke run validates Accordion imports directly (instead of fallback mode).

Manual runtime checks after smoke scripts

npm --prefix .tmp/smoke-local/react-vite-local start
npm --prefix .tmp/smoke-local/react-cra-local start
npm --prefix .tmp/smoke-local/react-next-local dev
npm --prefix .tmp/smoke-local/angular-18-local start
npm --prefix .tmp/smoke-local/angular-21-local start

Validate:

  • The three variants render with distinct visual intent.
  • Slot text appears inside the button.
  • Increment/decrement/reset actions update framework state.
  • Accordion imports render in React and Angular smoke apps without compile/runtime errors.
  • Accordion trigger/panel interactions render expected expanded/collapsed states.

Storybook and Chromatic Workflow

Chromatic workspace folders:

  • chromatic/react
  • chromatic/angular_18
  • chromatic/angular_21

Install dependencies once:

cd chromatic
npm run install:all

Run Storybook locally:

npm run storybook:react
npm run storybook:angular_18
npm run storybook:angular_21

Publish to Chromatic:

npm run chromatic:react
npm run chromatic:angular_18
npm run chromatic:angular_21

Required environment variables (~/.config/accretion_ui/chromatic.env):

export CHROMATIC_PROJECT_TOKEN_REACT="<token>"
export CHROMATIC_PROJECT_TOKEN_ANGULAR_18="<token>"
export CHROMATIC_PROJECT_TOKEN_ANGULAR_21="<token>"

Release and Publish Workflow

Required publish checklist

Use this checklist before merging a release branch and publishing packages:

  • Scope is confirmed for all framework wrappers (core, react, angular_18, angular_21).
  • Storybook stories are updated for all affected frameworks.
  • npm --prefix components/core run test passes.
  • npm --prefix chromatic run test:accordion passes.
  • npm --prefix testing run verify:local passes.
  • Package versions are bumped and wrapper core ranges are aligned.
  • Production builds for all publishable packages pass.
  • Chromatic publishes are complete and README Storybook links point to the latest builds.
  • npm authentication is confirmed (npm whoami).
  • Publish commands run in strict dependency order (core first).
  • npm --prefix testing run verify:npm passes using published versions.
  • npm --prefix testing run verify:npm:browser passes against published versions.

Phase 1: Release Checklist (Non-Technical First)

Use this section for design leads, product, and engineering leads before commands are run.

  1. Confirm release scope:
    • Which components changed, what behavior changed, and which frameworks are impacted.
  2. Confirm visual sign-off:
    • Review Storybook/Chromatic for React, Angular 18-20, and Angular 21+.
  3. Confirm compatibility sign-off:
    • React track (@accretion_ui/react) and Angular tracks (@accretion_ui/angular_18, @accretion_ui/angular_21) still match the support matrix.
  4. Confirm rollout communication:
    • Decide release notes and internal/external announcement timing.
  5. Confirm go/no-go:
    • Release only after smoke checks pass and npm publish credentials are available.

Phase 2: Engineering Runbook

Run all commands from repo root unless noted otherwise.

0) Preflight

# Confirm npm auth (required for publish)
npm whoami

# Optional: verify clean working tree before release
git status --short

1) Production-readiness gate

# Core package test runner
npm --prefix components/core run test

# Story-level behavior + prop + accessibility coverage
npm --prefix chromatic run test:accordion

# Full local artifact + consumer smoke matrix (React Vite/CRA/Next + Angular 18/21)
npm --prefix testing run verify:local

2) Update package versions

Choose patch, minor, or major based on the release scope.

npm --prefix components/core version patch --no-git-tag-version
npm --prefix components/react version patch --no-git-tag-version
npm --prefix components/angular_18 version patch --no-git-tag-version
npm --prefix components/angular_21 version patch --no-git-tag-version

After bumping core, align wrapper references to the new core version:

CORE_VERSION="$(node -p "require('./components/core/package.json').version")"
npm --prefix components/react pkg set "dependencies.@accretion_ui/core=^${CORE_VERSION}"
npm --prefix components/angular_18 pkg set "peerDependencies.@accretion_ui/core=^${CORE_VERSION}"
npm --prefix components/angular_21 pkg set "peerDependencies.@accretion_ui/core=^${CORE_VERSION}"
npm --prefix components/react install --package-lock-only
npm --prefix components/angular_18 install --package-lock-only
npm --prefix components/angular_21 install --package-lock-only

3) Build production artifacts

npm --prefix components/core run build
npm --prefix components/react run build
npm --prefix components/angular_18 run build
npm --prefix components/angular_21 run build

4) Publish to npm (strict order)

npm --prefix components/core run publish:package
npm --prefix components/react run publish:package
npm --prefix components/angular_18 run publish:package
npm --prefix components/angular_21 run publish:package

5) Post-publish validation

CORE_VERSION="$(node -p "require('./components/core/package.json').version")"
REACT_VERSION="$(node -p "require('./components/react/package.json').version")"
ANGULAR_18_VERSION="$(node -p "require('./components/angular_18/package.json').version")"
ANGULAR_21_VERSION="$(node -p "require('./components/angular_21/package.json').version")"

npm view @accretion_ui/core version
npm view @accretion_ui/react version
npm view @accretion_ui/angular_18 version
npm view @accretion_ui/angular_21 version

ACCRETION_CORE_VERSION="$CORE_VERSION" \
ACCRETION_REACT_VERSION="$REACT_VERSION" \
ACCRETION_ANGULAR_18_VERSION="$ANGULAR_18_VERSION" \
ACCRETION_ANGULAR_21_VERSION="$ANGULAR_21_VERSION" \
npm --prefix testing run verify:npm

ACCRETION_CORE_VERSION="$CORE_VERSION" \
ACCRETION_REACT_VERSION="$REACT_VERSION" \
ACCRETION_ANGULAR_18_VERSION="$ANGULAR_18_VERSION" \
ACCRETION_ANGULAR_21_VERSION="$ANGULAR_21_VERSION" \
npm --prefix testing run verify:npm:browser

Release guardrails:

  • Publish @accretion_ui/core first.
  • Publish wrapper packages only after core publish succeeds.
  • Never republish an existing version number.
  • If publish fails mid-sequence, fix the issue and resume from the first unpublished package.

Publish Command Reference

Run from repo root in this exact order:

npm --prefix components/core run publish:package
npm --prefix components/react run publish:package
npm --prefix components/angular_18 run publish:package
npm --prefix components/angular_21 run publish:package

Recommended post-publish verification:

CORE_VERSION="$(node -p "require('./components/core/package.json').version")"
REACT_VERSION="$(node -p "require('./components/react/package.json').version")"
ANGULAR_18_VERSION="$(node -p "require('./components/angular_18/package.json').version")"
ANGULAR_21_VERSION="$(node -p "require('./components/angular_21/package.json').version")"

ACCRETION_CORE_VERSION="$CORE_VERSION" \
ACCRETION_REACT_VERSION="$REACT_VERSION" \
ACCRETION_ANGULAR_18_VERSION="$ANGULAR_18_VERSION" \
ACCRETION_ANGULAR_21_VERSION="$ANGULAR_21_VERSION" \
npm --prefix testing run verify:npm

ACCRETION_CORE_VERSION="$CORE_VERSION" \
ACCRETION_REACT_VERSION="$REACT_VERSION" \
ACCRETION_ANGULAR_18_VERSION="$ANGULAR_18_VERSION" \
ACCRETION_ANGULAR_21_VERSION="$ANGULAR_21_VERSION" \
npm --prefix testing run verify:npm:browser

Known Bugs

  • Server-rendered application support still has caveats:
    • Import the package predefine.css file globally in SSR/SSG apps to avoid a first-paint flash while custom elements are still unresolved.
    • The runtime now injects the same stylesheet once per document as a fallback, but runtime injection alone cannot eliminate flash before the first HTML/CSS paint.
    • The library is still client-first because behavior is finalized during client-side custom-element upgrade, not during server render.
    • In SSR + hydration workflows (for example, Next.js App Router and Angular Universal), web-component upgrade timing can still produce hydration warnings or behavior differences for nested components.
    • If strict SSR hydration parity is required, use framework-level client-only rendering or hydration opt-out for affected component regions until full SSR compatibility is formally released.

Contributing

Branch strategy

  • Branch from main.
  • Use codex/<feature_name> naming.
  • Keep change sets scoped (core logic, wrappers, stories, docs, testing).

Typical change flow

  1. Update component logic/styles in components/core.
  2. Build wrappers (components/react, components/angular_18, components/angular_21).
  3. Run smoke tests in testing/ (verify:local, then verify:npm + verify:npm:browser after publish).
  4. Update affected stories in chromatic/*.
  5. Publish Chromatic and verify links.
  6. Update README if behavior, support ranges, or workflows changed.

Repository and Branch Map

Main development repository

Active branches

Package and Chromatic Repositories

Package repositories

Chromatic repositories

Project Layout

  • components/core: Stencil core (@accretion_ui/core)
  • components/react: React wrapper (@accretion_ui/react)
  • components/angular_18: Angular 18-20 wrapper (@accretion_ui/angular_18)
  • components/angular_21: Angular 21 wrapper (@accretion_ui/angular_21)
  • chromatic: Storybook + Chromatic projects for all wrappers
  • testing: generated smoke-test harness for local tarballs and npm-installed packages
  • .tmp: temporary generated apps used by smoke testing (gitignored)

Coming Soon (TODO)

  • Add per-component Figma references with direct token mapping links.
  • Move proof-of-concept Button styles to finalized Figma-backed token definitions.
  • Expand component coverage beyond Button while preserving one-core/multi-wrapper generation.
  • Add documented accessibility expectations and test status per component.

About

Accretion UI is a cross-framework component system built from one core web component library and distributed to multiple framework ecosystems.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors