Converting a Drupal 11 Status Message into a Single Directory Component (SDC) in Drupal
1. Message Component Structure
modules/custom/uswds_messages/components/status-message/ ├── status-message.component.yml ├── status-message.twig ├── status-message.css ├── status-message.js └── README.md
2. Component Definition
status-message.component.yml
name: 'Status Message' description: 'USWDS-styled dismissible status message' status: 'stable' props: type: type: string label: 'Message Type' description: 'Type of message (status, warning, error, info)' required: true default: 'status' enum: - status - warning - error - info message: type: string label: 'Message Content' description: 'The message text to display' required: true heading: type: string label: 'Message Heading' description: 'Optional heading for the message' dismissible: type: boolean label: 'Dismissible' description: 'Whether the message can be dismissed' default: true id: type: string label: 'Message ID' description: 'Unique identifier for the message element' classes: type: string label: 'Additional CSS classes' description: 'Extra classes to add to the message container' slots: actions: title: 'Actions' description: 'Additional action buttons or links' library: css: component: - dist/css/status-message.css: {} js: component: - dist/js/status-message.js: {}
3. Twig Template
status-message.twig
{# /** * @file * Component for USWDS-styled dismissible status messages. * * Available variables: * - type: The message type (status, warning, error, info). * - message: The message text content. * - heading: Optional heading for the message. * - dismissible: Whether the message can be dismissed. * - id: Unique identifier for the message. * - classes: Additional CSS classes. * - attributes: Additional HTML attributes. */ #} {% set type_classes = { 'status': 'usa-alert--success', 'warning': 'usa-alert--warning', 'error': 'usa-alert--error', 'info': 'usa-alert--info' } %} {% set icon_map = { 'status': 'check_circle', 'warning': 'warning', 'error': 'error', 'info': 'info' } %} {% set role_map = { 'status': 'status', 'warning': 'alert', 'error': 'alert', 'info': 'status' } %} {% set default_id = 'message-' ~ random() %} {% set message_id = id|default(default_id) %} {% set role = role_map[type]|default('status') %} {% set alert_class = type_classes[type]|default('usa-alert--info') %} {% set icon = icon_map[type]|default('info') %} <div id="{{ message_id }}" class="usa-alert {{ alert_class }} {{ classes|default('') }}" role="{{ role }}" data-message-type="{{ type }}" {% if dismissible %}data-dismissible="true"{% endif %} {{ attributes|default('') }} > <div class="usa-alert__body"> {% if dismissible %} <button class="usa-alert__close" aria-label="Close this message" data-close-message="{{ message_id }}" type="button" > <span class="usa-icon"> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img"> <use xlink:href="/themes/custom/your_theme/dist/img/sprite.svg#close"></use> </svg> </span> </button> {% endif %} <div class="usa-alert__content"> {% if heading %} <h3 class="usa-alert__heading">{{ heading }}</h3> {% endif %} <div class="usa-alert__text"> {{ message|raw }} </div> {% if slots.actions %} <div class="usa-alert__actions"> {{ slots.actions }} </div> {% endif %} </div> </div> </div>
4. CSS Styling
status-message.css
/** * @file * USWDS Alert Component - Drupal Integration */ .usa-alert { position: relative; margin-bottom: 1rem; border-radius: 0.25rem; border-width: 1px; border-style: solid; padding: 1rem; background-color: #f0f0f0; transition: opacity 0.3s ease, transform 0.3s ease; } /* Base alert colors */ .usa-alert--success { border-color: #00a91c; background-color: #ecf3ec; } .usa-alert--success .usa-alert__body::before { background-color: #00a91c; } .usa-alert--warning { border-color: #ffbe2e; background-color: #faf3d1; } .usa-alert--warning .usa-alert__body::before { background-color: #ffbe2e; } .usa-alert--error { border-color: #d54309; background-color: #f4e3db; } .usa-alert--error .usa-alert__body::before { background-color: #d54309; } .usa-alert--info { border-color: #00bde3; background-color: #e7f6f8; } .usa-alert--info .usa-alert__body::before { background-color: #00bde3; } /* Alert body */ .usa-alert__body { position: relative; padding-left: 2.5rem; } .usa-alert__body::before { content: ''; position: absolute; top: 0; left: 0; width: 0.5rem; height: 100%; border-radius: 2px 0 0 2px; } /* Close button */ .usa-alert__close { position: absolute; top: 0.5rem; right: 0.5rem; background: transparent; border: 0; padding: 0.5rem; cursor: pointer; color: #565c65; transition: color 0.2s ease; } .usa-alert__close:hover, .usa-alert__close:focus { color: #1b1b1b; outline: 2px solid #005ea2; outline-offset: 2px; } .usa-alert__close .usa-icon { width: 1.5rem; height: 1.5rem; } /* Content */ .usa-alert__content { padding-right: 2.5rem; } .usa-alert__heading { margin-top: 0; margin-bottom: 0.5rem; font-size: 1.25rem; font-weight: 700; line-height: 1.2; } .usa-alert__text { margin-bottom: 0.5rem; } .usa-alert__text p:last-child { margin-bottom: 0; } .usa-alert__text a { font-weight: 700; text-decoration: underline; } .usa-alert__text a:hover { text-decoration: none; } /* Actions */ .usa-alert__actions { margin-top: 1rem; } .usa-alert__actions .usa-button { margin-right: 0.5rem; margin-bottom: 0.5rem; } /* Animation for dismiss */ .usa-alert--dismissing { opacity: 0; transform: translateX(-20px); } /* RTL support */ [dir="rtl"] .usa-alert__body { padding-left: 0; padding-right: 2.5rem; } [dir="rtl"] .usa-alert__body::before { left: auto; right: 0; } [dir="rtl"] .usa-alert__close { right: auto; left: 0.5rem; } [dir="rtl"] .usa-alert__content { padding-right: 0; padding-left: 2.5rem; } /* Focus styles */ .usa-alert:focus-within { outline: 2px solid #005ea2; outline-offset: 2px; } /* High contrast mode */ @media (forced-colors: active) { .usa-alert { border: 1px solid ButtonText; } .usa-alert__close { border: 1px solid ButtonText; } }
5. JavaScript for Dismiss Functionality
status-message.js
/** * @file * Dismissible status message component. */ (function (Drupal, once) { 'use strict'; /** * Initialize dismissible messages. */ function initMessages(element) { const messages = element.querySelectorAll('[data-dismissible="true"]'); messages.forEach(message => { const closeButton = message.querySelector('.usa-alert__close'); const messageId = message.id; if (closeButton) { // Check localStorage for dismissed state const storageKey = `dismissed-message-${messageId}`; if (localStorage.getItem(storageKey)) { message.style.display = 'none'; return; } // Add click handler closeButton.addEventListener('click', function (e) { e.preventDefault(); dismissMessage(message, messageId, storageKey); }); // Add keyboard support closeButton.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); dismissMessage(message, messageId, storageKey); } }); } }); } /** * Dismiss a message with animation. */ function dismissMessage(message, messageId, storageKey) { // Add dismissing class for animation message.classList.add('usa-alert--dismissing'); // Store dismissal in localStorage try { localStorage.setItem(storageKey, 'true'); } catch (e) { console.warn('Unable to save dismissal to localStorage:', e); } // Remove message after animation setTimeout(() => { message.style.display = 'none'; message.classList.remove('usa-alert--dismissing'); // Dispatch custom event for other scripts to listen to const event = new CustomEvent('uswds:message:dismissed', { detail: { messageId: messageId, message: message } }); document.dispatchEvent(event); // Move focus if needed manageFocusAfterDismissal(message); }, 300); } /** * Manage focus after message dismissal. */ function manageFocusAfterDismissal(message) { const focusableElements = document.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); // Try to find next focusable element for (let i = 0; i < focusableElements.length; i++) { if (focusableElements[i] === message.querySelector('.usa-alert__close')) { const nextElement = focusableElements[i + 1]; if (nextElement) { nextElement.focus(); } else if (focusableElements[0]) { focusableElements[0].focus(); } break; } } } /** * Initialize all messages on page load. */ Drupal.behaviors.uswdsStatusMessage = { attach: function (context) { once('uswds-status-message', '[data-dismissible="true"]', context).forEach(initMessages); } }; /** * Public API for programmatic control. */ Drupal.uswdsMessages = { /** * Dismiss a specific message by ID. */ dismiss: function(messageId) { const message = document.getElementById(messageId); if (message && message.dataset.dismissible === 'true') { const storageKey = `dismissed-message-${messageId}`; dismissMessage(message, messageId, storageKey); } }, /** * Show a previously dismissed message. */ show: function(messageId) { const message = document.getElementById(messageId); const storageKey = `dismissed-message-${messageId}`; if (message) { message.style.display = ''; try { localStorage.removeItem(storageKey); } catch (e) { console.warn('Unable to remove from localStorage:', e); } } }, /** * Clear all message dismissals. */ clearDismissals: function() { Object.keys(localStorage).forEach(key => { if (key.startsWith('dismissed-message-')) { try { localStorage.removeItem(key); } catch (e) { console.warn('Unable to clear localStorage key:', key, e); } } }); } }; })(Drupal, once);
6. README File
README.md
# USWDS Status Message Component A Single Directory Component for Drupal 11 that provides USWDS-styled dismissible status messages. ## Installation 1. Copy the `status-message` directory to your theme or module's `components` folder 2. Clear Drupal cache ## Usage ### In Twig templates: ```twig {% embed 'uswds_messages:status-message' with { type: 'success', message: 'Your action was completed successfully!', heading: 'Success', dismissible: true, id: 'unique-message-id', classes: 'custom-additional-class' } %} {% block actions %} <a href="/some-action" class="usa-button usa-button--secondary">Learn More</a> {% endblock %} {% endembed %}
In PHP:
$build['message'] = [ '#type' => 'component', '#component' => 'uswds_messages:status-message', '#props' => [ 'type' => 'warning', 'message' => 'This is a warning message.', 'heading' => 'Heads up!', 'dismissible' => TRUE, ], ];
With Drupal messages:
// Convert Drupal messages to USWDS alerts function your_theme_preprocess_status_messages(&$variables) { foreach ($variables['message_list'] as $type => $messages) { foreach ($messages as $key => $message) { $variables['message_list'][$type][$key] = [ '#type' => 'component', '#component' => 'uswds_messages:status-message', '#props' => [ 'type' => $type, 'message' => $message, 'dismissible' => TRUE, ], ]; } } }
JavaScript API
// Dismiss a message programmatically Drupal.uswdsMessages.dismiss('message-id'); // Show a dismissed message Drupal.uswdsMessages.show('message-id'); // Clear all dismissals Drupal.uswdsMessages.clearDismissals(); // Listen for dismissal events document.addEventListener('uswds:message:dismissed', function(e) { console.log('Message dismissed:', e.detail.messageId); });
Props
Prop | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | 'status' | Message type: 'status', 'warning', 'error', 'info' |
message | string | Yes | - | The message content |
heading | string | No | - | Optional message heading |
dismissible | boolean | No | true | Whether message can be dismissed |
id | string | No | auto-generated | Unique message identifier |
classes | string | No | - | Additional CSS classes |
Features
✅ USWDS styling and accessibility
✅ Dismissible with animation
✅ Persistent dismissal (localStorage)
✅ Keyboard navigation support
✅ Screen reader announcements
✅ RTL language support
✅ High contrast mode support
✅ Programmatic control via JavaScript API
✅ Custom event dispatch on dismissal
## 7. Integration with Drupal Messages
To automatically convert Drupal messages to this component, add this to your theme:
**your_theme.theme**
```php
<?php
/**
* Implements hook_preprocess_status_messages().
*/
function your_theme_preprocess_status_messages(&$variables) {
// Ensure the component library is attached.
$variables['#attached']['library'][] = 'uswds_messages/status-message';
// Check if SDC is available.
if (\Drupal::service('extension.list.module')->exists('sdc')) {
$converted_messages = [];
foreach ($variables['message_list'] as $type => $messages) {
foreach ($messages as $key => $message) {
$converted_messages[$type][$key] = [
'#type' => 'component',
'#component' => 'uswds_messages:status-message',
'#props' => [
'type' => $type,
'message' => $message,
'dismissible' => TRUE,
'id' => 'drupal-message-' . $type . '-' . $key,
],
];
}
}
$variables['message_list'] = $converted_messages;
}
}Key Features:
USWDS Compliance: Uses proper USWDS classes and structure
Dismissible: Close button with animation
Persistent: Remembers dismissed messages via localStorage
Accessible: Proper ARIA roles, keyboard navigation, focus management
Customizable: Multiple types, optional heading, additional actions slot
Programmatic Control: JavaScript API for external control
Drupal Integration: Works with Drupal's message system
The component is fully self-contained and can be used anywhere in your Drupal site - in templates,
Recent content
-
2 hours 28 minutes ago
-
1 week 1 day ago
-
1 week 1 day ago
-
1 week 1 day ago
-
1 week 1 day ago
-
1 week 6 days ago
-
2 weeks 1 day ago
-
2 weeks 1 day ago
-
2 weeks 1 day ago
-
2 weeks 2 days ago