Skip to content

Web components : Add usa-alert component#271

Open
ericsorenson wants to merge 8 commits into
uswds:developfrom
ericsorenson:develop
Open

Web components : Add usa-alert component#271
ericsorenson wants to merge 8 commits into
uswds:developfrom
ericsorenson:develop

Conversation

@ericsorenson
Copy link
Copy Markdown
Contributor

@ericsorenson ericsorenson commented May 18, 2026

Summary

New alert component. Added usa-alert with support for info, warning, error, success, and emergency statuses, including slim, no-icon, and closeable variants.

Related issue

N/A

Preview link

N/A

Problem statement

The library currently has one real component (usa-banner). The alert pattern is one of the most commonly used USWDS components and its absence blocks adoption. Additionally, the architecture needed validation from a second non-trivial component to confirm that patterns around slots, CSS custom properties, variant attributes, and event dispatch work correctly.
Solution

Ported the VADS va-alert (Stencil) component to Lit as usa-alert, using USWDS core's alert pattern as the visual/behavioral reference. Key design decisions:

Reactive rendering only — no imperative DOM manipulation (avoids the anti-pattern present in the original banner)
Context-appropriate ARIA roles — role="alert" for error/warning/emergency, role="status" for info/success
CSS custom properties follow --usa-alert-* naming convention
Status icons via CSS masks (no SVG imports per-status, keeps bundle small)
Closeable via close event — consumers control focus management and element lifecycle
2 kB brotli — well within 3 kB budget

Major changes

src/components/usa-alert/index.ts — LitElement component class
src/components/usa-alert/usa-alert.css — Styles with 5 status variants, slim, no-icon, forced-colors support
src/components/usa-alert/usa-alert.spec.ts — 24 unit tests
src/components/usa-alert/usa-alert.stories.ts — 9 Storybook stories
config/vite.config.ts — Entry point with 3 kB size budget
src/components/index.ts — Export added
custom-elements.json — Regenerated manifest (auto-generated)

Testing and review

npm run test:ci — 46 tests pass (24 for usa-alert)
npm run build — succeeds, all bundle size checks pass, framework wrappers generated
Recommend reviewing in Storybook (npm start) to verify visual appearance of all variants
Feedback welcome on: component API surface, CSS custom property naming, status-to-role mapping

ericsorenson and others added 4 commits May 14, 2026 09:10
Move "sass" from dependencies to devDependencies in package.json to reflect it's a build-time tool. Update UsaBanner to use Lit's boolean attribute binding (?hidden) driven by isOpen and remove the manual DOM attribute toggle in toggle(). Correct spacing token "05" from .025rem to .25rem in tokens/dimension/spacing.json.
Introduce a new UsaAlert web component (LitElement) with status variants (info, warning, error, success, emergency), slim, no-icon, and closeable options. Ship styles, Storybook stories, and unit tests; dispatches a close event and uses role=alert/status for accessibility. Also register/export the component, add a Vite build entry with size limit, and update custom-elements.json and a changeset.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 18, 2026

⚠️ No Changeset found

Latest commit: be7808f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@ericsorenson ericsorenson marked this pull request as ready for review May 18, 2026 18:28
statuses.forEach((status) => {
it(`renders ${status} variant`, async () => {
document.body.innerHTML = `<usa-alert status="${status}"><p>Test</p></usa-alert>`;
await getAlert().updateComplete;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you have helpers at the top of this file, you might create a helper for waiting for the update cycle to complete, something like:

async function updateComplete(): Promise<void> {
  await getAlert().updateComplete;
}

This would allow you to easily decouple the test from proprietary lit syntax, if that were to be needed in the future. I think you could even make this framework agnostic with:

async function updateComplete(): Promise<void> {
  await new Promise<void>((resolve) => requestAnimationFrame(resolve));
}

And then where you call await getAlert().updateComplete; in the tests, replace it with await updateComplete().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated: extracted an updateComplete() helper

Comment thread src/components/usa-alert/index.ts Outdated
this._visible = true;
}

private get _role(): string {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move to get #role(): string { … } for true privacy? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements

Same for this._visible --> this.#visible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated: moved _role to get #role() for true privacy.

For _visible: this one needs to stay as _visible because it's registered in static properties with { state: true }. Lit's reactive property system needs to intercept get/set on the field to trigger re-renders, and true private fields (#) are invisible to that mechanism. The _ prefix convention is Lit's standard pattern for internal reactive state.

Comment thread src/components/usa-alert/usa-alert.css Outdated
/** Slim */
.usa-alert--slim .usa-alert__body {
padding: 0.5rem var(--usa-alert-padding-x);
padding-left: calc(var(--usa-alert-padding-x) + 1.5rem + 0.5rem);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not + 2rem?

Copy link
Copy Markdown
Contributor Author

@ericsorenson ericsorenson May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, updated! The two values represent icon-size + gap semantically, but pre-computed is simpler since they're both constants.

Comment thread src/components/usa-alert/index.ts Outdated
aria-label="${this.closeLabel}"
@click="${this._handleClose}"
>
<span class="usa-alert__close-icon" aria-hidden="true"></span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might verify whether aria-hidden="true" is necessary on an empty span.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, updated!

--usa-alert-emergency-icon: #fff;

display: block;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should all of the below be within the :host { … }?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, the styles below :host target shadow DOM internals (.usa-alert, .usa-alert__body, .usa-alert__close, etc.) rather than the host element itself. In shadow DOM CSS, these selectors work at the top level of the stylesheet since they're already scoped to this component's shadow root. Only the custom property declarations and display: block belong in :host because they define the host element's own behavior and its theming API surface.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right you are. I was thinking there was a risk of it leaking out without nesting it within host, but as you say it all gets scoped into a particular component's shadow-root > adopted-style-sheets. And nesting under host actually bloats each selector by prefixing host on it. Good to know, thanks!

static properties = {
status: { type: String, reflect: true },
slim: { type: Boolean, reflect: true },
noIcon: { type: Boolean, attribute: "no-icon", reflect: true },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a combo of noIcon and each of the statuses, or is the noIcon state actually one of the statuses? For example, are the alerts without icons always in info status? If so, I would consider creating a new status that is the no icon appearance, and ditch the noIcon property. If you do need the combination of any status with or without an icon, what you have works.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a combo, USWDS core documents no-icon as an independent modifier applicable to any status.

this.slim = false;
this.noIcon = false;
this.closeable = false;
this.closeLabel = "Close alert";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this integrate msg(…) https://lit.dev/docs/localization/overview/ ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought! The existing usa-banner also doesn't use @lit/localize, it uses a manual translations object with typed strings. Adding @lit/localize as a dependency for a single default string feels premature at this stage, but I'm open to it if the team wants to establish that pattern now. Worth noting that close-label is already a configurable attribute, so consumers can pass in whatever localized string they need from their own i18n system. Happy to follow whatever direction is decided for the library's localization strategy.

Introduce an async updateComplete() helper that awaits getAlert().updateComplete in src/components/usa-alert/usa-alert.spec.ts, and replace repeated await getAlert().updateComplete calls with await updateComplete() to reduce duplication and improve test readability/maintainability.
Update usa-alert.css to replace calc(var(--usa-alert-padding-x) + 1.5rem + 0.5rem) with calc(var(--usa-alert-padding-x) + 2rem) for .usa-alert--slim .usa-alert__body. This simplifies the expression by combining the two fixed offsets into a single value, keeping the computed spacing unchanged and improving readability.
Switch the private role getter from `_role` to the ECMAScript private accessor `#role` and update the template reference accordingly. Also remove the `aria-hidden="true"` attribute from the close-icon span so the icon is not explicitly hidden; the close button still uses an `aria-label` for its accessible name.
@annepetersen
Copy link
Copy Markdown
Contributor

annepetersen commented May 19, 2026

Localization-strategy-wise, let's ping @ethangardner and/or @heymatthenry to chat with me to determine a call on that. May take us a minute to reply, but we'll put it on the list. Thanks for this work, both of you!

Comment thread .changeset/add-usa-alert-component.md Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants