Sorry, you need to enable JavaScript to visit this website.
Skip to main content
Welcome to our website! Explore our services and portfolio.

Converting a Drupal 11 Status Message into a Single Directory Component (SDC) in Drupal

Submitted by admin on

1. Message Component Structure

text
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

yaml
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

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

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

javascript
/**
 * @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

markdown
# 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:

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:

php
// 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

javascript
// 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

text
## 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:

  1. USWDS Compliance: Uses proper USWDS classes and structure

  2. Dismissible: Close button with animation

  3. Persistent: Remembers dismissed messages via localStorage

  4. Accessible: Proper ARIA roles, keyboard navigation, focus management

  5. Customizable: Multiple types, optional heading, additional actions slot

  6. Programmatic Control: JavaScript API for external control

  7. 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,