From ui-design-visualizer
Comprehensive UI patterns library with components, layouts, and interaction patterns. Use when searching for UI component patterns, layout structures, or design best practices.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ui-design-visualizer:ui-patterns-libraryThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A comprehensive reference library of modern UI patterns, components, and layouts for web and mobile applications.
A comprehensive reference library of modern UI patterns, components, and layouts for web and mobile applications.
Use Case: Display discrete content items with consistent styling
Structure:
┌─────────────────────────┐
│ [Image] │
├─────────────────────────┤
│ Title │
│ Description text... │
│ │
│ [Action Button] │
└─────────────────────────┘
HTML Structure:
<!-- card.component.html -->
<article class="card" role="article">
<div class="card-media">
<img src="image.jpg" alt="Card image description" loading="lazy">
</div>
<div class="card-content">
<h3 class="card-title">Card Title</h3>
<p class="card-description">Card description text goes here.</p>
</div>
<div class="card-actions">
<button type="button" class="btn btn-primary">Learn More</button>
</div>
</article>
CSS:
/* card.component.css */
/**
* Basic Card Component
*
* A versatile container for content with image, text, and actions.
* Follows WCAG 2.1 AA contrast requirements.
*
* @component Card
* @status Stable
* @accessibility AA compliant
*/
.card {
--card-bg: var(--color-surface, #ffffff);
--card-border: var(--color-border, #e5e7eb);
--card-radius: var(--radius-lg, 12px);
--card-shadow: var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
--card-padding: var(--space-lg, 1.5rem);
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: var(--card-radius);
box-shadow: var(--card-shadow);
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
/**
* Hover effect for interactive cards
*/
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.15));
}
/**
* Card media container
* Uses aspect-ratio for consistent image dimensions
*/
.card-media {
position: relative;
aspect-ratio: 16 / 9;
overflow: hidden;
background: var(--color-gray-100);
}
.card-media img {
width: 100%;
height: 100%;
object-fit: cover;
}
/**
* Card content area
*/
.card-content {
padding: var(--card-padding);
}
.card-title {
margin: 0 0 var(--space-sm, 0.5rem);
font-size: var(--text-lg, 1.125rem);
font-weight: var(--font-semibold, 600);
color: var(--color-text-primary);
}
.card-description {
margin: 0;
font-size: var(--text-base, 1rem);
color: var(--color-text-secondary);
line-height: 1.6;
}
/**
* Card actions area
*/
.card-actions {
padding: 0 var(--card-padding) var(--card-padding);
display: flex;
gap: var(--space-sm, 0.5rem);
}
/**
* Responsive adjustments
*/
@media (max-width: 640px) {
.card {
--card-padding: var(--space-md, 1rem);
}
}
Use Case: E-commerce product display with pricing and quick actions
Structure:
┌─────────────────────────┐
│ [Product Image] │
│ [Wishlist ❤] │
├─────────────────────────┤
│ Product Name │
│ ⭐⭐⭐⭐⭐ (4.5) │
│ $99.99 [was $149.99] │
│ [Add to Cart] │
└─────────────────────────┘
HTML Structure:
<!-- product-card.component.html -->
<article class="product-card" itemscope itemtype="https://schema.org/Product">
<div class="product-card-media">
<img
itemprop="image"
src="product.jpg"
alt="Product name"
loading="lazy">
<button
type="button"
class="btn-wishlist"
aria-label="Add to wishlist">
<svg aria-hidden="true"><!-- heart icon --></svg>
</button>
</div>
<div class="product-card-content">
<h3 class="product-name" itemprop="name">Product Name</h3>
<div class="product-rating" role="img" aria-label="Rated 4.5 out of 5 stars">
<div class="stars">
<span class="star filled" aria-hidden="true">★</span>
<span class="star filled" aria-hidden="true">★</span>
<span class="star filled" aria-hidden="true">★</span>
<span class="star filled" aria-hidden="true">★</span>
<span class="star half" aria-hidden="true">★</span>
</div>
<span class="rating-value">(4.5)</span>
</div>
<div class="product-price" itemprop="offers" itemscope itemtype="https://schema.org/Offer">
<span class="price-current" itemprop="price" content="99.99">$99.99</span>
<span class="price-original">$149.99</span>
<meta itemprop="priceCurrency" content="USD">
</div>
</div>
<div class="product-card-actions">
<button type="button" class="btn btn-primary btn-block">
Add to Cart
</button>
</div>
</article>
CSS:
/* product-card.component.css */
/**
* Product Card Component
*
* Displays product information with image, pricing, ratings, and actions.
* Includes structured data for SEO.
*
* @component ProductCard
* @status Stable
* @accessibility AA compliant
* @seo Enhanced with schema.org markup
*/
.product-card {
--product-card-bg: var(--color-surface);
--product-card-radius: var(--radius-lg, 12px);
background: var(--product-card-bg);
border-radius: var(--product-card-radius);
overflow: hidden;
transition: transform 0.2s ease;
}
.product-card:hover {
transform: translateY(-4px);
}
/**
* Product media with wishlist button overlay
*/
.product-card-media {
position: relative;
aspect-ratio: 1 / 1;
overflow: hidden;
background: var(--color-gray-100);
}
.product-card-media img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.product-card:hover .product-card-media img {
transform: scale(1.05);
}
/**
* Wishlist button positioned in top-right
*/
.btn-wishlist {
position: absolute;
top: var(--space-sm, 0.5rem);
right: var(--space-sm, 0.5rem);
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
}
.btn-wishlist:hover {
background: rgba(255, 255, 255, 1);
}
.btn-wishlist svg {
width: 20px;
height: 20px;
fill: var(--color-text-secondary);
}
.btn-wishlist.active svg {
fill: var(--color-error, #dc2626);
}
/**
* Product information
*/
.product-card-content {
padding: var(--space-md, 1rem);
}
.product-name {
margin: 0 0 var(--space-xs, 0.25rem);
font-size: var(--text-base, 1rem);
font-weight: var(--font-semibold, 600);
color: var(--color-text-primary);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/**
* Star rating display
*/
.product-rating {
display: flex;
align-items: center;
gap: var(--space-xs, 0.25rem);
margin-bottom: var(--space-sm, 0.5rem);
}
.stars {
display: flex;
gap: 2px;
}
.star {
color: var(--color-gray-300);
font-size: var(--text-sm, 0.875rem);
}
.star.filled {
color: var(--color-warning, #f59e0b);
}
.rating-value {
font-size: var(--text-sm, 0.875rem);
color: var(--color-text-secondary);
}
/**
* Pricing display with sale indicator
*/
.product-price {
display: flex;
align-items: center;
gap: var(--space-sm, 0.5rem);
margin-bottom: var(--space-md, 1rem);
}
.price-current {
font-size: var(--text-xl, 1.25rem);
font-weight: var(--font-bold, 700);
color: var(--color-primary);
}
.price-original {
font-size: var(--text-sm, 0.875rem);
color: var(--color-text-tertiary);
text-decoration: line-through;
}
/**
* Action buttons
*/
.product-card-actions {
padding: 0 var(--space-md, 1rem) var(--space-md, 1rem);
}
.btn-block {
width: 100%;
}
Use Case: Complex forms broken into manageable steps
Structure:
┌─────────────────────────────────────┐
│ Step 1 ━━━ Step 2 ─── Step 3 ─── │
├─────────────────────────────────────┤
│ │
│ Step 1: Personal Information │
│ │
│ [First Name] │
│ [Last Name] │
│ [Email] │
│ │
│ [Back] [Next Step →] │
└─────────────────────────────────────┘
HTML Structure:
<!-- multi-step-form.component.html -->
<form class="multi-step-form" [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<!-- Progress Indicator -->
<div class="form-progress" role="progressbar"
[attr.aria-valuenow]="currentStep"
[attr.aria-valuemin]="1"
[attr.aria-valuemax]="totalSteps">
<div class="progress-steps">
<div class="progress-step"
*ngFor="let step of steps; let i = index"
[class.active]="i + 1 === currentStep"
[class.completed]="i + 1 < currentStep">
<div class="step-number">{{ i + 1 }}</div>
<div class="step-label">{{ step.label }}</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill"
[style.width.%]="(currentStep / totalSteps) * 100"></div>
</div>
</div>
<!-- Step Content -->
<div class="form-content">
<!-- Step 1: Personal Information -->
<div class="form-step" *ngIf="currentStep === 1">
<h2 class="step-title">Personal Information</h2>
<div class="form-group">
<label for="firstName" class="form-label">
First Name <span class="required">*</span>
</label>
<input
type="text"
id="firstName"
class="form-control"
formControlName="firstName"
[attr.aria-invalid]="formGroup.get('firstName')?.invalid && formGroup.get('firstName')?.touched"
[attr.aria-describedby]="formGroup.get('firstName')?.invalid ? 'firstName-error' : null">
<div
*ngIf="formGroup.get('firstName')?.invalid && formGroup.get('firstName')?.touched"
id="firstName-error"
class="form-error"
role="alert">
First name is required
</div>
</div>
<div class="form-group">
<label for="lastName" class="form-label">
Last Name <span class="required">*</span>
</label>
<input
type="text"
id="lastName"
class="form-control"
formControlName="lastName"
[attr.aria-invalid]="formGroup.get('lastName')?.invalid && formGroup.get('lastName')?.touched"
[attr.aria-describedby]="formGroup.get('lastName')?.invalid ? 'lastName-error' : null">
<div
*ngIf="formGroup.get('lastName')?.invalid && formGroup.get('lastName')?.touched"
id="lastName-error"
class="form-error"
role="alert">
Last name is required
</div>
</div>
<div class="form-group">
<label for="email" class="form-label">
Email <span class="required">*</span>
</label>
<input
type="email"
id="email"
class="form-control"
formControlName="email"
[attr.aria-invalid]="formGroup.get('email')?.invalid && formGroup.get('email')?.touched"
[attr.aria-describedby]="formGroup.get('email')?.invalid ? 'email-error' : null">
<div
*ngIf="formGroup.get('email')?.invalid && formGroup.get('email')?.touched"
id="email-error"
class="form-error"
role="alert">
Please enter a valid email address
</div>
</div>
</div>
<!-- Additional steps would go here -->
</div>
<!-- Form Navigation -->
<div class="form-actions">
<button
type="button"
class="btn btn-secondary"
*ngIf="currentStep > 1"
(click)="previousStep()">
← Back
</button>
<button
type="button"
class="btn btn-primary"
*ngIf="currentStep < totalSteps"
(click)="nextStep()"
[disabled]="!isCurrentStepValid()">
Next Step →
</button>
<button
type="submit"
class="btn btn-primary"
*ngIf="currentStep === totalSteps"
[disabled]="formGroup.invalid">
Submit
</button>
</div>
</form>
TypeScript:
/* multi-step-form.component.ts */
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
/**
* Multi-Step Form Component
*
* Provides a stepped form interface with progress tracking and validation.
* Implements WCAG 2.1 AA accessibility standards.
*
* @component MultiStepFormComponent
* @status Stable
* @accessibility AA compliant
*
* @example
* <app-multi-step-form
* [steps]="formSteps"
* (formSubmit)="handleSubmit($event)">
* </app-multi-step-form>
*/
@Component({
selector: 'app-multi-step-form',
templateUrl: './multi-step-form.component.html',
styleUrls: ['./multi-step-form.component.css'],
standalone: true
})
export class MultiStepFormComponent implements OnInit {
/** Current active step (1-indexed) */
currentStep: number = 1;
/** Total number of steps */
totalSteps: number = 3;
/** Form group containing all form controls */
formGroup!: FormGroup;
/** Step configuration */
steps = [
{ label: 'Personal Info', fields: ['firstName', 'lastName', 'email'] },
{ label: 'Address', fields: ['street', 'city', 'zipCode'] },
{ label: 'Review', fields: [] }
];
constructor(private fb: FormBuilder) {}
/**
* Initialize form with validation rules
*/
ngOnInit(): void {
this.formGroup = this.fb.group({
// Step 1 fields
firstName: ['', [Validators.required, Validators.minLength(2)]],
lastName: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
// Step 2 fields
street: ['', Validators.required],
city: ['', Validators.required],
zipCode: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]],
});
}
/**
* Navigate to next step
* Validates current step before proceeding
*/
nextStep(): void {
if (this.isCurrentStepValid()) {
this.currentStep++;
this.scrollToTop();
}
}
/**
* Navigate to previous step
*/
previousStep(): void {
if (this.currentStep > 1) {
this.currentStep--;
this.scrollToTop();
}
}
/**
* Check if current step has valid fields
*
* @returns {boolean} True if all fields in current step are valid
*/
isCurrentStepValid(): boolean {
const currentStepConfig = this.steps[this.currentStep - 1];
if (currentStepConfig.fields.length === 0) {
return true;
}
return currentStepConfig.fields.every(field => {
const control = this.formGroup.get(field);
return control?.valid ?? false;
});
}
/**
* Handle form submission
*
* @emits formSubmit Event with form data
*/
onSubmit(): void {
if (this.formGroup.valid) {
console.log('Form submitted:', this.formGroup.value);
// Emit event or call service
}
}
/**
* Scroll to top of form on step change
* Improves accessibility and UX
*/
private scrollToTop(): void {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
CSS:
/* multi-step-form.component.css */
/**
* Multi-Step Form Component Styles
*
* Provides visual feedback for form progress and validation states.
* All interactive elements meet WCAG 2.1 AA contrast requirements.
*
* @component MultiStepForm
* @status Stable
* @accessibility AA compliant
*/
.multi-step-form {
--form-max-width: 600px;
--form-padding: var(--space-xl, 2rem);
max-width: var(--form-max-width);
margin: 0 auto;
padding: var(--form-padding);
}
/**
* Progress Indicator
*/
.form-progress {
margin-bottom: var(--space-xl, 2rem);
}
.progress-steps {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-md, 1rem);
}
.progress-step {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-xs, 0.25rem);
}
/**
* Step number circle
*/
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text-tertiary);
display: flex;
align-items: center;
justify-content: center;
font-weight: var(--font-semibold, 600);
transition: all 0.3s ease;
}
.progress-step.active .step-number {
border-color: var(--color-primary);
background: var(--color-primary);
color: white;
}
.progress-step.completed .step-number {
border-color: var(--color-success);
background: var(--color-success);
color: white;
}
/**
* Step label
*/
.step-label {
font-size: var(--text-sm, 0.875rem);
color: var(--color-text-secondary);
text-align: center;
}
.progress-step.active .step-label {
color: var(--color-primary);
font-weight: var(--font-semibold, 600);
}
/**
* Progress bar
*/
.progress-bar {
height: 4px;
background: var(--color-gray-200);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-primary);
transition: width 0.3s ease;
}
/**
* Form Content
*/
.form-content {
min-height: 400px;
}
.form-step {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.step-title {
margin: 0 0 var(--space-lg, 1.5rem);
font-size: var(--text-2xl, 1.5rem);
font-weight: var(--font-bold, 700);
color: var(--color-text-primary);
}
/**
* Form Groups
*/
.form-group {
margin-bottom: var(--space-lg, 1.5rem);
}
.form-label {
display: block;
margin-bottom: var(--space-xs, 0.25rem);
font-size: var(--text-sm, 0.875rem);
font-weight: var(--font-medium, 500);
color: var(--color-text-primary);
}
.required {
color: var(--color-error, #dc2626);
}
/**
* Form Controls
*/
.form-control {
width: 100%;
padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
font-size: var(--text-base, 1rem);
border: 1px solid var(--color-border);
border-radius: var(--radius-md, 6px);
background: var(--color-surface);
color: var(--color-text-primary);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-control:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-control[aria-invalid="true"] {
border-color: var(--color-error);
}
.form-control[aria-invalid="true"]:focus {
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
}
/**
* Error messages
*/
.form-error {
margin-top: var(--space-xs, 0.25rem);
font-size: var(--text-sm, 0.875rem);
color: var(--color-error);
}
/**
* Form Actions
*/
.form-actions {
display: flex;
gap: var(--space-md, 1rem);
justify-content: flex-end;
margin-top: var(--space-xl, 2rem);
padding-top: var(--space-xl, 2rem);
border-top: 1px solid var(--color-border);
}
.form-actions .btn {
min-width: 120px;
}
/**
* Responsive adjustments
*/
@media (max-width: 640px) {
.multi-step-form {
--form-padding: var(--space-md, 1rem);
}
.step-label {
font-size: var(--text-xs, 0.75rem);
}
.form-actions {
flex-direction: column;
}
.form-actions .btn {
width: 100%;
}
}
Use Case: Display focused content or forms over main content
Structure:
╔═════════════════════════════════════╗
║ [×] ║
║ ║
║ Dialog Title ║
║ ║
║ Content goes here... ║
║ ║
║ [Cancel] [Confirm] ║
╚═════════════════════════════════════╝
HTML Structure:
<!-- modal.component.html -->
<div class="modal-overlay"
*ngIf="isOpen"
(click)="closeOnBackdrop($event)"
[@fadeIn]
role="dialog"
aria-modal="true"
[attr.aria-labelledby]="'modal-title-' + id"
[attr.aria-describedby]="'modal-desc-' + id">
<div class="modal-container" (click)="$event.stopPropagation()">
<!-- Close Button -->
<button
type="button"
class="modal-close"
(click)="close()"
aria-label="Close dialog">
<svg aria-hidden="true" width="24" height="24">
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<!-- Modal Header -->
<div class="modal-header" *ngIf="title">
<h2 class="modal-title" [id]="'modal-title-' + id">{{ title }}</h2>
</div>
<!-- Modal Body -->
<div class="modal-body" [id]="'modal-desc-' + id">
<ng-content></ng-content>
</div>
<!-- Modal Footer -->
<div class="modal-footer" *ngIf="showActions">
<button
type="button"
class="btn btn-secondary"
(click)="cancel()">
{{ cancelText }}
</button>
<button
type="button"
class="btn btn-primary"
(click)="confirm()">
{{ confirmText }}
</button>
</div>
</div>
</div>
TypeScript:
/* modal.component.ts */
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { trigger, transition, style, animate } from '@angular/animations';
/**
* Modal Dialog Component
*
* Displays content in an overlay with backdrop.
* Implements WCAG 2.1 AA accessibility including focus trap and escape key handling.
*
* @component ModalComponent
* @status Stable
* @accessibility AA compliant
*
* @example
* <app-modal
* [isOpen]="showModal"
* title="Confirm Action"
* (modalClose)="handleClose()">
* <p>Are you sure you want to proceed?</p>
* </app-modal>
*/
@Component({
selector: 'app-modal',
templateUrl: './modal.component.html',
styleUrls: ['./modal.component.css'],
standalone: true,
animations: [
trigger('fadeIn', [
transition(':enter', [
style({ opacity: 0 }),
animate('200ms ease-out', style({ opacity: 1 }))
]),
transition(':leave', [
animate('200ms ease-in', style({ opacity: 0 }))
])
])
]
})
export class ModalComponent implements OnInit, OnDestroy {
/** Unique identifier for ARIA attributes */
@Input() id: string = `modal-${Math.random().toString(36).substr(2, 9)}`;
/** Controls modal visibility */
@Input() isOpen: boolean = false;
/** Modal title */
@Input() title?: string;
/** Show action buttons in footer */
@Input() showActions: boolean = true;
/** Cancel button text */
@Input() cancelText: string = 'Cancel';
/** Confirm button text */
@Input() confirmText: string = 'Confirm';
/** Close modal on backdrop click */
@Input() closeOnBackdropClick: boolean = true;
/** Emits when modal is closed */
@Output() modalClose = new EventEmitter<void>();
/** Emits when cancel button is clicked */
@Output() modalCancel = new EventEmitter<void>();
/** Emits when confirm button is clicked */
@Output() modalConfirm = new EventEmitter<void>();
/** Store previously focused element */
private previouslyFocusedElement?: HTMLElement;
ngOnInit(): void {
if (this.isOpen) {
this.trapFocus();
this.addEscapeListener();
}
}
ngOnDestroy(): void {
this.removeEscapeListener();
this.restoreFocus();
}
/**
* Close modal
*/
close(): void {
this.isOpen = false;
this.modalClose.emit();
this.restoreFocus();
}
/**
* Handle cancel action
*/
cancel(): void {
this.modalCancel.emit();
this.close();
}
/**
* Handle confirm action
*/
confirm(): void {
this.modalConfirm.emit();
this.close();
}
/**
* Close modal on backdrop click
*/
closeOnBackdrop(event: MouseEvent): void {
if (this.closeOnBackdropClick && event.target === event.currentTarget) {
this.close();
}
}
/**
* Trap focus within modal
*/
private trapFocus(): void {
this.previouslyFocusedElement = document.activeElement as HTMLElement;
// Implementation of focus trap logic
}
/**
* Restore focus to previously focused element
*/
private restoreFocus(): void {
this.previouslyFocusedElement?.focus();
}
/**
* Add escape key listener
*/
private addEscapeListener(): void {
document.addEventListener('keydown', this.handleEscape);
}
/**
* Remove escape key listener
*/
private removeEscapeListener(): void {
document.removeEventListener('keydown', this.handleEscape);
}
/**
* Handle escape key press
*/
private handleEscape = (event: KeyboardEvent): void => {
if (event.key === 'Escape' && this.isOpen) {
this.close();
}
};
}
CSS:
/* modal.component.css */
/**
* Modal Dialog Styles
*
* Full-screen overlay with centered dialog.
* Includes backdrop blur and smooth animations.
*
* @component Modal
* @status Stable
* @accessibility AA compliant
*/
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-lg, 1.5rem);
z-index: var(--z-modal, 1000);
}
/**
* Modal container
*/
.modal-container {
--modal-bg: var(--color-surface);
--modal-radius: var(--radius-lg, 12px);
--modal-shadow: var(--shadow-2xl, 0 25px 50px rgba(0, 0, 0, 0.25));
--modal-max-width: 500px;
position: relative;
background: var(--modal-bg);
border-radius: var(--modal-radius);
box-shadow: var(--modal-shadow);
max-width: var(--modal-max-width);
width: 100%;
max-height: 90vh;
overflow: auto;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/**
* Close button
*/
.modal-close {
position: absolute;
top: var(--space-md, 1rem);
right: var(--space-md, 1rem);
width: 32px;
height: 32px;
border: none;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
border-radius: var(--radius-sm, 4px);
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease, color 0.2s ease;
z-index: 1;
}
.modal-close:hover {
background: var(--color-gray-100);
color: var(--color-text-primary);
}
.modal-close:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/**
* Modal sections
*/
.modal-header {
padding: var(--space-xl, 2rem);
padding-bottom: 0;
}
.modal-title {
margin: 0;
font-size: var(--text-xl, 1.25rem);
font-weight: var(--font-bold, 700);
color: var(--color-text-primary);
}
.modal-body {
padding: var(--space-xl, 2rem);
color: var(--color-text-secondary);
line-height: 1.6;
}
.modal-footer {
padding: var(--space-xl, 2rem);
padding-top: 0;
display: flex;
gap: var(--space-md, 1rem);
justify-content: flex-end;
}
/**
* Responsive adjustments
*/
@media (max-width: 640px) {
.modal-overlay {
padding: var(--space-md, 1rem);
}
.modal-header,
.modal-body,
.modal-footer {
padding: var(--space-lg, 1.5rem);
}
.modal-footer {
flex-direction: column-reverse;
padding-top: 0;
}
.modal-footer .btn {
width: 100%;
}
}
Use Case: Main site navigation that adapts to mobile and desktop
Desktop:
┌─────────────────────────────────────────────┐
│ [Logo] Home Products About Contact │
└─────────────────────────────────────────────┘
Mobile:
┌─────────────────────────┐
│ [Logo] [☰] │
└─────────────────────────┘
When opened:
┌─────────────────────────┐
│ [Logo] [×] │
├─────────────────────────┤
│ Home │
│ Products │
│ About │
│ Contact │
└─────────────────────────┘
HTML Structure:
<!-- navigation.component.html -->
<nav class="navbar" role="navigation" aria-label="Main navigation">
<div class="navbar-container">
<!-- Logo -->
<a href="/" class="navbar-brand">
<img src="logo.svg" alt="Company Name" width="120" height="40">
</a>
<!-- Mobile Toggle Button -->
<button
type="button"
class="navbar-toggle"
(click)="toggleMenu()"
[attr.aria-expanded]="isMenuOpen"
aria-controls="navbar-menu"
aria-label="Toggle navigation menu">
<span class="toggle-icon" [class.open]="isMenuOpen">
<span></span>
<span></span>
<span></span>
</span>
</button>
<!-- Navigation Links -->
<div
id="navbar-menu"
class="navbar-menu"
[class.open]="isMenuOpen">
<ul class="navbar-nav">
<li class="nav-item">
<a
href="/"
class="nav-link"
routerLink="/"
routerLinkActive="active"
[routerLinkActiveOptions]="{exact: true}">
Home
</a>
</li>
<li class="nav-item">
<a
href="/products"
class="nav-link"
routerLink="/products"
routerLinkActive="active">
Products
</a>
</li>
<li class="nav-item">
<a
href="/about"
class="nav-link"
routerLink="/about"
routerLinkActive="active">
About
</a>
</li>
<li class="nav-item">
<a
href="/contact"
class="nav-link"
routerLink="/contact"
routerLinkActive="active">
Contact
</a>
</li>
</ul>
<div class="navbar-actions">
<button type="button" class="btn btn-primary">
Sign In
</button>
</div>
</div>
</div>
</nav>
TypeScript:
/* navigation.component.ts */
import { Component, HostListener } from '@angular/core';
/**
* Responsive Navigation Component
*
* Provides main site navigation with mobile hamburger menu.
* Implements WCAG 2.1 AA accessibility standards.
*
* @component NavigationComponent
* @status Stable
* @accessibility AA compliant
*/
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.css'],
standalone: true
})
export class NavigationComponent {
/** Mobile menu open state */
isMenuOpen: boolean = false;
/**
* Toggle mobile menu
*/
toggleMenu(): void {
this.isMenuOpen = !this.isMenuOpen;
// Prevent body scroll when menu is open
if (this.isMenuOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
}
/**
* Close menu when clicking outside
*/
@HostListener('document:click', ['$event'])
onDocumentClick(event: MouseEvent): void {
const target = event.target as HTMLElement;
const navbar = document.querySelector('.navbar');
if (this.isMenuOpen && navbar && !navbar.contains(target)) {
this.toggleMenu();
}
}
/**
* Close menu on window resize to desktop size
*/
@HostListener('window:resize')
onResize(): void {
if (window.innerWidth >= 768 && this.isMenuOpen) {
this.toggleMenu();
}
}
}
CSS:
/* navigation.component.css */
/**
* Responsive Navigation Styles
*
* Mobile-first navigation with hamburger menu.
* Transitions to horizontal nav on desktop.
*
* @component Navigation
* @status Stable
* @accessibility AA compliant
*/
.navbar {
--navbar-height: 64px;
--navbar-bg: var(--color-surface);
--navbar-border: var(--color-border);
position: sticky;
top: 0;
z-index: var(--z-header, 100);
background: var(--navbar-bg);
border-bottom: 1px solid var(--navbar-border);
height: var(--navbar-height);
}
.navbar-container {
max-width: var(--container-xl, 1280px);
margin: 0 auto;
padding: 0 var(--space-lg, 1.5rem);
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
/**
* Brand/Logo
*/
.navbar-brand {
display: flex;
align-items: center;
text-decoration: none;
z-index: 2;
}
.navbar-brand img {
display: block;
}
/**
* Mobile Toggle Button
*/
.navbar-toggle {
display: none;
width: 40px;
height: 40px;
border: none;
background: transparent;
cursor: pointer;
padding: 0;
z-index: 2;
}
.toggle-icon {
display: block;
position: relative;
width: 24px;
height: 18px;
}
.toggle-icon span {
display: block;
position: absolute;
height: 2px;
width: 100%;
background: var(--color-text-primary);
border-radius: 2px;
opacity: 1;
left: 0;
transform: rotate(0deg);
transition: 0.25s ease-in-out;
}
.toggle-icon span:nth-child(1) {
top: 0;
}
.toggle-icon span:nth-child(2) {
top: 8px;
}
.toggle-icon span:nth-child(3) {
top: 16px;
}
.toggle-icon.open span:nth-child(1) {
top: 8px;
transform: rotate(135deg);
}
.toggle-icon.open span:nth-child(2) {
opacity: 0;
left: -60px;
}
.toggle-icon.open span:nth-child(3) {
top: 8px;
transform: rotate(-135deg);
}
/**
* Navigation Menu
*/
.navbar-menu {
display: flex;
align-items: center;
gap: var(--space-xl, 2rem);
}
.navbar-nav {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: var(--space-md, 1rem);
}
.nav-item {
margin: 0;
}
.nav-link {
display: block;
padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
color: var(--color-text-secondary);
text-decoration: none;
font-weight: var(--font-medium, 500);
border-radius: var(--radius-md, 6px);
transition: color 0.2s ease, background 0.2s ease;
}
.nav-link:hover {
color: var(--color-text-primary);
background: var(--color-gray-100);
}
.nav-link.active {
color: var(--color-primary);
}
.navbar-actions {
display: flex;
gap: var(--space-sm, 0.5rem);
}
/**
* Mobile Styles
*/
@media (max-width: 767px) {
.navbar-toggle {
display: block;
}
.navbar-menu {
position: fixed;
top: var(--navbar-height);
left: 0;
right: 0;
bottom: 0;
background: var(--navbar-bg);
flex-direction: column;
align-items: stretch;
padding: var(--space-xl, 2rem);
gap: var(--space-lg, 1.5rem);
transform: translateX(-100%);
transition: transform 0.3s ease;
overflow-y: auto;
}
.navbar-menu.open {
transform: translateX(0);
}
.navbar-nav {
flex-direction: column;
gap: 0;
}
.nav-link {
padding: var(--space-md, 1rem);
font-size: var(--text-lg, 1.125rem);
}
.navbar-actions {
flex-direction: column;
}
.navbar-actions .btn {
width: 100%;
}
}
CSS:
/* button.component.css */
/**
* Button Component
*
* Provides consistent button styling across the application.
* All variants meet WCAG 2.1 AA contrast requirements (4.5:1 minimum).
*
* @component Button
* @status Stable
* @accessibility AA compliant
*/
.btn {
--btn-padding-x: var(--space-md, 1rem);
--btn-padding-y: var(--space-sm, 0.5rem);
--btn-font-size: var(--text-base, 1rem);
--btn-border-radius: var(--radius-md, 6px);
--btn-transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-xs, 0.25rem);
padding: var(--btn-padding-y) var(--btn-padding-x);
font-size: var(--btn-font-size);
font-weight: var(--font-medium, 500);
line-height: 1.5;
text-align: center;
text-decoration: none;
border: 1px solid transparent;
border-radius: var(--btn-border-radius);
cursor: pointer;
transition: var(--btn-transition);
user-select: none;
}
.btn:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
/**
* Primary Button
* Used for main actions (submit, save, etc.)
*/
.btn-primary {
background: var(--color-primary, #3b82f6);
color: white;
border-color: var(--color-primary, #3b82f6);
}
.btn-primary:hover {
background: var(--color-primary-dark, #2563eb);
border-color: var(--color-primary-dark, #2563eb);
}
.btn-primary:active {
background: var(--color-primary-darker, #1d4ed8);
}
/**
* Secondary Button
* Used for secondary actions (cancel, back, etc.)
*/
.btn-secondary {
background: transparent;
color: var(--color-text-primary);
border-color: var(--color-border);
}
.btn-secondary:hover {
background: var(--color-gray-100);
border-color: var(--color-gray-300);
}
/**
* Danger Button
* Used for destructive actions (delete, remove, etc.)
*/
.btn-danger {
background: var(--color-error, #dc2626);
color: white;
border-color: var(--color-error, #dc2626);
}
.btn-danger:hover {
background: var(--color-error-dark, #b91c1c);
border-color: var(--color-error-dark, #b91c1c);
}
/**
* Ghost Button
* Transparent button with colored text
*/
.btn-ghost {
background: transparent;
color: var(--color-primary);
border-color: transparent;
}
.btn-ghost:hover {
background: var(--color-primary-light, #eff6ff);
}
/**
* Size Variants
*/
.btn-sm {
--btn-padding-x: var(--space-sm, 0.5rem);
--btn-padding-y: var(--space-xs, 0.25rem);
--btn-font-size: var(--text-sm, 0.875rem);
}
.btn-lg {
--btn-padding-x: var(--space-lg, 1.5rem);
--btn-padding-y: var(--space-md, 1rem);
--btn-font-size: var(--text-lg, 1.125rem);
}
/**
* Block Button (Full Width)
*/
.btn-block {
width: 100%;
}
/**
* Icon Button
*/
.btn-icon {
--btn-padding-x: var(--space-sm, 0.5rem);
--btn-padding-y: var(--space-sm, 0.5rem);
width: 40px;
height: 40px;
padding: 0;
}
.btn-icon svg {
width: 20px;
height: 20px;
}
/**
* Button with Loading State
*/
.btn-loading {
position: relative;
color: transparent;
pointer-events: none;
}
.btn-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
Structure:
┌─────────────────────────────────────────┐
│ Dashboard │
├─────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Total │ │ Active │ │ Revenue │ │
│ │ Users │ │ Users │ │ $50k │ │
│ │ 1,234 │ │ 567 │ │ ↑ 12% │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ┌─────────────────────┐ ┌───────────┐ │
│ │ Chart Area │ │ Recent │ │
│ │ │ │ Activity │ │
│ │ │ │ │ │
│ └─────────────────────┘ └───────────┘ │
└─────────────────────────────────────────┘
HTML Structure:
<!-- dashboard.component.html -->
<div class="dashboard">
<header class="dashboard-header">
<h1 class="dashboard-title">Analytics Dashboard</h1>
<div class="dashboard-actions">
<button type="button" class="btn btn-secondary">
Export
</button>
<button type="button" class="btn btn-primary">
New Report
</button>
</div>
</header>
<!-- Stat Cards -->
<div class="stat-grid">
<article class="stat-card">
<div class="stat-icon" style="background: #eff6ff;">
<svg width="24" height="24" fill="#3b82f6">
<!-- Users icon -->
</svg>
</div>
<div class="stat-content">
<h3 class="stat-label">Total Users</h3>
<p class="stat-value">1,234</p>
<p class="stat-trend positive">
<span class="trend-icon">↑</span>
<span class="trend-value">12%</span>
<span class="trend-label">from last month</span>
</p>
</div>
</article>
<article class="stat-card">
<div class="stat-icon" style="background: #f0fdf4;">
<svg width="24" height="24" fill="#22c55e">
<!-- Active users icon -->
</svg>
</div>
<div class="stat-content">
<h3 class="stat-label">Active Users</h3>
<p class="stat-value">567</p>
<p class="stat-trend positive">
<span class="trend-icon">↑</span>
<span class="trend-value">8%</span>
<span class="trend-label">from last month</span>
</p>
</div>
</article>
<article class="stat-card">
<div class="stat-icon" style="background: #fef3c7;">
<svg width="24" height="24" fill="#f59e0b">
<!-- Revenue icon -->
</svg>
</div>
<div class="stat-content">
<h3 class="stat-label">Revenue</h3>
<p class="stat-value">$50,234</p>
<p class="stat-trend positive">
<span class="trend-icon">↑</span>
<span class="trend-value">23%</span>
<span class="trend-label">from last month</span>
</p>
</div>
</article>
</div>
<!-- Main Content Grid -->
<div class="dashboard-grid">
<!-- Chart Section -->
<section class="dashboard-section chart-section">
<header class="section-header">
<h2 class="section-title">Revenue Overview</h2>
<select class="select-input" aria-label="Select time period">
<option>Last 7 days</option>
<option>Last 30 days</option>
<option>Last 90 days</option>
</select>
</header>
<div class="section-content">
<!-- Chart component would go here -->
<div class="chart-placeholder">
Chart visualization
</div>
</div>
</section>
<!-- Activity Feed -->
<section class="dashboard-section activity-section">
<header class="section-header">
<h2 class="section-title">Recent Activity</h2>
</header>
<div class="section-content">
<ul class="activity-list">
<li class="activity-item">
<div class="activity-icon">
<svg width="16" height="16"><!-- icon --></svg>
</div>
<div class="activity-content">
<p class="activity-text">New user registered</p>
<time class="activity-time" datetime="2025-01-15T10:30:00">
2 hours ago
</time>
</div>
</li>
<!-- More activity items -->
</ul>
</div>
</section>
</div>
</div>
CSS:
/* dashboard.component.css */
/**
* Dashboard Layout
*
* Responsive dashboard with stat cards, charts, and activity feeds.
* Uses CSS Grid for flexible layout.
*
* @component Dashboard
* @status Stable
* @accessibility AA compliant
*/
.dashboard {
padding: var(--space-xl, 2rem);
max-width: var(--container-xl, 1280px);
margin: 0 auto;
}
/**
* Dashboard Header
*/
.dashboard-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-xl, 2rem);
}
.dashboard-title {
margin: 0;
font-size: var(--text-3xl, 1.875rem);
font-weight: var(--font-bold, 700);
color: var(--color-text-primary);
}
.dashboard-actions {
display: flex;
gap: var(--space-sm, 0.5rem);
}
/**
* Stat Cards Grid
*/
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-lg, 1.5rem);
margin-bottom: var(--space-xl, 2rem);
}
.stat-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg, 12px);
padding: var(--space-lg, 1.5rem);
display: flex;
gap: var(--space-md, 1rem);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-md, 6px);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.stat-content {
flex: 1;
}
.stat-label {
margin: 0 0 var(--space-xs, 0.25rem);
font-size: var(--text-sm, 0.875rem);
font-weight: var(--font-medium, 500);
color: var(--color-text-secondary);
}
.stat-value {
margin: 0 0 var(--space-xs, 0.25rem);
font-size: var(--text-2xl, 1.5rem);
font-weight: var(--font-bold, 700);
color: var(--color-text-primary);
}
.stat-trend {
display: flex;
align-items: center;
gap: var(--space-xs, 0.25rem);
font-size: var(--text-sm, 0.875rem);
margin: 0;
}
.stat-trend.positive {
color: var(--color-success, #22c55e);
}
.stat-trend.negative {
color: var(--color-error, #dc2626);
}
.trend-label {
color: var(--color-text-tertiary);
}
/**
* Main Dashboard Grid
*/
.dashboard-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: var(--space-lg, 1.5rem);
}
.dashboard-section {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg, 12px);
overflow: hidden;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-lg, 1.5rem);
border-bottom: 1px solid var(--color-border);
}
.section-title {
margin: 0;
font-size: var(--text-lg, 1.125rem);
font-weight: var(--font-semibold, 600);
color: var(--color-text-primary);
}
.section-content {
padding: var(--space-lg, 1.5rem);
}
/**
* Chart Section
*/
.chart-placeholder {
height: 300px;
background: var(--color-gray-50);
border-radius: var(--radius-md, 6px);
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-tertiary);
}
/**
* Activity Feed
*/
.activity-list {
list-style: none;
margin: 0;
padding: 0;
}
.activity-item {
display: flex;
gap: var(--space-md, 1rem);
padding: var(--space-md, 1rem) 0;
border-bottom: 1px solid var(--color-border);
}
.activity-item:last-child {
border-bottom: none;
}
.activity-icon {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--color-gray-100);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.activity-content {
flex: 1;
}
.activity-text {
margin: 0 0 var(--space-xs, 0.25rem);
font-size: var(--text-sm, 0.875rem);
color: var(--color-text-primary);
}
.activity-time {
font-size: var(--text-xs, 0.75rem);
color: var(--color-text-tertiary);
}
/**
* Responsive Adjustments
*/
@media (max-width: 1024px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.dashboard {
padding: var(--space-md, 1rem);
}
.dashboard-header {
flex-direction: column;
align-items: flex-start;
gap: var(--space-md, 1rem);
}
.dashboard-actions {
width: 100%;
flex-direction: column;
}
.dashboard-actions .btn {
width: 100%;
}
.stat-grid {
grid-template-columns: 1fr;
}
}
CSS:
/* focus.css */
/**
* Focus Styles
*
* Consistent focus indicators across all interactive elements.
* Meets WCAG 2.1 AA requirements for focus visibility.
*
* @category Accessibility
* @status Stable
* @wcag 2.4.7 Focus Visible (Level AA)
*/
/**
* Remove default browser outline and add custom focus styles
*/
*:focus {
outline: none;
}
*:focus-visible {
outline: 2px solid var(--color-focus, #3b82f6);
outline-offset: 2px;
}
/**
* Focus styles for specific elements
*/
/* Buttons */
button:focus-visible,
.btn:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
/* Links */
a:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
border-radius: var(--radius-sm, 4px);
}
/* Form inputs */
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 0;
border-color: var(--color-focus);
}
/**
* Skip to main content link
* Hidden until focused for keyboard navigation
*/
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--color-primary);
color: white;
padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
text-decoration: none;
border-radius: var(--radius-md, 6px);
z-index: var(--z-popover, 200);
}
.skip-link:focus {
top: var(--space-md, 1rem);
}
Use Case: Mobile app navigation at bottom of screen
Structure:
┌─────────────────────────┐
│ │
│ Main Content Area │
│ │
├─────────────────────────┤
│ [🏠] [🔍] [+] [💬] [👤] │
│ Home Search Add Chat Me │
└─────────────────────────┘
HTML Structure:
<!-- bottom-nav.component.html -->
<nav class="bottom-nav" role="navigation" aria-label="Mobile navigation">
<a
href="/"
class="bottom-nav-item"
routerLink="/"
routerLinkActive="active"
[routerLinkActiveOptions]="{exact: true}"
aria-label="Home">
<svg class="nav-icon" aria-hidden="true"><!-- home icon --></svg>
<span class="nav-label">Home</span>
</a>
<a
href="/search"
class="bottom-nav-item"
routerLink="/search"
routerLinkActive="active"
aria-label="Search">
<svg class="nav-icon" aria-hidden="true"><!-- search icon --></svg>
<span class="nav-label">Search</span>
</a>
<button
type="button"
class="bottom-nav-item fab"
aria-label="Create new post">
<svg class="nav-icon" aria-hidden="true"><!-- plus icon --></svg>
</button>
<a
href="/messages"
class="bottom-nav-item"
routerLink="/messages"
routerLinkActive="active"
aria-label="Messages">
<svg class="nav-icon" aria-hidden="true"><!-- message icon --></svg>
<span class="nav-label">Messages</span>
<span class="badge" *ngIf="unreadCount > 0">{{ unreadCount }}</span>
</a>
<a
href="/profile"
class="bottom-nav-item"
routerLink="/profile"
routerLinkActive="active"
aria-label="Profile">
<svg class="nav-icon" aria-hidden="true"><!-- user icon --></svg>
<span class="nav-label">Profile</span>
</a>
</nav>
CSS:
/* bottom-nav.component.css */
/**
* Bottom Navigation Component
*
* Fixed bottom navigation for mobile apps.
* Follows iOS and Android design guidelines.
*
* @component BottomNav
* @status Stable
* @accessibility AA compliant
* @platform Mobile
*/
.bottom-nav {
--nav-height: 64px;
--nav-bg: var(--color-surface);
--nav-border: var(--color-border);
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--nav-height);
background: var(--nav-bg);
border-top: 1px solid var(--nav-border);
display: flex;
justify-content: space-around;
align-items: center;
padding: var(--space-xs, 0.25rem) var(--space-md, 1rem);
z-index: var(--z-header, 100);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
/* Safe area for notched devices */
padding-bottom: max(var(--space-xs, 0.25rem), env(safe-area-inset-bottom));
}
/**
* Navigation Items
*/
.bottom-nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: var(--space-xs, 0.25rem);
color: var(--color-text-tertiary);
text-decoration: none;
border: none;
background: transparent;
cursor: pointer;
transition: color 0.2s ease;
position: relative;
}
.bottom-nav-item:hover {
color: var(--color-text-secondary);
}
.bottom-nav-item.active {
color: var(--color-primary);
}
.nav-icon {
width: 24px;
height: 24px;
fill: currentColor;
}
.nav-label {
font-size: var(--text-xs, 0.75rem);
font-weight: var(--font-medium, 500);
}
/**
* Floating Action Button (FAB)
*/
.bottom-nav-item.fab {
margin-top: -20px;
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--color-primary);
color: white;
box-shadow: var(--shadow-lg, 0 10px 15px rgba(0, 0, 0, 0.15));
}
.bottom-nav-item.fab .nav-icon {
width: 28px;
height: 28px;
}
/**
* Notification Badge
*/
.badge {
position: absolute;
top: 0;
right: 50%;
transform: translateX(12px);
min-width: 18px;
height: 18px;
padding: 0 4px;
background: var(--color-error, #dc2626);
color: white;
font-size: 10px;
font-weight: var(--font-bold, 700);
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
}
This UI Patterns Library provides:
Each pattern includes:
Use this library to quickly implement consistent, accessible UI components across your application.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub ehssanatassi/ui-marketplace --plugin ui-design-visualizer