Skip to content

feat!: Refactor Alert component for composability#3497

Open
brandonlenz wants to merge 1 commit into
mainfrom
bl-alert-refactor
Open

feat!: Refactor Alert component for composability#3497
brandonlenz wants to merge 1 commit into
mainfrom
bl-alert-refactor

Conversation

@brandonlenz

@brandonlenz brandonlenz commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • AlertHeading and AlertText are now composable components
  • Adds emergency alert type
  • cta (Call to action) customization removed since it is not part of base USWDS. The storybook example has been preserved for assistance with migration
  • A className can now be passed to the Alert body
  • Storybook examples have been updated to more closely match USWDS
  • Storybook now allows interactivity for this component via controls

Related Issues or PRs

closes: #3244
closes: #3496 (supersedes)

Migration guide

The Alert component moved from a prop-driven API to a compound component API. Instead of describing an alert's contents through props (heading, headingLevel, cta) and relying on Alert to wrap your text, you now compose the alert's contents from Alert, AlertHeading, and AlertText.

This unlocks flexible composition: you control the structure and ordering of an alert's contents, and you can pass className/standard DOM attributes to each part.

What changed at a glance

Before (props on <Alert>) After
heading="..." <AlertHeading level="...">...</AlertHeading> child
headingLevel="h4" level prop on <AlertHeading>
Bare children text Wrap text in <AlertText>...</AlertText> to get usa-alert__text
cta={...} Compose freely as children and style it yourself (see below)

New, non-breaking additions you may want to use: a new emergency value for type, a new bodyClassName prop on <Alert>, and the new AlertHeading / AlertText exports (with AlertHeadingProps / AlertTextProps).

Breaking changes

heading and headingLevel have been removed

Alert no longer renders a heading for you. Render an <AlertHeading> child instead, and pass the heading level via its required level prop.

// Before
<Alert type="success" heading="Success status" headingLevel="h4">
  // ...
</Alert>

// After
<Alert type="success">
  <AlertHeading level="h4">Success status</AlertHeading>
  // ...
</Alert>

If your alert previously had no heading, you no longer pass headingLevel at all (it used to be required even without a heading):

// Before
<Alert type="info" headingLevel="h4" slim>
  // ...
</Alert>

// After
<Alert type="info" slim>
  // ...
</Alert>

Body text is no longer auto-wrapped

Previously, children was automatically wrapped it in a <p className="usa-alert__text"> unless using the validation property. Now Alert renders its children directly inside usa-alert__body with no wrapping, which enables non-text children to be used more intuitively. To get the USWDS text paragraph, use <AlertText>:

// Before
// - String was wrapped in <p class="usa-alert__text"> for you
// - Confusingly, ReactNodes (which might not be valid children of paragraph elements) were also wrapped in the paragraph tag
<Alert type="info" headingLevel="h4">Heads up.</Alert>

// After — wrap it yourself
<Alert type="info">
  <AlertText>Heads up.</AlertText>
</Alert>

A bare string child will still render, but it will not have usa-alert__text styling unless you wrap it in <AlertText>.

cta

The cta prop has been removed. A call-to-action is not part of base USWDS. It was a project-specific customization that lived in React USWDS as a one-off. Instead of inroducing an opinionated approach to implementing a CTA, we now offer the flexibility to compose elements however your project requires.

If you were using the cta prop, you can still build exactly the same UI as before by composing it as a child, and you now have full control over its layout and styling:

// Before
<Alert
  type="warning"
  heading="Warning status"
  headingLevel="h4"
  cta={
    <Button type="button" outline>
      Click here
    </Button>
  }>
  Please review your submission.
</Alert>

// After
// - Inline styles used for example purposes. 
// - Add custom styles in accordance with your project's styling framework
<Alert type="warning">
  <div
    style={{
      display: 'flex',
      justifyContent: 'space-between',
      alignItems: 'center',
    }}>
    <div>
      <AlertHeading level="h4">Warning status</AlertHeading>
      <AlertText>Please review your submission.</AlertText>
    </div>
    <Button type="button" outline>
      Click here
    </Button>
  </div>
</Alert>

validation

validation previously changed how children were rendered (skipping the <p> wrapper so you could supply elements that produce semantically correct html). It was not intuitive without looking at the source code, that this was the purpose of this prop. That is especially true in the context of USWDS deprecating the Validation component.

Now, validation only toggles the usa-alert--validation modifier class; the inner markup is entirely yours to compose:

// Before
<Alert type="info" heading="Code requirements" headingLevel="h4" validation>
  <ul>
    <li>Use at least one uppercase character</li>
    <li>Use at least one number</li>
  </ul>
</Alert>

// After
<Alert type="info" validation>
  <AlertHeading level="h4">Code requirements</AlertHeading>
  <ul>
    <li>Use at least one uppercase character</li>
    <li>Use at least one number</li>
  </ul>
</Alert>

Resulting markup

For reference, equivalent alerts produce effectively the same USWDS markup before and after — the difference is that you now author the inner structure explicitly:

<div class="usa-alert usa-alert--success" data-testid="alert">
  <div class="usa-alert__body">
    <h4 class="usa-alert__heading">Success status</h4>
    <p class="usa-alert__text">Your changes were saved.</p>
  </div>
</div>

How To Test

  • Unit tests
  • Storybook
  • Verify on a project by pulling in this branch and undertaking the migration

Screenshots

image

Author & Maintainer checklist

  • Is this is a breaking change?
    • Yes, and I accounted for the breaking changes according to the linked documentation
    • No

- AlertHeading and AlertText are now composable components
- CTA (Call to action) customization removed since it is not part of base USWDS. The storybook example has been preserved for assistance with migration
- A className can now be passed to the Alert body
- Storybook examples have been updated to more closely match USWDS
- Storybook now allows interactivity for this component via controls
@brandonlenz brandonlenz self-assigned this Jun 9, 2026
@brandonlenz brandonlenz changed the title feat!: Major Refactor of Alert Component for composability feat!: Major refactor of Alert component for composability Jun 9, 2026
@brandonlenz brandonlenz marked this pull request as ready for review June 9, 2026 16:14
@brandonlenz brandonlenz requested review from a team as code owners June 9, 2026 16:14
@ronaktruss

Copy link
Copy Markdown
Contributor

The Alert component moved from a prop-driven API to a compound component API

Is there a need/desire for this to be more composable?

Is there a reason to deviate from the straightforward solution of just making the prop optional and checking if both the heading and heading level is present before rendering the heading? (This would be a non breaking change.) If we had other plans to make breaking changes it may be worth codifying this logic into a discriminated union such that you get actual type errors, but otherwise might feel a bit heavy handed since the headingLevel is required and it would make all instances of not having a heading error.

@brandonlenz

brandonlenz commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Is there a need/desire for this to be more composable?

When I on boarded as a maintainer, I wanted to always be very rigid about making the components produce only the markup as USWDS recommended.

But I learned through feedback and issues raised, that consumers of the library generally prefer the flexibility to compose components.

Our more-recently added components tend to follow that pattern, while our oldest components are more rigid. The more rigid ones, like the Alert implementation generate more issues.

Is there a reason to deviate from the straightforward solution of just making the prop optional and checking if both the heading and heading level is present before rendering the heading? (This would be a non breaking change.)

Yes (and you've landed on one alternative I considered below, but first): I think that compile-time checks should prevent consumers from improperly configuring components. That's a high-value developer experience add.

Allowing props to be used in such a way that

  • Alert can be misconfigured (headingLevel is present when it doesn't need to be)
  • Alert does not render a heading because both props happened to not be added

makes the component less intuitive to use, and builds on top of a component design that already wasn't working for us.

I also think we're overdue for some healthy breaking changes, I do have some bias there 😆

If we had other plans to make breaking changes it may be worth codifying this logic into a discriminated union such that you get actual type errors, but otherwise might feel a bit heavy handed since the headingLevel is required and it would make all instances of not having a heading error.

I started with this approach. It was fine. There was some storybook kludge that I had to add because CSF3 does not work well with component props that are unions. I was prepared to accept that, though.

Ultimately, since this was already a breaking change (consumers would have to remove the redundant and previously required headingLevel prop to silence the new TS errors), I figured, why not make the breaking changes that would bring the component more in line with the composable components we have, that consumers of the library don't have issues with?

@kleinschmidtj kleinschmidtj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@brandonlenz brandonlenz changed the title feat!: Major refactor of Alert component for composability feat!: Refactor Alert component for composability Jun 18, 2026
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.

[feat] Make Alert headingLevel prop optional

3 participants