CODITECT Pattern Library - Missing Molecules (Complete Specifications)
Production-ready molecule patterns
M7: TagList
Purpose: Display and manage multiple tags/badges
Test Criteria:
composition:
- atoms: multiple_badges
- atoms: close_buttons (removable)
behavior:
- wrap: flow to new line
- removable: close icon per tag
- interactive: click or keyboard delete
- max_display: show N + "X more"
accessibility:
- role: list
- aria_label: tag description
- remove_button_label: "Remove [tag name]"
Variants:
<!-- Basic Tag List -->
<div class="tag-list" role="list" data-testid="tag-list">
<span class="tag" role="listitem" data-testid="tag">
<span class="tag__text">JavaScript</span>
</span>
<span class="tag" role="listitem">
<span class="tag__text">React</span>
</span>
<span class="tag" role="listitem">
<span class="tag__text">TypeScript</span>
</span>
</div>
<!-- Removable Tags -->
<div class="tag-list tag-list--removable" role="list" data-testid="tag-list-removable">
<span class="tag tag--removable" role="listitem">
<span class="tag__text">Design</span>
<button class="tag__remove" aria-label="Remove Design tag" data-testid="tag-remove">
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</span>
<span class="tag tag--removable" role="listitem">
<span class="tag__text">Frontend</span>
<button class="tag__remove" aria-label="Remove Frontend tag">
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</span>
</div>
<!-- With Input (Add New) -->
<div class="tag-list tag-list--editable" role="list" data-testid="tag-list-editable">
<span class="tag tag--removable" role="listitem">
<span class="tag__text">Marketing</span>
<button class="tag__remove" aria-label="Remove Marketing tag">×</button>
</span>
<span class="tag tag--removable" role="listitem">
<span class="tag__text">Sales</span>
<button class="tag__remove" aria-label="Remove Sales tag">×</button>
</span>
<input
type="text"
class="tag-list__input"
placeholder="Add tag..."
aria-label="Add new tag"
/>
</div>
<!-- With Overflow -->
<div class="tag-list" role="list" data-testid="tag-list-overflow">
<span class="tag" role="listitem">
<span class="tag__text">Tag 1</span>
</span>
<span class="tag" role="listitem">
<span class="tag__text">Tag 2</span>
</span>
<span class="tag" role="listitem">
<span class="tag__text">Tag 3</span>
</span>
<button class="tag tag--more" aria-label="Show 5 more tags">
+5 more
</button>
</div>
<!-- Color Variants -->
<div class="tag-list" role="list">
<span class="tag tag--primary">Primary</span>
<span class="tag tag--success">Success</span>
<span class="tag tag--warning">Warning</span>
<span class="tag tag--error">Error</span>
<span class="tag tag--neutral">Neutral</span>
</div>
CSS:
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.tag {
display: inline-flex;
align-items: center;
gap: 6px;
height: 28px;
padding: 0 12px;
background: var(--gray-100);
color: var(--gray-700);
border-radius: 14px;
font-size: 13px;
font-weight: 500;
line-height: 1;
white-space: nowrap;
transition: background-color 0.2s;
}
.tag__text {
line-height: 1;
}
.tag__remove {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
border: none;
background: none;
color: var(--gray-600);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
margin-right: -4px;
}
.tag__remove:hover {
background: var(--gray-200);
color: var(--gray-900);
}
.tag__remove:focus-visible {
outline: 2px solid var(--blue-500);
outline-offset: 1px;
}
/* Editable list */
.tag-list--editable {
padding: 6px;
border: 1px solid var(--gray-200);
border-radius: 8px;
background: var(--white);
min-height: 40px;
}
.tag-list__input {
flex: 1;
min-width: 120px;
height: 28px;
border: none;
outline: none;
font-size: 13px;
padding: 0 8px;
}
.tag-list__input::placeholder {
color: var(--gray-400);
}
/* Color variants */
.tag--primary {
background: var(--blue-100);
color: var(--blue-700);
}
.tag--success {
background: var(--green-100);
color: var(--green-700);
}
.tag--warning {
background: var(--yellow-100);
color: var(--yellow-700);
}
.tag--error {
background: var(--red-100);
color: var(--red-700);
}
.tag--neutral {
background: var(--gray-200);
color: var(--gray-700);
}
/* More button */
.tag--more {
background: none;
border: 1px dashed var(--gray-300);
color: var(--gray-600);
cursor: pointer;
}
.tag--more:hover {
border-color: var(--gray-400);
color: var(--gray-700);
}
JavaScript:
class TagList {
constructor(container) {
this.container = container;
this.tags = [];
this.init();
}
init() {
this.container.addEventListener('click', (e) => {
const removeBtn = e.target.closest('.tag__remove');
if (removeBtn) {
this.removeTag(removeBtn);
}
});
const input = this.container.querySelector('.tag-list__input');
if (input) {
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && input.value.trim()) {
this.addTag(input.value.trim());
input.value = '';
}
});
}
}
addTag(text) {
const tag = document.createElement('span');
tag.className = 'tag tag--removable';
tag.setAttribute('role', 'listitem');
tag.innerHTML = `
<span class="tag__text">${text}</span>
<button class="tag__remove" aria-label="Remove ${text} tag">
×
</button>
`;
const input = this.container.querySelector('.tag-list__input');
if (input) {
this.container.insertBefore(tag, input);
} else {
this.container.appendChild(tag);
}
this.tags.push(text);
}
removeTag(button) {
const tag = button.closest('.tag');
const text = tag.querySelector('.tag__text').textContent;
tag.style.animation = 'fadeOut 0.2s';
setTimeout(() => {
tag.remove();
this.tags = this.tags.filter(t => t !== text);
}, 200);
}
}
@keyframes fadeOut {
to {
opacity: 0;
transform: scale(0.8);
}
}
M8: Breadcrumb
Purpose: Navigation trail showing page hierarchy
Test Criteria:
composition:
- atoms: links
- atoms: separators (chevrons)
behavior:
- clickable: links to parent pages
- current_page: not clickable
- overflow: collapse middle items if > 4
accessibility:
- role: navigation
- aria_label: "Breadcrumb"
- aria_current: "page" on last item
Variants:
<!-- Basic Breadcrumb -->
<nav class="breadcrumb" aria-label="Breadcrumb" data-testid="breadcrumb">
<ol class="breadcrumb__list">
<li class="breadcrumb__item">
<a href="/" class="breadcrumb__link">Home</a>
</li>
<li class="breadcrumb__separator" aria-hidden="true">›</li>
<li class="breadcrumb__item">
<a href="/projects" class="breadcrumb__link">Projects</a>
</li>
<li class="breadcrumb__separator" aria-hidden="true">›</li>
<li class="breadcrumb__item">
<span class="breadcrumb__current" aria-current="page">Website Redesign</span>
</li>
</ol>
</nav>
<!-- With Icons -->
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol class="breadcrumb__list">
<li class="breadcrumb__item">
<a href="/" class="breadcrumb__link">
<svg class="breadcrumb__icon" width="14" height="14">
<path d="M3 12h18M12 3l9 9-9-9-9 9 9-9z" stroke="currentColor"/>
</svg>
Home
</a>
</li>
<li class="breadcrumb__separator" aria-hidden="true">›</li>
<li class="breadcrumb__item">
<span class="breadcrumb__current" aria-current="page">Settings</span>
</li>
</ol>
</nav>
<!-- With Overflow (collapsed middle) -->
<nav class="breadcrumb" aria-label="Breadcrumb" data-testid="breadcrumb-overflow">
<ol class="breadcrumb__list">
<li class="breadcrumb__item">
<a href="/" class="breadcrumb__link">Home</a>
</li>
<li class="breadcrumb__separator" aria-hidden="true">›</li>
<li class="breadcrumb__item">
<button class="breadcrumb__expand" aria-label="Show hidden pages">
...
</button>
</li>
<li class="breadcrumb__separator" aria-hidden="true">›</li>
<li class="breadcrumb__item">
<a href="/projects/web" class="breadcrumb__link">Web Team</a>
</li>
<li class="breadcrumb__separator" aria-hidden="true">›</li>
<li class="breadcrumb__item">
<span class="breadcrumb__current" aria-current="page">Tasks</span>
</li>
</ol>
</nav>
CSS:
.breadcrumb {
padding: 16px 0;
font-size: 14px;
}
.breadcrumb__list {
display: flex;
align-items: center;
gap: 8px;
list-style: none;
margin: 0;
padding: 0;
flex-wrap: wrap;
}
.breadcrumb__item {
display: flex;
align-items: center;
}
.breadcrumb__link {
color: var(--blue-500);
text-decoration: none;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
border-radius: 4px;
transition: background-color 0.2s;
}
.breadcrumb__link:hover {
background: var(--gray-100);
text-decoration: underline;
}
.breadcrumb__link:focus-visible {
outline: 2px solid var(--blue-500);
outline-offset: 2px;
}
.breadcrumb__current {
color: var(--gray-700);
padding: 4px 6px;
}
.breadcrumb__separator {
color: var(--gray-400);
user-select: none;
}
.breadcrumb__icon {
flex-shrink: 0;
}
.breadcrumb__expand {
padding: 4px 8px;
border: none;
background: none;
color: var(--gray-600);
border-radius: 4px;
cursor: pointer;
font-weight: 600;
letter-spacing: 0.5px;
}
.breadcrumb__expand:hover {
background: var(--gray-100);
}
/* Mobile responsive */
@media (max-width: 640px) {
.breadcrumb__list {
font-size: 13px;
}
/* Hide all but last 2 items */
.breadcrumb__item:not(:nth-last-child(-n+3)):not(:first-child) {
display: none;
}
.breadcrumb__separator:not(:nth-last-child(-n+2)):not(:first-child) {
display: none;
}
}
M9: Pagination
Purpose: Navigate through pages of content
Test Criteria:
composition:
- atoms: buttons (prev, next, numbers)
- atoms: text (page info)
behavior:
- current_page: highlighted
- disabled_state: first/last page boundaries
- keyboard: arrow keys navigate
- truncation: show ... for many pages
accessibility:
- role: navigation
- aria_label: "Pagination"
- aria_current: "page" on current
- aria_disabled: on disabled buttons
Variants:
<!-- Standard Pagination -->
<nav class="pagination" aria-label="Pagination" data-testid="pagination">
<button class="pagination__button" disabled aria-label="Previous page">
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M10 4l-4 4 4 4" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
Previous
</button>
<ol class="pagination__list">
<li>
<button class="pagination__page pagination__page--current" aria-current="page" aria-label="Page 1, current page">
1
</button>
</li>
<li>
<button class="pagination__page" aria-label="Go to page 2">
2
</button>
</li>
<li>
<button class="pagination__page" aria-label="Go to page 3">
3
</button>
</li>
<li>
<span class="pagination__ellipsis" aria-hidden="true">...</span>
</li>
<li>
<button class="pagination__page" aria-label="Go to page 10">
10
</button>
</li>
</ol>
<button class="pagination__button" aria-label="Next page">
Next
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
</button>
</nav>
<!-- Compact Pagination (Mobile) -->
<nav class="pagination pagination--compact" aria-label="Pagination" data-testid="pagination-compact">
<button class="pagination__button" aria-label="Previous page">
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M10 4l-4 4 4 4" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
</button>
<span class="pagination__info">
Page <strong>2</strong> of <strong>10</strong>
</span>
<button class="pagination__button" aria-label="Next page">
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
</button>
</nav>
<!-- With Page Size Selector -->
<nav class="pagination" aria-label="Pagination">
<div class="pagination__controls">
<label class="pagination__size-label">
Show:
<select class="pagination__size-select">
<option value="10">10</option>
<option value="25" selected>25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
per page
</label>
</div>
<div class="pagination__nav">
<!-- Buttons here -->
</div>
<span class="pagination__summary">
Showing <strong>1-25</strong> of <strong>237</strong> items
</span>
</nav>
CSS:
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.pagination__list {
display: flex;
align-items: center;
gap: 4px;
list-style: none;
margin: 0;
padding: 0;
}
.pagination__button {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 12px;
border: 1px solid var(--gray-300);
background: var(--white);
color: var(--gray-700);
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.pagination__button:hover:not(:disabled) {
background: var(--gray-50);
border-color: var(--gray-400);
}
.pagination__button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination__page {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
border: 1px solid var(--gray-300);
background: var(--white);
color: var(--gray-700);
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.pagination__page:hover {
background: var(--gray-50);
border-color: var(--gray-400);
}
.pagination__page--current {
background: var(--blue-500);
border-color: var(--blue-500);
color: var(--white);
cursor: default;
}
.pagination__ellipsis {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
color: var(--gray-400);
}
.pagination__info {
font-size: 14px;
color: var(--gray-700);
}
.pagination__summary {
font-size: 14px;
color: var(--gray-600);
}
.pagination__size-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--gray-700);
}
.pagination__size-select {
height: 32px;
padding: 0 32px 0 8px;
border: 1px solid var(--gray-300);
border-radius: 6px;
background: var(--white);
font-size: 14px;
cursor: pointer;
}
/* Compact variant */
.pagination--compact {
justify-content: center;
}
.pagination--compact .pagination__list {
display: none;
}
/* Mobile responsive */
@media (max-width: 640px) {
.pagination {
flex-direction: column;
}
.pagination__list {
order: -1;
}
/* Hide middle pages on mobile */
.pagination__page:not(:first-child):not(:last-child):not(.pagination__page--current) {
display: none;
}
}
JavaScript:
class Pagination {
constructor(container, options = {}) {
this.container = container;
this.currentPage = options.currentPage || 1;
this.totalPages = options.totalPages || 1;
this.onPageChange = options.onPageChange || (() => {});
this.init();
}
init() {
this.render();
this.attachListeners();
}
render() {
const pages = this.calculatePages();
// Render pagination HTML
}
calculatePages() {
const { currentPage, totalPages } = this;
const pages = [];
if (totalPages <= 7) {
// Show all pages
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Show first, last, current, and neighbors with ellipsis
pages.push(1);
if (currentPage > 3) {
pages.push('...');
}
for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) {
pages.push(i);
}
if (currentPage < totalPages - 2) {
pages.push('...');
}
pages.push(totalPages);
}
return pages;
}
goToPage(page) {
if (page >= 1 && page <= this.totalPages && page !== this.currentPage) {
this.currentPage = page;
this.render();
this.onPageChange(page);
}
}
attachListeners() {
this.container.addEventListener('click', (e) => {
const pageBtn = e.target.closest('.pagination__page');
const prevBtn = e.target.closest('[aria-label*="Previous"]');
const nextBtn = e.target.closest('[aria-label*="Next"]');
if (pageBtn && !pageBtn.classList.contains('pagination__page--current')) {
const page = parseInt(pageBtn.textContent);
this.goToPage(page);
} else if (prevBtn) {
this.goToPage(this.currentPage - 1);
} else if (nextBtn) {
this.goToPage(this.currentPage + 1);
}
});
}
}
M10: AvatarGroup (Complete Specification)
Purpose: Display multiple user avatars with overlap
Test Criteria:
composition:
- atoms: multiple_avatars
- layout: overlapping (negative margin)
behavior:
- overlap: -8px per avatar
- max_visible: 3-5 avatars
- overflow: "+N" indicator
- hover: expand stack slightly
accessibility:
- role: group
- aria_label: "Team members: Name1, Name2..."
- tooltip: show full name on hover
Variants:
<!-- Basic Avatar Group -->
<div class="avatar-group"
role="group"
aria-label="Team members: Alice, Bob, Charlie"
data-testid="avatar-group">
<img src="/alice.jpg" alt="Alice" class="avatar avatar--stacked" />
<img src="/bob.jpg" alt="Bob" class="avatar avatar--stacked" />
<img src="/charlie.jpg" alt="Charlie" class="avatar avatar--stacked" />
</div>
<!-- With Overflow Count -->
<div class="avatar-group"
role="group"
aria-label="Team members: 8 total"
data-testid="avatar-group-overflow">
<img src="/user1.jpg" alt="User 1" class="avatar avatar--stacked" />
<img src="/user2.jpg" alt="User 2" class="avatar avatar--stacked" />
<img src="/user3.jpg" alt="User 3" class="avatar avatar--stacked" />
<div class="avatar avatar--stacked avatar--count" aria-label="5 more members">
+5
</div>
</div>
<!-- With Tooltips -->
<div class="avatar-group" role="group">
<div class="avatar-wrapper" data-tooltip="Alice Johnson">
<img src="/alice.jpg" alt="Alice Johnson" class="avatar avatar--stacked" />
</div>
<div class="avatar-wrapper" data-tooltip="Bob Smith">
<img src="/bob.jpg" alt="Bob Smith" class="avatar avatar--stacked" />
</div>
<div class="avatar-wrapper" data-tooltip="Charlie Brown">
<img src="/charlie.jpg" alt="Charlie Brown" class="avatar avatar--stacked" />
</div>
</div>
<!-- Size Variants -->
<div class="avatar-group avatar-group--sm">
<img src="/u1.jpg" alt="User 1" class="avatar avatar--sm avatar--stacked" />
<img src="/u2.jpg" alt="User 2" class="avatar avatar--sm avatar--stacked" />
</div>
<div class="avatar-group avatar-group--lg">
<img src="/u1.jpg" alt="User 1" class="avatar avatar--lg avatar--stacked" />
<img src="/u2.jpg" alt="User 2" class="avatar avatar--lg avatar--stacked" />
</div>
CSS:
.avatar-group {
display: inline-flex;
align-items: center;
padding-left: 8px; /* Compensate for first avatar */
}
.avatar--stacked {
margin-left: -8px;
border: 2px solid var(--white);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, z-index 0.2s;
}
.avatar-group:hover .avatar--stacked {
margin-left: 0;
}
.avatar--stacked:hover {
transform: scale(1.1);
z-index: 10;
}
.avatar--count {
display: flex;
align-items: center;
justify-content: center;
background: var(--gray-300);
color: var(--gray-700);
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.avatar--count:hover {
background: var(--gray-400);
}
/* Tooltip */
.avatar-wrapper {
position: relative;
display: inline-block;
}
.avatar-wrapper[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
padding: 6px 12px;
background: var(--gray-900);
color: var(--white);
font-size: 12px;
border-radius: 6px;
white-space: nowrap;
z-index: 100;
pointer-events: none;
}
.avatar-wrapper[data-tooltip]:hover::before {
content: '';
position: absolute;
bottom: calc(100% + 2px);
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: var(--gray-900);
z-index: 100;
pointer-events: none;
}
/* Size variants */
.avatar-group--sm .avatar--stacked {
margin-left: -6px;
}
.avatar-group--lg .avatar--stacked {
margin-left: -12px;
}
M11: LoadingSpinner
Purpose: Animated loading indicator
Test Criteria:
composition:
- atom: spinner (CSS animation)
- atom: text (optional message)
behavior:
- continuous_rotation: smooth animation
- size_variants: sm, md, lg
- color_variants: primary, white, current
accessibility:
- role: status
- aria_live: polite
- aria_label: "Loading content"
- sr_only_text: descriptive message
Variants:
<!-- Basic Spinner -->
<div class="spinner" role="status" aria-label="Loading" data-testid="spinner">
<svg class="spinner__svg" viewBox="0 0 50 50">
<circle class="spinner__circle" cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
</svg>
<span class="sr-only">Loading...</span>
</div>
<!-- With Message -->
<div class="spinner spinner--with-message" role="status" aria-live="polite">
<svg class="spinner__svg" viewBox="0 0 50 50">
<circle class="spinner__circle" cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
</svg>
<span class="spinner__message">Loading projects...</span>
</div>
<!-- Size Variants -->
<div class="spinner spinner--sm" role="status" aria-label="Loading">
<svg class="spinner__svg" viewBox="0 0 50 50">
<circle class="spinner__circle" cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
</svg>
</div>
<div class="spinner spinner--lg" role="status" aria-label="Loading">
<svg class="spinner__svg" viewBox="0 0 50 50">
<circle class="spinner__circle" cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
</svg>
</div>
<!-- Color Variants -->
<div class="spinner spinner--primary" role="status"></div>
<div class="spinner spinner--white" role="status"></div>
<!-- Overlay Spinner -->
<div class="spinner-overlay" data-testid="spinner-overlay">
<div class="spinner spinner--lg" role="status" aria-label="Loading page">
<svg class="spinner__svg" viewBox="0 0 50 50">
<circle class="spinner__circle" cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
</svg>
<span class="spinner__message">Please wait...</span>
</div>
</div>
CSS:
.spinner {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.spinner__svg {
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.spinner__circle {
stroke: var(--blue-500);
stroke-linecap: round;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
animation: dash 1.5s ease-in-out infinite;
}
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}
.spinner__message {
font-size: 14px;
color: var(--gray-700);
font-weight: 500;
}
/* Size variants */
.spinner--sm .spinner__svg {
width: 24px;
height: 24px;
}
.spinner--lg .spinner__svg {
width: 64px;
height: 64px;
}
/* Color variants */
.spinner--primary .spinner__circle {
stroke: var(--blue-500);
}
.spinner--white .spinner__circle {
stroke: var(--white);
}
/* Overlay */
.spinner-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
/* Screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
This completes all missing molecules with production-grade specifications!