Initial commit
This commit is contained in:
622
skills/frontend-designer/references/accessibility_checklist.md
Normal file
622
skills/frontend-designer/references/accessibility_checklist.md
Normal file
@@ -0,0 +1,622 @@
|
||||
# WCAG 2.1 AA Accessibility Checklist
|
||||
|
||||
Comprehensive checklist for ensuring your frontend meets WCAG 2.1 Level AA compliance.
|
||||
|
||||
## Perceivable
|
||||
|
||||
Information and user interface components must be presentable to users in ways they can perceive.
|
||||
|
||||
### 1.1 Text Alternatives
|
||||
|
||||
**1.1.1 Non-text Content (Level A)**
|
||||
- [ ] All images have appropriate alt text
|
||||
- [ ] Decorative images use empty alt (`alt=""`)
|
||||
- [ ] Complex images have detailed descriptions
|
||||
- [ ] Icons have text alternatives or aria-label
|
||||
- [ ] Charts/graphs have text descriptions
|
||||
- [ ] CAPTCHAs have alternative forms
|
||||
|
||||
```html
|
||||
<!-- Good examples -->
|
||||
<img src="logo.png" alt="Company Name">
|
||||
<img src="decorative.png" alt="" role="presentation">
|
||||
<button aria-label="Close dialog"><span aria-hidden="true">×</span></button>
|
||||
```
|
||||
|
||||
### 1.2 Time-based Media
|
||||
|
||||
**1.2.1 Audio-only and Video-only (Level A)**
|
||||
- [ ] Audio-only content has transcripts
|
||||
- [ ] Video-only content has transcripts or audio description
|
||||
|
||||
**1.2.2 Captions (Level A)**
|
||||
- [ ] All pre-recorded videos have captions
|
||||
- [ ] Captions are synchronized and accurate
|
||||
|
||||
**1.2.3 Audio Description or Media Alternative (Level A)**
|
||||
- [ ] Videos have audio descriptions or text alternative
|
||||
|
||||
**1.2.4 Captions (Live) (Level AA)**
|
||||
- [ ] Live videos have captions
|
||||
|
||||
**1.2.5 Audio Description (Level AA)**
|
||||
- [ ] All pre-recorded videos have audio descriptions
|
||||
|
||||
```html
|
||||
<video controls>
|
||||
<source src="video.mp4" type="video/mp4">
|
||||
<track kind="captions" src="captions.vtt" srclang="en" label="English">
|
||||
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="English descriptions">
|
||||
</video>
|
||||
```
|
||||
|
||||
### 1.3 Adaptable
|
||||
|
||||
**1.3.1 Info and Relationships (Level A)**
|
||||
- [ ] Semantic HTML used correctly (headings, lists, tables)
|
||||
- [ ] Form labels properly associated with inputs
|
||||
- [ ] Related form controls are grouped
|
||||
- [ ] Visual presentation matches code structure
|
||||
- [ ] ARIA roles used when needed
|
||||
|
||||
```html
|
||||
<!-- Semantic structure -->
|
||||
<main>
|
||||
<h1>Main Heading</h1>
|
||||
<section>
|
||||
<h2>Section Heading</h2>
|
||||
<p>Content</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Proper form labels -->
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email">
|
||||
|
||||
<!-- Grouped controls -->
|
||||
<fieldset>
|
||||
<legend>Contact preferences</legend>
|
||||
<label><input type="checkbox" name="email"> Email</label>
|
||||
<label><input type="checkbox" name="phone"> Phone</label>
|
||||
</fieldset>
|
||||
```
|
||||
|
||||
**1.3.2 Meaningful Sequence (Level A)**
|
||||
- [ ] Reading order is logical
|
||||
- [ ] Tab order follows visual flow
|
||||
- [ ] CSS positioning doesn't disrupt reading order
|
||||
|
||||
**1.3.3 Sensory Characteristics (Level A)**
|
||||
- [ ] Instructions don't rely solely on shape
|
||||
- [ ] Instructions don't rely solely on size
|
||||
- [ ] Instructions don't rely solely on location
|
||||
- [ ] Instructions don't rely solely on sound
|
||||
|
||||
```html
|
||||
<!-- ❌ Bad -->
|
||||
<p>Click the blue button on the right</p>
|
||||
|
||||
<!-- ✅ Good -->
|
||||
<p>Click the "Submit" button to continue</p>
|
||||
```
|
||||
|
||||
**1.3.4 Orientation (Level AA)**
|
||||
- [ ] Content works in portrait and landscape
|
||||
- [ ] No orientation restrictions unless essential
|
||||
|
||||
**1.3.5 Identify Input Purpose (Level AA)**
|
||||
- [ ] Input fields use autocomplete attribute when appropriate
|
||||
|
||||
```html
|
||||
<input type="email" name="email" autocomplete="email">
|
||||
<input type="tel" name="phone" autocomplete="tel">
|
||||
<input type="text" name="address" autocomplete="street-address">
|
||||
```
|
||||
|
||||
### 1.4 Distinguishable
|
||||
|
||||
**1.4.1 Use of Color (Level A)**
|
||||
- [ ] Color not used as only visual means of conveying information
|
||||
- [ ] Color not used as only way to distinguish interactive elements
|
||||
- [ ] Links are distinguishable without color alone
|
||||
|
||||
```css
|
||||
/* ✅ Good - underline + color */
|
||||
a {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Or use icons, borders, etc. */
|
||||
.error {
|
||||
color: red;
|
||||
border-left: 4px solid red;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.error::before {
|
||||
content: "⚠ ";
|
||||
}
|
||||
```
|
||||
|
||||
**1.4.2 Audio Control (Level A)**
|
||||
- [ ] Auto-playing audio can be paused
|
||||
- [ ] Auto-playing audio stops after 3 seconds
|
||||
- [ ] Volume controls available
|
||||
|
||||
**1.4.3 Contrast (Minimum) (Level AA)**
|
||||
- [ ] Normal text: 4.5:1 contrast ratio
|
||||
- [ ] Large text (18pt+): 3:1 contrast ratio
|
||||
- [ ] UI components: 3:1 contrast ratio
|
||||
- [ ] Graphical objects: 3:1 contrast ratio
|
||||
|
||||
```css
|
||||
/* Check with contrast checkers */
|
||||
.text {
|
||||
color: #595959; /* 7:1 on white ✅ */
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #FFFFFF;
|
||||
background: #0066FF; /* 4.6:1 ✅ */
|
||||
border: 2px solid #0052CC; /* 3:1 ✅ */
|
||||
}
|
||||
```
|
||||
|
||||
**1.4.4 Resize Text (Level AA)**
|
||||
- [ ] Text can be resized to 200% without loss of content
|
||||
- [ ] No horizontal scrolling at 200% zoom
|
||||
- [ ] Use relative units (rem, em)
|
||||
|
||||
```css
|
||||
/* ✅ Good */
|
||||
body {
|
||||
font-size: 1rem; /* Respects user preferences */
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem; /* Scales with body */
|
||||
}
|
||||
|
||||
/* ❌ Avoid */
|
||||
.text {
|
||||
font-size: 14px; /* Fixed size */
|
||||
}
|
||||
```
|
||||
|
||||
**1.4.5 Images of Text (Level AA)**
|
||||
- [ ] Text is text, not images
|
||||
- [ ] Exception: logos, essential presentations
|
||||
|
||||
**1.4.10 Reflow (Level AA)**
|
||||
- [ ] Content reflows at 320px viewport width
|
||||
- [ ] No horizontal scrolling (except tables, diagrams)
|
||||
- [ ] Responsive design implemented
|
||||
|
||||
```css
|
||||
/* Mobile-first responsive */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**1.4.11 Non-text Contrast (Level AA)**
|
||||
- [ ] UI components: 3:1 contrast against background
|
||||
- [ ] Graphical objects: 3:1 contrast
|
||||
- [ ] Focus indicators: 3:1 contrast
|
||||
|
||||
**1.4.12 Text Spacing (Level AA)**
|
||||
- [ ] Content adapts to increased text spacing
|
||||
- [ ] Line height: at least 1.5x font size
|
||||
- [ ] Paragraph spacing: at least 2x font size
|
||||
- [ ] Letter spacing: at least 0.12x font size
|
||||
- [ ] Word spacing: at least 0.16x font size
|
||||
|
||||
```css
|
||||
/* Ensure content doesn't break */
|
||||
body {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
```
|
||||
|
||||
**1.4.13 Content on Hover or Focus (Level AA)**
|
||||
- [ ] Additional content (tooltips, dropdowns) is dismissible
|
||||
- [ ] Hoverable content stays visible when hovering over it
|
||||
- [ ] Content remains visible until dismissed or no longer relevant
|
||||
|
||||
```css
|
||||
/* Tooltip stays visible when hovering over it */
|
||||
.tooltip:hover .tooltip-content,
|
||||
.tooltip .tooltip-content:hover {
|
||||
display: block;
|
||||
}
|
||||
```
|
||||
|
||||
## Operable
|
||||
|
||||
User interface components and navigation must be operable.
|
||||
|
||||
### 2.1 Keyboard Accessible
|
||||
|
||||
**2.1.1 Keyboard (Level A)**
|
||||
- [ ] All functionality available via keyboard
|
||||
- [ ] No keyboard traps
|
||||
- [ ] Logical tab order
|
||||
- [ ] Custom controls are keyboard accessible
|
||||
|
||||
```html
|
||||
<!-- Custom button needs tabindex and keyboard handlers -->
|
||||
<div role="button" tabindex="0"
|
||||
onclick="handleClick()"
|
||||
onkeydown="if(event.key==='Enter'||event.key===' ') handleClick()">
|
||||
Click me
|
||||
</div>
|
||||
```
|
||||
|
||||
**2.1.2 No Keyboard Trap (Level A)**
|
||||
- [ ] Focus can move away from all components
|
||||
- [ ] Instructions provided if non-standard exit method
|
||||
|
||||
**2.1.4 Character Key Shortcuts (Level A)**
|
||||
- [ ] Single-key shortcuts can be turned off
|
||||
- [ ] Or remapped by user
|
||||
- [ ] Or only active when component has focus
|
||||
|
||||
### 2.2 Enough Time
|
||||
|
||||
**2.2.1 Timing Adjustable (Level A)**
|
||||
- [ ] Time limits can be turned off, adjusted, or extended
|
||||
- [ ] User warned before time expires
|
||||
- [ ] At least 20 seconds to extend
|
||||
|
||||
**2.2.2 Pause, Stop, Hide (Level A)**
|
||||
- [ ] Moving content can be paused
|
||||
- [ ] Auto-updating content can be paused/stopped
|
||||
- [ ] Blinking content can be stopped
|
||||
|
||||
```html
|
||||
<!-- Provide controls -->
|
||||
<div class="carousel">
|
||||
<button aria-label="Pause carousel">⏸</button>
|
||||
<button aria-label="Play carousel">▶</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2.3 Seizures and Physical Reactions
|
||||
|
||||
**2.3.1 Three Flashes or Below Threshold (Level A)**
|
||||
- [ ] No content flashes more than 3 times per second
|
||||
- [ ] Or flashes are below general flash/red flash thresholds
|
||||
|
||||
### 2.4 Navigable
|
||||
|
||||
**2.4.1 Bypass Blocks (Level A)**
|
||||
- [ ] Skip navigation link provided
|
||||
- [ ] Landmark regions defined
|
||||
- [ ] Headings structure content
|
||||
|
||||
```html
|
||||
<!-- Skip link (visually hidden until focused) -->
|
||||
<a href="#main" class="skip-link">Skip to main content</a>
|
||||
|
||||
<header>
|
||||
<nav aria-label="Main navigation">...</nav>
|
||||
</header>
|
||||
|
||||
<main id="main">
|
||||
<h1>Page Title</h1>
|
||||
</main>
|
||||
```
|
||||
|
||||
```css
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
```
|
||||
|
||||
**2.4.2 Page Titled (Level A)**
|
||||
- [ ] Page has descriptive title
|
||||
- [ ] Title identifies page content
|
||||
|
||||
```html
|
||||
<title>Contact Us - Company Name</title>
|
||||
```
|
||||
|
||||
**2.4.3 Focus Order (Level A)**
|
||||
- [ ] Focus order is logical and intuitive
|
||||
- [ ] Matches visual order
|
||||
- [ ] No positive tabindex values
|
||||
|
||||
```css
|
||||
/* ❌ Avoid */
|
||||
.element { tabindex: 5; }
|
||||
|
||||
/* ✅ Use */
|
||||
.element { tabindex: 0; } /* In natural order */
|
||||
.element { tabindex: -1; } /* Programmatic focus only */
|
||||
```
|
||||
|
||||
**2.4.4 Link Purpose (Level A)**
|
||||
- [ ] Link text describes destination
|
||||
- [ ] Context is clear
|
||||
- [ ] Avoid "click here" or "read more"
|
||||
|
||||
```html
|
||||
<!-- ❌ Bad -->
|
||||
<a href="/report.pdf">Click here</a>
|
||||
|
||||
<!-- ✅ Good -->
|
||||
<a href="/report.pdf">Download 2024 Annual Report (PDF, 2MB)</a>
|
||||
```
|
||||
|
||||
**2.4.5 Multiple Ways (Level AA)**
|
||||
- [ ] Multiple ways to find pages (menu, search, sitemap)
|
||||
- [ ] Exception: pages that are steps in a process
|
||||
|
||||
**2.4.6 Headings and Labels (Level AA)**
|
||||
- [ ] Headings describe content
|
||||
- [ ] Labels describe purpose
|
||||
- [ ] Headings and labels are clear
|
||||
|
||||
**2.4.7 Focus Visible (Level AA)**
|
||||
- [ ] Keyboard focus indicator is visible
|
||||
- [ ] Sufficient contrast (3:1)
|
||||
- [ ] Clearly indicates focused element
|
||||
|
||||
```css
|
||||
/* ✅ Strong focus indicator */
|
||||
*:focus-visible {
|
||||
outline: 2px solid #0066FF;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Or custom focus styles */
|
||||
button:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.5);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 Input Modalities
|
||||
|
||||
**2.5.1 Pointer Gestures (Level A)**
|
||||
- [ ] Complex gestures have single-pointer alternative
|
||||
- [ ] Path-based gestures have simple alternative
|
||||
|
||||
**2.5.2 Pointer Cancellation (Level A)**
|
||||
- [ ] Actions triggered on up-event (not down)
|
||||
- [ ] Or can be aborted/undone
|
||||
|
||||
**2.5.3 Label in Name (Level A)**
|
||||
- [ ] Visible label matches accessible name
|
||||
- [ ] Accessible name starts with visible text
|
||||
|
||||
```html
|
||||
<!-- ✅ Good - matches -->
|
||||
<button aria-label="Submit form">Submit</button>
|
||||
|
||||
<!-- ❌ Bad - doesn't match -->
|
||||
<button aria-label="Send">Submit</button>
|
||||
```
|
||||
|
||||
**2.5.4 Motion Actuation (Level A)**
|
||||
- [ ] Device motion triggers have UI alternative
|
||||
- [ ] Can disable motion actuation
|
||||
|
||||
## Understandable
|
||||
|
||||
Information and user interface operation must be understandable.
|
||||
|
||||
### 3.1 Readable
|
||||
|
||||
**3.1.1 Language of Page (Level A)**
|
||||
- [ ] Page language is identified
|
||||
|
||||
```html
|
||||
<html lang="en">
|
||||
```
|
||||
|
||||
**3.1.2 Language of Parts (Level AA)**
|
||||
- [ ] Language changes are marked
|
||||
|
||||
```html
|
||||
<p>The French phrase <span lang="fr">c'est la vie</span> means "that's life".</p>
|
||||
```
|
||||
|
||||
### 3.2 Predictable
|
||||
|
||||
**3.2.1 On Focus (Level A)**
|
||||
- [ ] Focusing an element doesn't trigger context change
|
||||
- [ ] No automatic form submission on focus
|
||||
|
||||
**3.2.2 On Input (Level A)**
|
||||
- [ ] Changing settings doesn't automatically cause context change
|
||||
- [ ] User is warned of automatic changes
|
||||
|
||||
**3.2.3 Consistent Navigation (Level AA)**
|
||||
- [ ] Navigation order is consistent across pages
|
||||
- [ ] Repeated navigation in same order
|
||||
|
||||
**3.2.4 Consistent Identification (Level AA)**
|
||||
- [ ] Components with same functionality are identified consistently
|
||||
- [ ] Icons mean the same thing throughout
|
||||
|
||||
### 3.3 Input Assistance
|
||||
|
||||
**3.3.1 Error Identification (Level A)**
|
||||
- [ ] Errors are identified in text
|
||||
- [ ] Error is described to user
|
||||
|
||||
```html
|
||||
<input type="email" aria-invalid="true" aria-describedby="email-error">
|
||||
<span id="email-error" role="alert">Please enter a valid email address</span>
|
||||
```
|
||||
|
||||
**3.3.2 Labels or Instructions (Level A)**
|
||||
- [ ] Labels provided for input
|
||||
- [ ] Instructions provided when needed
|
||||
|
||||
```html
|
||||
<label for="password">
|
||||
Password (must be at least 8 characters)
|
||||
</label>
|
||||
<input type="password" id="password" required minlength="8">
|
||||
```
|
||||
|
||||
**3.3.3 Error Suggestion (Level AA)**
|
||||
- [ ] Errors suggest how to fix
|
||||
- [ ] Specific, actionable feedback
|
||||
|
||||
```html
|
||||
<span role="alert">
|
||||
Password must contain at least one uppercase letter,
|
||||
one number, and be at least 8 characters long.
|
||||
</span>
|
||||
```
|
||||
|
||||
**3.3.4 Error Prevention (Level AA)**
|
||||
- [ ] Legal/financial transactions are reversible
|
||||
- [ ] Data is checked and confirmed before submission
|
||||
- [ ] User can review and correct before submitting
|
||||
|
||||
```html
|
||||
<!-- Confirmation step -->
|
||||
<div role="region" aria-labelledby="review-heading">
|
||||
<h2 id="review-heading">Review Your Order</h2>
|
||||
<!-- Show all details -->
|
||||
<button>Edit Order</button>
|
||||
<button>Confirm Purchase</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Robust
|
||||
|
||||
Content must be robust enough to be interpreted by a wide variety of user agents, including assistive technologies.
|
||||
|
||||
### 4.1 Compatible
|
||||
|
||||
**4.1.1 Parsing (Level A)** *[Obsolete in WCAG 2.2]*
|
||||
- [ ] Valid HTML (no duplicate IDs, proper nesting)
|
||||
|
||||
**4.1.2 Name, Role, Value (Level A)**
|
||||
- [ ] All UI components have accessible name
|
||||
- [ ] Roles are appropriate
|
||||
- [ ] States communicated to assistive tech
|
||||
|
||||
```html
|
||||
<!-- Custom checkbox -->
|
||||
<div role="checkbox"
|
||||
aria-checked="false"
|
||||
aria-labelledby="label-id"
|
||||
tabindex="0">
|
||||
</div>
|
||||
<span id="label-id">Accept terms</span>
|
||||
|
||||
<!-- Button states -->
|
||||
<button aria-pressed="false" aria-label="Mute">🔊</button>
|
||||
<button aria-pressed="true" aria-label="Mute">🔇</button>
|
||||
```
|
||||
|
||||
**4.1.3 Status Messages (Level AA)**
|
||||
- [ ] Status messages can be perceived by assistive tech
|
||||
- [ ] Use aria-live, role="status", role="alert"
|
||||
|
||||
```html
|
||||
<!-- Success message -->
|
||||
<div role="status" aria-live="polite">
|
||||
Form submitted successfully!
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<div role="alert" aria-live="assertive">
|
||||
Error: Connection lost. Please try again.
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div aria-live="polite" aria-busy="true">
|
||||
Loading content...
|
||||
</div>
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Automated Testing
|
||||
- [ ] Run axe DevTools
|
||||
- [ ] Run Lighthouse accessibility audit
|
||||
- [ ] Run WAVE browser extension
|
||||
- [ ] HTML validator (W3C)
|
||||
- [ ] Color contrast checker
|
||||
|
||||
### Manual Testing
|
||||
- [ ] Keyboard-only navigation
|
||||
- [ ] Screen reader testing (NVDA, JAWS, VoiceOver)
|
||||
- [ ] Zoom to 200% (text resize)
|
||||
- [ ] Test with browser zoom (page zoom)
|
||||
- [ ] Test in high contrast mode
|
||||
- [ ] Test with dark mode
|
||||
- [ ] Test responsive breakpoints
|
||||
|
||||
### Screen Reader Testing
|
||||
- [ ] NVDA (Windows, free)
|
||||
- [ ] JAWS (Windows, paid)
|
||||
- [ ] VoiceOver (Mac/iOS, built-in)
|
||||
- [ ] TalkBack (Android, built-in)
|
||||
- [ ] ORCA (Linux, free)
|
||||
|
||||
### Browser Testing
|
||||
- [ ] Chrome + screen reader
|
||||
- [ ] Firefox + screen reader
|
||||
- [ ] Safari + VoiceOver
|
||||
- [ ] Edge + screen reader
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Critical Items (Must Fix)
|
||||
1. ✅ Images have alt text
|
||||
2. ✅ Form inputs have labels
|
||||
3. ✅ Sufficient color contrast (4.5:1 text, 3:1 UI)
|
||||
4. ✅ Keyboard accessible (all functionality)
|
||||
5. ✅ Focus indicators visible
|
||||
6. ✅ No keyboard traps
|
||||
7. ✅ Semantic HTML (headings, landmarks)
|
||||
8. ✅ ARIA used correctly
|
||||
9. ✅ Page has title
|
||||
10. ✅ Language identified
|
||||
|
||||
### Tools
|
||||
- [axe DevTools](https://www.deque.com/axe/devtools/)
|
||||
- [Lighthouse](https://developers.google.com/web/tools/lighthouse)
|
||||
- [WAVE](https://wave.webaim.org/)
|
||||
- [Color Contrast Analyzer](https://www.tpgi.com/color-contrast-checker/)
|
||||
- [NVDA Screen Reader](https://www.nvaccess.org/)
|
||||
|
||||
### Resources
|
||||
- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [WebAIM](https://webaim.org/)
|
||||
- [The A11Y Project](https://www.a11yproject.com/)
|
||||
- [MDN Accessibility](https://developer.mozilla.org/en-US/docs/Web/Accessibility)
|
||||
|
||||
---
|
||||
|
||||
**"Accessibility is not a feature, it's a fundamental right."**
|
||||
937
skills/frontend-designer/references/component_library.md
Normal file
937
skills/frontend-designer/references/component_library.md
Normal file
@@ -0,0 +1,937 @@
|
||||
# Component Library Architecture
|
||||
|
||||
Complete guide to building, organizing, and maintaining a scalable component library.
|
||||
|
||||
## Component Architecture Principles
|
||||
|
||||
### 1. Atomic Design
|
||||
|
||||
Organize components hierarchically from smallest to largest.
|
||||
|
||||
**Hierarchy:**
|
||||
```
|
||||
Atoms → Molecules → Organisms → Templates → Pages
|
||||
```
|
||||
|
||||
**Example Structure:**
|
||||
```
|
||||
components/
|
||||
├── atoms/
|
||||
│ ├── Button/
|
||||
│ ├── Input/
|
||||
│ ├── Icon/
|
||||
│ └── Label/
|
||||
├── molecules/
|
||||
│ ├── FormField/
|
||||
│ ├── SearchBar/
|
||||
│ └── Card/
|
||||
├── organisms/
|
||||
│ ├── Header/
|
||||
│ ├── LoginForm/
|
||||
│ └── ProductCard/
|
||||
├── templates/
|
||||
│ ├── DashboardLayout/
|
||||
│ └── AuthLayout/
|
||||
└── pages/
|
||||
├── HomePage/
|
||||
└── ProfilePage/
|
||||
```
|
||||
|
||||
### 2. Component Types
|
||||
|
||||
**Presentational (Dumb) Components:**
|
||||
- Focus on how things look
|
||||
- No state management
|
||||
- Receive data via props
|
||||
- Highly reusable
|
||||
|
||||
```tsx
|
||||
// Presentational Button
|
||||
interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
children,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className={`btn btn--${variant} btn--${size}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Container (Smart) Components:**
|
||||
- Focus on how things work
|
||||
- Manage state
|
||||
- Connect to data sources
|
||||
- Use presentational components
|
||||
|
||||
```tsx
|
||||
// Container Component
|
||||
export const UserProfileContainer: React.FC = () => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUser().then(data => {
|
||||
setUser(data);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (!user) return <ErrorMessage />;
|
||||
|
||||
return <UserProfile user={user} />;
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Component Composition
|
||||
|
||||
**Build complex components from simple ones.**
|
||||
|
||||
```tsx
|
||||
// Atoms
|
||||
const Avatar: React.FC<AvatarProps> = ({ src, alt }) => (
|
||||
<img className="avatar" src={src} alt={alt} />
|
||||
);
|
||||
|
||||
const Badge: React.FC<BadgeProps> = ({ children }) => (
|
||||
<span className="badge">{children}</span>
|
||||
);
|
||||
|
||||
// Molecule (composed of atoms)
|
||||
const UserBadge: React.FC<UserBadgeProps> = ({ user }) => (
|
||||
<div className="user-badge">
|
||||
<Avatar src={user.avatar} alt={user.name} />
|
||||
<span className="user-badge__name">{user.name}</span>
|
||||
<Badge>{user.role}</Badge>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Organism (composed of molecules)
|
||||
const UserList: React.FC<UserListProps> = ({ users }) => (
|
||||
<ul className="user-list">
|
||||
{users.map(user => (
|
||||
<li key={user.id}>
|
||||
<UserBadge user={user} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
### Standard Component Structure
|
||||
|
||||
```
|
||||
Button/
|
||||
├── Button.tsx # Main component
|
||||
├── Button.test.tsx # Tests
|
||||
├── Button.stories.tsx # Storybook stories
|
||||
├── Button.module.css # Styles (CSS Modules)
|
||||
├── Button.types.ts # TypeScript types
|
||||
└── index.ts # Barrel export
|
||||
```
|
||||
|
||||
### Example Files
|
||||
|
||||
**Button/Button.tsx**
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import styles from './Button.module.css';
|
||||
import { ButtonProps } from './Button.types';
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
children,
|
||||
onClick,
|
||||
...props
|
||||
}) => {
|
||||
const className = [
|
||||
styles.button,
|
||||
styles[`button--${variant}`],
|
||||
styles[`button--${size}`],
|
||||
disabled && styles['button--disabled'],
|
||||
loading && styles['button--loading'],
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
disabled={disabled || loading}
|
||||
onClick={onClick}
|
||||
aria-busy={loading}
|
||||
{...props}
|
||||
>
|
||||
{loading && <span className={styles.spinner} />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Button/Button.types.ts**
|
||||
```tsx
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
/** Visual style variant */
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
|
||||
/** Size variant */
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
|
||||
/** Disabled state */
|
||||
disabled?: boolean;
|
||||
|
||||
/** Loading state */
|
||||
loading?: boolean;
|
||||
|
||||
/** Button content */
|
||||
children: React.ReactNode;
|
||||
|
||||
/** Click handler */
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Button/Button.test.tsx**
|
||||
```tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { Button } from './Button';
|
||||
|
||||
describe('Button', () => {
|
||||
it('renders children correctly', () => {
|
||||
render(<Button>Click me</Button>);
|
||||
expect(screen.getByText('Click me')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles click events', () => {
|
||||
const handleClick = jest.fn();
|
||||
render(<Button onClick={handleClick}>Click me</Button>);
|
||||
|
||||
fireEvent.click(screen.getByText('Click me'));
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('applies variant classes', () => {
|
||||
render(<Button variant="secondary">Click me</Button>);
|
||||
const button = screen.getByText('Click me');
|
||||
expect(button).toHaveClass('button--secondary');
|
||||
});
|
||||
|
||||
it('disables interaction when loading', () => {
|
||||
const handleClick = jest.fn();
|
||||
render(<Button loading onClick={handleClick}>Click me</Button>);
|
||||
|
||||
const button = screen.getByText('Click me');
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveAttribute('aria-busy', 'true');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Button/Button.stories.tsx**
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Button } from './Button';
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary', 'ghost', 'danger'],
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['sm', 'md', 'lg'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Button>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
children: 'Primary Button',
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
variant: 'secondary',
|
||||
children: 'Secondary Button',
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
loading: true,
|
||||
children: 'Loading...',
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
children: 'Disabled Button',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Button/index.ts**
|
||||
```tsx
|
||||
export { Button } from './Button';
|
||||
export type { ButtonProps } from './Button.types';
|
||||
```
|
||||
|
||||
## Common Components
|
||||
|
||||
### 1. Button Component
|
||||
|
||||
```tsx
|
||||
interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
iconPosition?: 'left' | 'right';
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Input Component
|
||||
|
||||
```tsx
|
||||
interface InputProps {
|
||||
type?: 'text' | 'email' | 'password' | 'number' | 'tel';
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
error?: string;
|
||||
helpText?: string;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export const Input: React.FC<InputProps> = ({
|
||||
type = 'text',
|
||||
label,
|
||||
placeholder,
|
||||
value,
|
||||
error,
|
||||
helpText,
|
||||
required,
|
||||
disabled,
|
||||
onChange,
|
||||
}) => {
|
||||
const id = useId();
|
||||
const errorId = `${id}-error`;
|
||||
const helpId = `${id}-help`;
|
||||
|
||||
return (
|
||||
<div className="input-wrapper">
|
||||
{label && (
|
||||
<label htmlFor={id} className="input-label">
|
||||
{label}
|
||||
{required && <span aria-label="required">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<input
|
||||
id={id}
|
||||
type={type}
|
||||
className={`input ${error ? 'input--error' : ''}`}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={error ? errorId : helpText ? helpId : undefined}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<span id={errorId} className="input-error" role="alert">
|
||||
{error}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{helpText && !error && (
|
||||
<span id={helpId} className="input-help">
|
||||
{helpText}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Card Component
|
||||
|
||||
```tsx
|
||||
interface CardProps {
|
||||
variant?: 'default' | 'outlined' | 'elevated';
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||
clickable?: boolean;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const Card: React.FC<CardProps> = ({
|
||||
variant = 'default',
|
||||
padding = 'md',
|
||||
clickable = false,
|
||||
children,
|
||||
onClick,
|
||||
}) => {
|
||||
const Component = clickable ? 'button' : 'div';
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={`card card--${variant} card--padding-${padding}`}
|
||||
onClick={onClick}
|
||||
role={clickable ? 'button' : undefined}
|
||||
tabIndex={clickable ? 0 : undefined}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Modal Component
|
||||
|
||||
```tsx
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
title?: string;
|
||||
size?: 'sm' | 'md' | 'lg' | 'full';
|
||||
children: React.ReactNode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const Modal: React.FC<ModalProps> = ({
|
||||
open,
|
||||
title,
|
||||
size = 'md',
|
||||
children,
|
||||
onClose,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// Trap focus in modal
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return createPortal(
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div
|
||||
className={`modal modal--${size}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={title ? 'modal-title' : undefined}
|
||||
>
|
||||
{title && (
|
||||
<div className="modal__header">
|
||||
<h2 id="modal-title">{title}</h2>
|
||||
<button
|
||||
className="modal__close"
|
||||
onClick={onClose}
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="modal__body">{children}</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Dropdown Component
|
||||
|
||||
```tsx
|
||||
interface DropdownProps {
|
||||
trigger: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
align?: 'left' | 'right';
|
||||
}
|
||||
|
||||
export const Dropdown: React.FC<DropdownProps> = ({
|
||||
trigger,
|
||||
children,
|
||||
align = 'left',
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="dropdown" ref={dropdownRef}>
|
||||
<button
|
||||
className="dropdown__trigger"
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
{trigger}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className={`dropdown__menu dropdown__menu--${align}`} role="menu">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### 1. Compound Components
|
||||
|
||||
**Allow components to work together while sharing implicit state.**
|
||||
|
||||
```tsx
|
||||
// Context for shared state
|
||||
const AccordionContext = createContext<{
|
||||
activeIndex: number | null;
|
||||
setActiveIndex: (index: number | null) => void;
|
||||
} | null>(null);
|
||||
|
||||
// Parent component
|
||||
export const Accordion: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={{ activeIndex, setActiveIndex }}>
|
||||
<div className="accordion">{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Child component
|
||||
export const AccordionItem: React.FC<{
|
||||
index: number;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}> = ({ index, title, children }) => {
|
||||
const context = useContext(AccordionContext);
|
||||
if (!context) throw new Error('AccordionItem must be used within Accordion');
|
||||
|
||||
const { activeIndex, setActiveIndex } = context;
|
||||
const isOpen = activeIndex === index;
|
||||
|
||||
return (
|
||||
<div className="accordion-item">
|
||||
<button
|
||||
className="accordion-item__header"
|
||||
onClick={() => setActiveIndex(isOpen ? null : index)}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
|
||||
{isOpen && <div className="accordion-item__content">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Usage
|
||||
<Accordion>
|
||||
<AccordionItem index={0} title="Section 1">Content 1</AccordionItem>
|
||||
<AccordionItem index={1} title="Section 2">Content 2</AccordionItem>
|
||||
</Accordion>
|
||||
```
|
||||
|
||||
### 2. Render Props
|
||||
|
||||
**Pass rendering logic as a prop.**
|
||||
|
||||
```tsx
|
||||
interface DataFetcherProps<T> {
|
||||
url: string;
|
||||
children: (data: {
|
||||
data: T | null;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}) => React.ReactNode;
|
||||
}
|
||||
|
||||
export function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
|
||||
const [data, setData] = useState<T | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(setData)
|
||||
.catch(setError)
|
||||
.finally(() => setLoading(false));
|
||||
}, [url]);
|
||||
|
||||
return <>{children({ data, loading, error })}</>;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<DataFetcher<User> url="/api/user">
|
||||
{({ data, loading, error }) => {
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <ErrorMessage error={error} />;
|
||||
if (!data) return null;
|
||||
return <UserProfile user={data} />;
|
||||
}}
|
||||
</DataFetcher>
|
||||
```
|
||||
|
||||
### 3. Custom Hooks Pattern
|
||||
|
||||
**Extract reusable logic into custom hooks.**
|
||||
|
||||
```tsx
|
||||
// useToggle hook
|
||||
export function useToggle(initialValue = false) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const toggle = useCallback(() => setValue(v => !v), []);
|
||||
const setTrue = useCallback(() => setValue(true), []);
|
||||
const setFalse = useCallback(() => setValue(false), []);
|
||||
|
||||
return { value, toggle, setTrue, setFalse };
|
||||
}
|
||||
|
||||
// Usage in component
|
||||
export const Modal: React.FC<ModalProps> = ({ children }) => {
|
||||
const { value: isOpen, setTrue: open, setFalse: close } = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={open}>Open Modal</button>
|
||||
{isOpen && (
|
||||
<div className="modal">
|
||||
{children}
|
||||
<button onClick={close}>Close</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Styling Strategies
|
||||
|
||||
### 1. CSS Modules
|
||||
|
||||
```css
|
||||
/* Button.module.css */
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.button--primary {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
border: 1px solid var(--color-primary);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
import styles from './Button.module.css';
|
||||
|
||||
export const Button = ({ variant }) => (
|
||||
<button className={`${styles.button} ${styles[`button--${variant}`]}`}>
|
||||
Click me
|
||||
</button>
|
||||
);
|
||||
```
|
||||
|
||||
### 2. CSS-in-JS (Styled Components)
|
||||
|
||||
```tsx
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledButton = styled.button<{ variant: string }>`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: ${({ theme }) => theme.space[3]} ${({ theme }) => theme.space[4]};
|
||||
background: ${({ theme, variant }) =>
|
||||
variant === 'primary' ? theme.colors.primary : 'transparent'};
|
||||
color: ${({ theme, variant }) =>
|
||||
variant === 'primary' ? theme.colors.white : theme.colors.primary};
|
||||
`;
|
||||
|
||||
export const Button = ({ variant, children }) => (
|
||||
<StyledButton variant={variant}>{children}</StyledButton>
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Tailwind CSS
|
||||
|
||||
```tsx
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({ variant, size, children }) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'inline-flex items-center rounded-md font-medium',
|
||||
{
|
||||
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
|
||||
'bg-transparent text-blue-600 border border-blue-600': variant === 'secondary',
|
||||
},
|
||||
{
|
||||
'px-3 py-2 text-sm': size === 'sm',
|
||||
'px-4 py-2 text-base': size === 'md',
|
||||
'px-6 py-3 text-lg': size === 'lg',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Component Documentation Template
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* Button component for triggering actions and navigation.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Button variant="primary" onClick={handleClick}>
|
||||
* Click me
|
||||
* </Button>
|
||||
* ```
|
||||
*
|
||||
* @see {@link https://design-system.example.com/button | Design System Docs}
|
||||
*/
|
||||
export const Button: React.FC<ButtonProps> = ({ ... }) => {
|
||||
// Implementation
|
||||
};
|
||||
```
|
||||
|
||||
### Storybook Documentation
|
||||
|
||||
```tsx
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { Button } from './Button';
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
Button component for triggering actions.
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`tsx
|
||||
import { Button } from '@/components/Button';
|
||||
|
||||
<Button variant="primary">Click me</Button>
|
||||
\`\`\`
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Keyboard accessible
|
||||
- Screen reader friendly
|
||||
- WCAG 2.1 AA compliant
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Testing Strategies
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```tsx
|
||||
describe('Button', () => {
|
||||
it('renders with correct variant', () => {
|
||||
render(<Button variant="primary">Test</Button>);
|
||||
expect(screen.getByRole('button')).toHaveClass('button--primary');
|
||||
});
|
||||
|
||||
it('calls onClick when clicked', () => {
|
||||
const handleClick = jest.fn();
|
||||
render(<Button onClick={handleClick}>Test</Button>);
|
||||
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Accessibility Tests
|
||||
|
||||
```tsx
|
||||
import { axe, toHaveNoViolations } from 'jest-axe';
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
it('has no accessibility violations', async () => {
|
||||
const { container } = render(<Button>Test</Button>);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
```
|
||||
|
||||
### Visual Regression Tests
|
||||
|
||||
```tsx
|
||||
// Using Chromatic or Percy
|
||||
it('matches snapshot', () => {
|
||||
const { container } = render(<Button variant="primary">Test</Button>);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Code Splitting
|
||||
|
||||
```tsx
|
||||
// Lazy load heavy components
|
||||
const HeavyComponent = lazy(() => import('./HeavyComponent'));
|
||||
|
||||
export const App = () => (
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<HeavyComponent />
|
||||
</Suspense>
|
||||
);
|
||||
```
|
||||
|
||||
### Memoization
|
||||
|
||||
```tsx
|
||||
// Memoize expensive components
|
||||
export const ExpensiveComponent = memo(({ data }) => {
|
||||
// Expensive rendering logic
|
||||
return <div>{processData(data)}</div>;
|
||||
});
|
||||
|
||||
// Memoize callbacks
|
||||
const handleClick = useCallback(() => {
|
||||
// Handle click
|
||||
}, [dependencies]);
|
||||
|
||||
// Memoize values
|
||||
const expensiveValue = useMemo(() => {
|
||||
return computeExpensiveValue(data);
|
||||
}, [data]);
|
||||
```
|
||||
|
||||
## Version Control
|
||||
|
||||
### Semantic Versioning
|
||||
|
||||
```
|
||||
MAJOR.MINOR.PATCH
|
||||
|
||||
1.0.0 → Initial release
|
||||
1.1.0 → New feature (backwards compatible)
|
||||
1.1.1 → Bug fix (backwards compatible)
|
||||
2.0.0 → Breaking change
|
||||
```
|
||||
|
||||
### Changelog
|
||||
|
||||
```markdown
|
||||
# Changelog
|
||||
|
||||
## [2.0.0] - 2024-01-15
|
||||
|
||||
### Breaking Changes
|
||||
- Removed `type` prop from Button (use `variant` instead)
|
||||
|
||||
### Added
|
||||
- New `loading` state for Button
|
||||
- Icon support in Button component
|
||||
|
||||
### Fixed
|
||||
- Button focus indicator contrast ratio
|
||||
|
||||
## [1.1.0] - 2024-01-01
|
||||
|
||||
### Added
|
||||
- New Input component
|
||||
- Card component variants
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [React Component Patterns](https://kentcdodds.com/blog/compound-components-with-react-hooks)
|
||||
- [Design Systems Handbook](https://www.designbetter.co/design-systems-handbook)
|
||||
- [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/)
|
||||
- [Storybook Best Practices](https://storybook.js.org/docs/react/writing-docs/introduction)
|
||||
|
||||
---
|
||||
|
||||
**"Good components are reusable, accessible, and well-documented."**
|
||||
608
skills/frontend-designer/references/design_tokens.md
Normal file
608
skills/frontend-designer/references/design_tokens.md
Normal file
@@ -0,0 +1,608 @@
|
||||
# Design Tokens Reference
|
||||
|
||||
Complete guide to implementing and using design tokens in your design system.
|
||||
|
||||
## What Are Design Tokens?
|
||||
|
||||
Design tokens are the visual design atoms of your design system — specifically, they are named entities that store visual design attributes. They're used in place of hard-coded values to maintain a scalable and consistent visual system.
|
||||
|
||||
## Benefits
|
||||
|
||||
**Consistency**
|
||||
- Single source of truth for design values
|
||||
- Ensures brand consistency across platforms
|
||||
- Easy to maintain and update
|
||||
|
||||
**Scalability**
|
||||
- Change values in one place, update everywhere
|
||||
- Support multiple themes (light/dark)
|
||||
- Platform-agnostic (web, iOS, Android)
|
||||
|
||||
**Developer-Designer Communication**
|
||||
- Shared vocabulary between teams
|
||||
- Design decisions are codified
|
||||
- Reduces ambiguity
|
||||
|
||||
## Token Categories
|
||||
|
||||
### 1. Color Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Brand Colors */
|
||||
--color-primary: #0066FF;
|
||||
--color-primary-hover: #0052CC;
|
||||
--color-primary-active: #003D99;
|
||||
--color-primary-subtle: #E6F0FF;
|
||||
|
||||
--color-secondary: #6B7280;
|
||||
--color-secondary-hover: #4B5563;
|
||||
|
||||
/* Semantic Colors */
|
||||
--color-success: #10B981;
|
||||
--color-warning: #F59E0B;
|
||||
--color-error: #EF4444;
|
||||
--color-info: #3B82F6;
|
||||
|
||||
/* Neutral Palette */
|
||||
--color-white: #FFFFFF;
|
||||
--color-black: #000000;
|
||||
--color-gray-50: #F9FAFB;
|
||||
--color-gray-100: #F3F4F6;
|
||||
/* ... through gray-900 */
|
||||
|
||||
/* Surface Colors */
|
||||
--color-surface: var(--color-white);
|
||||
--color-background: var(--color-white);
|
||||
|
||||
/* Text Colors */
|
||||
--color-text: var(--color-gray-900);
|
||||
--color-text-secondary: var(--color-gray-600);
|
||||
--color-text-tertiary: var(--color-gray-400);
|
||||
|
||||
/* Border Colors */
|
||||
--color-border: var(--color-gray-200);
|
||||
--color-border-hover: var(--color-gray-300);
|
||||
|
||||
/* Focus */
|
||||
--color-focus: var(--color-primary);
|
||||
}
|
||||
```
|
||||
|
||||
**Naming Convention:**
|
||||
```
|
||||
--color-{category}-{variant}-{state}
|
||||
|
||||
Examples:
|
||||
--color-primary (base brand color)
|
||||
--color-primary-hover (interactive state)
|
||||
--color-success-subtle (background usage)
|
||||
--color-text-secondary (content hierarchy)
|
||||
```
|
||||
|
||||
### 2. Typography Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Font Families */
|
||||
--font-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-heading: var(--font-base);
|
||||
--font-mono: "SF Mono", Monaco, "Cascadia Code", monospace;
|
||||
|
||||
/* Font Sizes - Fluid Typography */
|
||||
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
||||
--text-sm: clamp(0.875rem, 0.825rem + 0.25vw, 1rem);
|
||||
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
|
||||
--text-lg: clamp(1.125rem, 1.05rem + 0.375vw, 1.25rem);
|
||||
--text-xl: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
|
||||
--text-2xl: clamp(1.5rem, 1.35rem + 0.75vw, 1.875rem);
|
||||
--text-3xl: clamp(1.875rem, 1.65rem + 1.125vw, 2.25rem);
|
||||
--text-4xl: clamp(2.25rem, 1.95rem + 1.5vw, 3rem);
|
||||
--text-5xl: clamp(3rem, 2.55rem + 2.25vw, 3.75rem);
|
||||
|
||||
/* Font Weights */
|
||||
--font-light: 300;
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
|
||||
/* Line Heights */
|
||||
--leading-tight: 1.25;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.625;
|
||||
--leading-loose: 2;
|
||||
|
||||
/* Letter Spacing */
|
||||
--tracking-tight: -0.025em;
|
||||
--tracking-normal: 0;
|
||||
--tracking-wide: 0.025em;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```css
|
||||
.heading-1 {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--text-4xl);
|
||||
font-weight: var(--font-bold);
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
|
||||
.body-text {
|
||||
font-family: var(--font-base);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-normal);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Spacing Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Spacing Scale (4px base) */
|
||||
--space-0: 0;
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-10: 2.5rem; /* 40px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
--space-16: 4rem; /* 64px */
|
||||
--space-20: 5rem; /* 80px */
|
||||
--space-24: 6rem; /* 96px */
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```css
|
||||
.card {
|
||||
padding: var(--space-4);
|
||||
margin-bottom: var(--space-6);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-block: var(--space-12);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Shadow Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
|
||||
/* Special Shadows */
|
||||
--shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
|
||||
--shadow-focus: 0 0 0 3px rgba(0, 102, 255, 0.5);
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```css
|
||||
.card {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
button:focus-visible {
|
||||
box-shadow: var(--shadow-focus);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Border Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.125rem; /* 2px */
|
||||
--radius-md: 0.375rem; /* 6px */
|
||||
--radius-lg: 0.5rem; /* 8px */
|
||||
--radius-xl: 0.75rem; /* 12px */
|
||||
--radius-2xl: 1rem; /* 16px */
|
||||
--radius-full: 9999px; /* Pills/circles */
|
||||
|
||||
/* Border Widths */
|
||||
--border-1: 1px;
|
||||
--border-2: 2px;
|
||||
--border-4: 4px;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```css
|
||||
.button {
|
||||
border-radius: var(--radius-md);
|
||||
border: var(--border-1) solid var(--color-border);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Z-Index Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Layer Management */
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1020;
|
||||
--z-fixed: 1030;
|
||||
--z-modal-backdrop: 1040;
|
||||
--z-modal: 1050;
|
||||
--z-popover: 1060;
|
||||
--z-tooltip: 1070;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```css
|
||||
.modal-backdrop {
|
||||
z-index: var(--z-modal-backdrop);
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: var(--z-modal);
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Animation/Transition Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Duration */
|
||||
--duration-instant: 0ms;
|
||||
--duration-fast: 150ms;
|
||||
--duration-base: 250ms;
|
||||
--duration-slow: 350ms;
|
||||
--duration-slower: 500ms;
|
||||
|
||||
/* Easing */
|
||||
--ease-linear: linear;
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```css
|
||||
.button {
|
||||
transition: all var(--duration-base) var(--ease-out);
|
||||
}
|
||||
|
||||
.modal {
|
||||
animation: slideIn var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
```
|
||||
|
||||
## Semantic vs Reference Tokens
|
||||
|
||||
### Reference Tokens (Raw Values)
|
||||
```css
|
||||
:root {
|
||||
--blue-500: #0066FF;
|
||||
--gray-100: #F3F4F6;
|
||||
--spacing-4: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
### Semantic Tokens (Meaningful Names)
|
||||
```css
|
||||
:root {
|
||||
--color-primary: var(--blue-500);
|
||||
--color-surface: var(--gray-100);
|
||||
--button-padding: var(--spacing-4);
|
||||
}
|
||||
```
|
||||
|
||||
**Always use semantic tokens in components:**
|
||||
```css
|
||||
/* ❌ Don't */
|
||||
.button {
|
||||
background: var(--blue-500);
|
||||
padding: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* ✅ Do */
|
||||
.button {
|
||||
background: var(--color-primary);
|
||||
padding: var(--button-padding);
|
||||
}
|
||||
```
|
||||
|
||||
## Theming with Tokens
|
||||
|
||||
### Light/Dark Mode
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Light theme (default) */
|
||||
--color-surface: #FFFFFF;
|
||||
--color-text: #111827;
|
||||
--color-border: #E5E7EB;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-surface: #1F2937;
|
||||
--color-text: #F9FAFB;
|
||||
--color-border: #4B5563;
|
||||
}
|
||||
}
|
||||
|
||||
/* Or class-based theming */
|
||||
[data-theme="dark"] {
|
||||
--color-surface: #1F2937;
|
||||
--color-text: #F9FAFB;
|
||||
--color-border: #4B5563;
|
||||
}
|
||||
```
|
||||
|
||||
### Brand Theming
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-primary: var(--brand-blue);
|
||||
}
|
||||
|
||||
[data-brand="tech"] {
|
||||
--brand-blue: #0066FF;
|
||||
}
|
||||
|
||||
[data-brand="health"] {
|
||||
--brand-blue: #10B981;
|
||||
}
|
||||
|
||||
[data-brand="finance"] {
|
||||
--brand-blue: #6366F1;
|
||||
}
|
||||
```
|
||||
|
||||
## Component-Specific Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Button */
|
||||
--button-padding-sm: var(--space-2) var(--space-3);
|
||||
--button-padding-md: var(--space-3) var(--space-4);
|
||||
--button-padding-lg: var(--space-4) var(--space-6);
|
||||
--button-radius: var(--radius-md);
|
||||
--button-font-weight: var(--font-medium);
|
||||
|
||||
/* Input */
|
||||
--input-height-sm: 36px;
|
||||
--input-height-md: 44px;
|
||||
--input-height-lg: 52px;
|
||||
--input-border-color: var(--color-border);
|
||||
--input-focus-color: var(--color-primary);
|
||||
|
||||
/* Card */
|
||||
--card-padding: var(--space-6);
|
||||
--card-radius: var(--radius-lg);
|
||||
--card-shadow: var(--shadow-md);
|
||||
}
|
||||
```
|
||||
|
||||
## Platform-Specific Tokens
|
||||
|
||||
### CSS Custom Properties (Web)
|
||||
```css
|
||||
:root {
|
||||
--color-primary: #0066FF;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
```
|
||||
|
||||
### JSON (React Native, iOS, Android)
|
||||
```json
|
||||
{
|
||||
"color": {
|
||||
"primary": "#0066FF",
|
||||
"surface": "#FFFFFF",
|
||||
"text": "#111827"
|
||||
},
|
||||
"spacing": {
|
||||
"4": 16,
|
||||
"6": 24,
|
||||
"8": 32
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript/TypeScript
|
||||
```typescript
|
||||
export const tokens = {
|
||||
color: {
|
||||
primary: '#0066FF',
|
||||
surface: '#FFFFFF',
|
||||
text: '#111827',
|
||||
},
|
||||
spacing: {
|
||||
4: '1rem',
|
||||
6: '1.5rem',
|
||||
8: '2rem',
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Usage
|
||||
const Button = styled.button`
|
||||
background: ${tokens.color.primary};
|
||||
padding: ${tokens.spacing.4};
|
||||
`;
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
**Do:**
|
||||
- Use descriptive, semantic names
|
||||
- Follow consistent patterns
|
||||
- Group related tokens
|
||||
|
||||
```css
|
||||
/* ✅ Good */
|
||||
--color-primary
|
||||
--color-primary-hover
|
||||
--color-primary-subtle
|
||||
--button-padding-md
|
||||
--shadow-card
|
||||
```
|
||||
|
||||
**Don't:**
|
||||
- Use arbitrary or cryptic names
|
||||
- Mix naming conventions
|
||||
- Use values in names
|
||||
|
||||
```css
|
||||
/* ❌ Bad */
|
||||
--blue
|
||||
--primary-color-hover-state
|
||||
--padding16px
|
||||
--btn-pad
|
||||
```
|
||||
|
||||
### Organization
|
||||
|
||||
Group tokens by category:
|
||||
```
|
||||
tokens/
|
||||
├── colors.css
|
||||
├── typography.css
|
||||
├── spacing.css
|
||||
├── shadows.css
|
||||
├── borders.css
|
||||
├── zindex.css
|
||||
└── index.css (imports all)
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
Document token usage:
|
||||
```css
|
||||
/**
|
||||
* Primary brand color
|
||||
* Used for: primary buttons, links, focus states
|
||||
* Contrast ratio: 4.6:1 (WCAG AA compliant)
|
||||
*/
|
||||
--color-primary: #0066FF;
|
||||
```
|
||||
|
||||
### Accessibility
|
||||
|
||||
Ensure proper contrast:
|
||||
```css
|
||||
:root {
|
||||
--color-primary: #0066FF;
|
||||
--color-text-on-primary: #FFFFFF;
|
||||
/* Contrast ratio: 4.5:1 ✅ */
|
||||
}
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
Use CSS variables efficiently:
|
||||
```css
|
||||
/* ✅ Define once, use everywhere */
|
||||
:root {
|
||||
--color-primary: #0066FF;
|
||||
}
|
||||
|
||||
/* ❌ Don't redefine in every selector */
|
||||
.button { --color-primary: #0066FF; }
|
||||
.link { --color-primary: #0066FF; }
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Step 1: Audit Current Values
|
||||
```bash
|
||||
# Find all color values
|
||||
grep -r "#[0-9A-Fa-f]\{6\}" src/
|
||||
|
||||
# Find all spacing values
|
||||
grep -r "padding:\|margin:" src/
|
||||
```
|
||||
|
||||
### Step 2: Create Token Definitions
|
||||
```css
|
||||
:root {
|
||||
/* Extract unique values */
|
||||
--color-primary: #0066FF;
|
||||
--space-4: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Replace Hard-Coded Values
|
||||
```css
|
||||
/* Before */
|
||||
.button {
|
||||
background: #0066FF;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* After */
|
||||
.button {
|
||||
background: var(--color-primary);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Test & Validate
|
||||
- Visual regression testing
|
||||
- Accessibility testing
|
||||
- Cross-browser testing
|
||||
- Dark mode verification
|
||||
|
||||
## Tools
|
||||
|
||||
**Design Token Tools:**
|
||||
- Style Dictionary (transforms tokens to multiple formats)
|
||||
- Theo (Salesforce's token transformer)
|
||||
- Design Tokens CLI
|
||||
|
||||
**Browser DevTools:**
|
||||
- Chrome: Inspect computed custom properties
|
||||
- Firefox: CSS Variables viewer
|
||||
|
||||
**Example Script:**
|
||||
```javascript
|
||||
// Extract all CSS custom properties
|
||||
const properties = Array.from(document.styleSheets)
|
||||
.flatMap(sheet => Array.from(sheet.cssRules))
|
||||
.filter(rule => rule.style)
|
||||
.flatMap(rule => Array.from(rule.style))
|
||||
.filter(prop => prop.startsWith('--'));
|
||||
|
||||
console.log([...new Set(properties)]);
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Design Tokens W3C Spec](https://design-tokens.github.io/community-group/format/)
|
||||
- [Style Dictionary](https://amzn.github.io/style-dictionary/)
|
||||
- [Design Tokens in Figma](https://www.figma.com/community/plugin/888356646278934516/Design-Tokens)
|
||||
|
||||
---
|
||||
|
||||
**"Design tokens are the translation layer between design decisions and code."**
|
||||
839
skills/frontend-designer/references/responsive_patterns.md
Normal file
839
skills/frontend-designer/references/responsive_patterns.md
Normal file
@@ -0,0 +1,839 @@
|
||||
# Responsive Design Patterns
|
||||
|
||||
Modern responsive design patterns and techniques for creating flexible, accessible layouts that work across all devices.
|
||||
|
||||
## Mobile-First Approach
|
||||
|
||||
Start with mobile design and enhance for larger screens.
|
||||
|
||||
### Why Mobile-First?
|
||||
|
||||
**Benefits:**
|
||||
- Forces focus on essential content
|
||||
- Better performance (smaller base CSS)
|
||||
- Progressive enhancement mindset
|
||||
- Easier to add features than remove them
|
||||
|
||||
**Basic Pattern:**
|
||||
```css
|
||||
/* Base (mobile) styles - no media query */
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Tablet and up */
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop and up */
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Breakpoints
|
||||
|
||||
### Standard Breakpoints
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Mobile first - these are MIN widths */
|
||||
--breakpoint-sm: 640px; /* Small tablets, large phones */
|
||||
--breakpoint-md: 768px; /* Tablets */
|
||||
--breakpoint-lg: 1024px; /* Laptops, desktops */
|
||||
--breakpoint-xl: 1280px; /* Large desktops */
|
||||
--breakpoint-2xl: 1536px; /* Extra large screens */
|
||||
}
|
||||
|
||||
/* Usage */
|
||||
@media (min-width: 768px) {
|
||||
/* Tablet and up */
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
/* Desktop and up */
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Breakpoints
|
||||
|
||||
```css
|
||||
/* Content-based breakpoints */
|
||||
@media (min-width: 400px) {
|
||||
/* When content needs it, not arbitrary device size */
|
||||
}
|
||||
|
||||
/* Prefer rem-based breakpoints for accessibility */
|
||||
@media (min-width: 48rem) { /* 768px at 16px base */
|
||||
/* Scales with user's font size preferences */
|
||||
}
|
||||
```
|
||||
|
||||
### Container Queries (Modern)
|
||||
|
||||
```css
|
||||
/* Respond to container size, not viewport */
|
||||
.card-container {
|
||||
container-type: inline-size;
|
||||
container-name: card;
|
||||
}
|
||||
|
||||
@container card (min-width: 400px) {
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Typography
|
||||
|
||||
### Fluid Typography
|
||||
|
||||
```css
|
||||
/* Scales smoothly between min and max */
|
||||
h1 {
|
||||
font-size: clamp(2rem, 5vw + 1rem, 4rem);
|
||||
/* Min: 2rem (32px) */
|
||||
/* Preferred: 5vw + 1rem */
|
||||
/* Max: 4rem (64px) */
|
||||
}
|
||||
|
||||
/* More examples */
|
||||
.text-sm {
|
||||
font-size: clamp(0.875rem, 0.85rem + 0.125vw, 1rem);
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: clamp(1.125rem, 1.05rem + 0.375vw, 1.25rem);
|
||||
}
|
||||
```
|
||||
|
||||
### Responsive Line Height
|
||||
|
||||
```css
|
||||
body {
|
||||
/* Tighter on mobile, looser on desktop */
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Layout Patterns
|
||||
|
||||
### 1. Stack Layout
|
||||
|
||||
**Everything stacks vertically on mobile, side-by-side on larger screens.**
|
||||
|
||||
```css
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.stack {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Sidebar Layout
|
||||
|
||||
**Sidebar stacks on mobile, side-by-side on desktop.**
|
||||
|
||||
```css
|
||||
.sidebar-layout {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.sidebar-layout {
|
||||
grid-template-columns: 250px 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Flexible sidebar */
|
||||
@media (min-width: 768px) {
|
||||
.sidebar-layout--flexible {
|
||||
grid-template-columns: minmax(200px, 300px) 1fr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Grid Layout
|
||||
|
||||
**Responsive column count.**
|
||||
|
||||
```css
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
/* Mobile: 1 column */
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.grid {
|
||||
/* Tablet: 2 columns */
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.grid {
|
||||
/* Desktop: 3-4 columns */
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Auto-responsive grid (no media queries!) */
|
||||
.grid-auto {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
/* Creates as many columns as fit, minimum 250px each */
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Holy Grail Layout
|
||||
|
||||
**Classic three-column layout that adapts to mobile.**
|
||||
|
||||
```css
|
||||
.holy-grail {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
min-height: 100vh;
|
||||
|
||||
/* Mobile: stack everything */
|
||||
grid-template:
|
||||
"header" auto
|
||||
"main" 1fr
|
||||
"sidebar1" auto
|
||||
"sidebar2" auto
|
||||
"footer" auto
|
||||
/ 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.holy-grail {
|
||||
/* Desktop: traditional layout */
|
||||
grid-template:
|
||||
"header header header" auto
|
||||
"sidebar1 main sidebar2" 1fr
|
||||
"footer footer footer" auto
|
||||
/ 200px 1fr 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.header { grid-area: header; }
|
||||
.sidebar-1 { grid-area: sidebar1; }
|
||||
.main { grid-area: main; }
|
||||
.sidebar-2 { grid-area: sidebar2; }
|
||||
.footer { grid-area: footer; }
|
||||
```
|
||||
|
||||
### 5. Card Layout
|
||||
|
||||
**Responsive cards that adapt their internal layout.**
|
||||
|
||||
```css
|
||||
.card {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
/* Mobile: stack image and content */
|
||||
grid-template:
|
||||
"image" auto
|
||||
"content" 1fr
|
||||
/ 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.card {
|
||||
/* Tablet+: side-by-side */
|
||||
grid-template:
|
||||
"image content" 1fr
|
||||
/ 200px 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.card__image { grid-area: image; }
|
||||
.card__content { grid-area: content; }
|
||||
```
|
||||
|
||||
### 6. Switcher Pattern
|
||||
|
||||
**Switch between horizontal and vertical based on available space.**
|
||||
|
||||
```css
|
||||
.switcher {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.switcher > * {
|
||||
/* Grow to fill, but switch to vertical when < 400px */
|
||||
flex-grow: 1;
|
||||
flex-basis: calc((400px - 100%) * 999);
|
||||
/* Clever calc that breaks at threshold */
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Pancake Stack
|
||||
|
||||
**Header, main, footer layout that adapts height.**
|
||||
|
||||
```css
|
||||
.pancake {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header { /* auto height */ }
|
||||
.main { /* fills available space */ }
|
||||
.footer { /* auto height */ }
|
||||
```
|
||||
|
||||
## Responsive Images
|
||||
|
||||
### 1. Flexible Images
|
||||
|
||||
```css
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Art Direction (Different Images per Breakpoint)
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source media="(min-width: 1024px)" srcset="large.jpg">
|
||||
<source media="(min-width: 768px)" srcset="medium.jpg">
|
||||
<img src="small.jpg" alt="Description">
|
||||
</picture>
|
||||
```
|
||||
|
||||
### 3. Resolution Switching (Same Image, Different Sizes)
|
||||
|
||||
```html
|
||||
<img
|
||||
srcset="
|
||||
small.jpg 400w,
|
||||
medium.jpg 800w,
|
||||
large.jpg 1200w
|
||||
"
|
||||
sizes="
|
||||
(min-width: 1024px) 800px,
|
||||
(min-width: 768px) 600px,
|
||||
100vw
|
||||
"
|
||||
src="medium.jpg"
|
||||
alt="Description"
|
||||
>
|
||||
```
|
||||
|
||||
### 4. Background Images
|
||||
|
||||
```css
|
||||
.hero {
|
||||
background-image: url('small.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.hero {
|
||||
background-image: url('medium.jpg');
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.hero {
|
||||
background-image: url('large.jpg');
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) {
|
||||
.hero {
|
||||
background-image: url('large@2x.jpg');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Aspect Ratio
|
||||
|
||||
```css
|
||||
/* Modern aspect ratio */
|
||||
.image-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Fallback for older browsers */
|
||||
.image-container-fallback {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 aspect ratio */
|
||||
}
|
||||
|
||||
.image-container-fallback img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Navigation
|
||||
|
||||
### 1. Mobile Menu (Hamburger)
|
||||
|
||||
```html
|
||||
<nav class="nav">
|
||||
<div class="nav__brand">Logo</div>
|
||||
|
||||
<button class="nav__toggle" aria-expanded="false" aria-controls="nav-menu">
|
||||
<span class="sr-only">Menu</span>
|
||||
<span class="hamburger"></span>
|
||||
</button>
|
||||
|
||||
<ul class="nav__menu" id="nav-menu">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
```css
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Mobile: hidden menu */
|
||||
.nav__menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.nav__menu[aria-expanded="true"] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav__toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Desktop: visible menu */
|
||||
@media (min-width: 768px) {
|
||||
.nav__menu {
|
||||
display: flex;
|
||||
position: static;
|
||||
flex-direction: row;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.nav__toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Priority+ Navigation
|
||||
|
||||
**Show important items, collapse others into "More" menu.**
|
||||
|
||||
```css
|
||||
.priority-nav {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.priority-nav__item {
|
||||
flex-shrink: 0; /* Don't shrink */
|
||||
}
|
||||
|
||||
.priority-nav__more {
|
||||
margin-left: auto; /* Push to end */
|
||||
}
|
||||
|
||||
/* Hide items that don't fit */
|
||||
@media (max-width: 768px) {
|
||||
.priority-nav__item:nth-child(n+4) {
|
||||
display: none; /* Hide items 4+ */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Tables
|
||||
|
||||
### 1. Horizontal Scroll
|
||||
|
||||
```css
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
|
||||
}
|
||||
|
||||
table {
|
||||
min-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Stacked Table (Card View)
|
||||
|
||||
```html
|
||||
<table class="responsive-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td data-label="Name">John Doe</td>
|
||||
<td data-label="Email">john@example.com</td>
|
||||
<td data-label="Role">Developer</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
```css
|
||||
/* Desktop: normal table */
|
||||
@media (min-width: 768px) {
|
||||
.responsive-table {
|
||||
display: table;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile: stacked cards */
|
||||
@media (max-width: 767px) {
|
||||
.responsive-table,
|
||||
.responsive-table thead,
|
||||
.responsive-table tbody,
|
||||
.responsive-table tr,
|
||||
.responsive-table th,
|
||||
.responsive-table td {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.responsive-table thead {
|
||||
display: none; /* Hide table header */
|
||||
}
|
||||
|
||||
.responsive-table tr {
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.responsive-table td {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.responsive-table td:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.responsive-table td::before {
|
||||
content: attr(data-label);
|
||||
font-weight: bold;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Forms
|
||||
|
||||
### 1. Single Column to Multi-Column
|
||||
|
||||
```css
|
||||
.form {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
/* Mobile: single column */
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.form {
|
||||
/* Desktop: two columns */
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.form__field--full {
|
||||
/* Some fields span both columns */
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Touch-Friendly Inputs
|
||||
|
||||
```css
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
textarea {
|
||||
/* Minimum 44x44px touch target */
|
||||
min-height: 44px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem; /* Prevents zoom on iOS */
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
textarea {
|
||||
/* Can be smaller on desktop */
|
||||
min-height: 40px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Utilities
|
||||
|
||||
### Show/Hide at Breakpoints
|
||||
|
||||
```css
|
||||
/* Hide on mobile */
|
||||
.hidden-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.hidden-mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Show only on mobile */
|
||||
.visible-mobile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.visible-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide on desktop */
|
||||
@media (min-width: 1024px) {
|
||||
.hidden-desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Responsive Spacing
|
||||
|
||||
```css
|
||||
.section {
|
||||
/* Mobile: smaller padding */
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.section {
|
||||
/* Tablet: medium padding */
|
||||
padding: 4rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.section {
|
||||
/* Desktop: larger padding */
|
||||
padding: 6rem 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Or use fluid spacing */
|
||||
.section-fluid {
|
||||
padding: clamp(2rem, 5vw, 6rem) clamp(1rem, 3vw, 3rem);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Responsive Design
|
||||
|
||||
### Browser DevTools
|
||||
|
||||
```javascript
|
||||
// Common viewport sizes to test
|
||||
const viewports = [
|
||||
{ width: 375, height: 667, name: 'iPhone SE' },
|
||||
{ width: 390, height: 844, name: 'iPhone 12 Pro' },
|
||||
{ width: 428, height: 926, name: 'iPhone 14 Pro Max' },
|
||||
{ width: 768, height: 1024, name: 'iPad' },
|
||||
{ width: 1024, height: 768, name: 'iPad Landscape' },
|
||||
{ width: 1280, height: 720, name: 'Desktop' },
|
||||
{ width: 1920, height: 1080, name: 'Full HD' },
|
||||
];
|
||||
```
|
||||
|
||||
### Responsive Testing Checklist
|
||||
|
||||
- [ ] Test all breakpoints
|
||||
- [ ] Test between breakpoints (awkward sizes)
|
||||
- [ ] Test portrait and landscape
|
||||
- [ ] Test zoom levels (100%, 200%, 400%)
|
||||
- [ ] Test with real devices when possible
|
||||
- [ ] Test touch interactions on mobile
|
||||
- [ ] Test with different font sizes
|
||||
- [ ] Test with slow network (images, fonts)
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```html
|
||||
<!-- Lazy load images below the fold -->
|
||||
<img src="image.jpg" loading="lazy" alt="Description">
|
||||
|
||||
<!-- Eager load above-the-fold images -->
|
||||
<img src="hero.jpg" loading="eager" alt="Hero image">
|
||||
```
|
||||
|
||||
### Conditional Loading
|
||||
|
||||
```javascript
|
||||
// Load component only on larger screens
|
||||
if (window.matchMedia('(min-width: 768px)').matches) {
|
||||
import('./DesktopComponent.js').then(module => {
|
||||
// Initialize desktop component
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Font Loading
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2') format('woff2');
|
||||
font-display: swap; /* Show fallback while loading */
|
||||
}
|
||||
```
|
||||
|
||||
## Modern CSS Features
|
||||
|
||||
### 1. CSS Grid Auto-Fill
|
||||
|
||||
```css
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
/* Automatically creates columns, minimum 250px */
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Flexbox Gap
|
||||
|
||||
```css
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem; /* No more margin hacks! */
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Container Queries
|
||||
|
||||
```css
|
||||
.card {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (min-width: 400px) {
|
||||
.card__title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Aspect Ratio
|
||||
|
||||
```css
|
||||
.video-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Logical Properties
|
||||
|
||||
```css
|
||||
/* Better for RTL/LTR support */
|
||||
.element {
|
||||
margin-block-start: 1rem; /* margin-top */
|
||||
margin-block-end: 1rem; /* margin-bottom */
|
||||
margin-inline-start: 1rem; /* margin-left in LTR, margin-right in RTL */
|
||||
margin-inline-end: 1rem; /* margin-right in LTR, margin-left in RTL */
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [MDN: Responsive Design](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Responsive_Design)
|
||||
- [This is Responsive](https://bradfrost.github.io/this-is-responsive/)
|
||||
- [Responsive Design Patterns](https://responsivedesign.is/patterns/)
|
||||
- [CSS-Tricks: Complete Guide to Grid](https://css-tricks.com/snippets/css/complete-guide-grid/)
|
||||
- [CSS-Tricks: Complete Guide to Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/)
|
||||
|
||||
---
|
||||
|
||||
**"The best design is the one that works everywhere, for everyone."**
|
||||
Reference in New Issue
Block a user