Episode 2 — React Frontend Architecture NextJS / 2.5 — Event Handling and Conditional Rendering

2.5.d — Dynamic UI Rendering

In one sentence: Dynamic UI rendering combines state management with conditional rendering to build interactive elements like tabs, accordions, modals, and toggling visibility — all driven by state changes that React re-renders automatically.

Navigation: ← Conditional Rendering Patterns · Next → Practical Build: Dynamic Form


Table of Contents

  1. What is Dynamic UI?
  2. Show/Hide Toggle
  3. Tab Component
  4. Accordion / Collapsible
  5. Modal / Dialog
  6. Dropdown Menu
  7. Notification / Toast System
  8. Dynamic Component Selection
  9. Multi-Step Wizard
  10. Toggle Between Views (Grid vs List)
  11. Skeleton Loading Screens
  12. Animated Transitions
  13. Accessibility Considerations
  14. Key Takeaways

1. What is Dynamic UI?

Dynamic UI means components that change their visual state in response to user interaction — without page reloads. Every dynamic UI pattern follows the same loop:

┌──────────────────────────────────┐
│         User Action               │
│  (click, hover, scroll, etc.)     │
│              │                     │
│              ▼                     │
│       State Change                │
│  (useState, useReducer)           │
│              │                     │
│              ▼                     │
│       React Re-renders            │
│              │                     │
│              ▼                     │
│  Conditional Rendering            │
│  decides what to show             │
│              │                     │
│              ▼                     │
│       Updated UI                  │
└──────────────────────────────────┘

2. Show/Hide Toggle

Simple Toggle

function ToggleSection({ title, children }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? '▼' : '▶'} {title}
      </button>
      {isOpen && <div style={{ padding: '12px 0' }}>{children}</div>}
    </div>
  );
}

// Usage
<ToggleSection title="Advanced Settings">
  <p>Secret options here...</p>
</ToggleSection>

Toggle with Animation Prep

function AnimatedToggle({ title, children }) {
  const [isOpen, setIsOpen] = useState(false);
  const contentRef = useRef(null);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {title} {isOpen ? '−' : '+'}
      </button>
      <div
        ref={contentRef}
        style={{
          maxHeight: isOpen ? contentRef.current?.scrollHeight + 'px' : '0px',
          overflow: 'hidden',
          transition: 'max-height 0.3s ease',
        }}
      >
        <div style={{ padding: '12px 0' }}>{children}</div>
      </div>
    </div>
  );
}

3. Tab Component

Basic Tabs

function Tabs({ tabs }) {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div>
      {/* Tab Headers */}
      <div style={{ display: 'flex', borderBottom: '2px solid #eee' }}>
        {tabs.map((tab, index) => (
          <button
            key={tab.label}
            onClick={() => setActiveTab(index)}
            style={{
              padding: '12px 24px',
              border: 'none',
              background: 'none',
              cursor: 'pointer',
              borderBottom: activeTab === index ? '2px solid #0070f3' : '2px solid transparent',
              color: activeTab === index ? '#0070f3' : '#666',
              fontWeight: activeTab === index ? 600 : 400,
              marginBottom: -2,
            }}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {/* Tab Content */}
      <div style={{ padding: 20 }}>
        {tabs[activeTab].content}
      </div>
    </div>
  );
}

// Usage
function App() {
  const tabs = [
    { label: 'Profile', content: <ProfileTab /> },
    { label: 'Settings', content: <SettingsTab /> },
    { label: 'Billing', content: <BillingTab /> },
  ];

  return <Tabs tabs={tabs} />;
}

Tabs That Preserve State

By default, switching tabs unmounts the previous content. To preserve state:

function StatefulTabs({ tabs }) {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div>
      <div style={{ display: 'flex', gap: 4 }}>
        {tabs.map((tab, index) => (
          <button
            key={tab.label}
            onClick={() => setActiveTab(index)}
            style={{
              padding: '8px 16px',
              background: activeTab === index ? '#0070f3' : '#f0f0f0',
              color: activeTab === index ? 'white' : '#333',
              border: 'none',
              borderRadius: '8px 8px 0 0',
              cursor: 'pointer',
            }}
          >
            {tab.label}
          </button>
        ))}
      </div>
      
      {/* ALL tabs are rendered, but only active one is visible */}
      {tabs.map((tab, index) => (
        <div
          key={tab.label}
          style={{ display: activeTab === index ? 'block' : 'none', padding: 20 }}
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}

4. Accordion / Collapsible

Single-Open Accordion

function Accordion({ items }) {
  const [openIndex, setOpenIndex] = useState(null);

  function handleToggle(index) {
    setOpenIndex(prev => (prev === index ? null : index));
  }

  return (
    <div style={{ border: '1px solid #ddd', borderRadius: 8, overflow: 'hidden' }}>
      {items.map((item, index) => (
        <div key={index}>
          <button
            onClick={() => handleToggle(index)}
            style={{
              width: '100%',
              padding: '16px 20px',
              background: openIndex === index ? '#f7f7f7' : 'white',
              border: 'none',
              borderBottom: '1px solid #eee',
              textAlign: 'left',
              cursor: 'pointer',
              display: 'flex',
              justifyContent: 'space-between',
              alignItems: 'center',
              fontWeight: 500,
            }}
          >
            {item.title}
            <span style={{
              transform: openIndex === index ? 'rotate(180deg)' : 'rotate(0)',
              transition: 'transform 0.2s',
            }}></span>
          </button>
          {openIndex === index && (
            <div style={{ padding: '16px 20px', background: '#fafafa' }}>
              {item.content}
            </div>
          )}
        </div>
      ))}
    </div>
  );
}

// Usage
const faqItems = [
  { title: 'What is React?', content: 'React is a JavaScript library for building UIs.' },
  { title: 'What is JSX?', content: 'JSX is a syntax extension that lets you write HTML-like code in JavaScript.' },
  { title: 'What are hooks?', content: 'Hooks let you use state and other React features in functional components.' },
];

<Accordion items={faqItems} />

Multi-Open Accordion

function MultiAccordion({ items }) {
  const [openItems, setOpenItems] = useState(new Set());

  function handleToggle(index) {
    setOpenItems(prev => {
      const next = new Set(prev);
      if (next.has(index)) {
        next.delete(index);
      } else {
        next.add(index);
      }
      return next;
    });
  }

  return (
    <div>
      {items.map((item, index) => (
        <div key={index} style={{ marginBottom: 4 }}>
          <button
            onClick={() => handleToggle(index)}
            style={{
              width: '100%',
              padding: 16,
              textAlign: 'left',
              border: '1px solid #ddd',
              borderRadius: openItems.has(index) ? '8px 8px 0 0' : 8,
              background: openItems.has(index) ? '#f0f0f0' : 'white',
              cursor: 'pointer',
            }}
          >
            {openItems.has(index) ? '−' : '+'} {item.title}
          </button>
          {openItems.has(index) && (
            <div style={{
              padding: 16,
              border: '1px solid #ddd',
              borderTop: 'none',
              borderRadius: '0 0 8px 8px',
            }}>
              {item.content}
            </div>
          )}
        </div>
      ))}
    </div>
  );
}

5. Modal / Dialog

Complete Modal Component

function Modal({ isOpen, onClose, title, children }) {
  // Close on Escape key
  useEffect(() => {
    function handleEscape(event) {
      if (event.key === 'Escape') onClose();
    }
    if (isOpen) {
      document.addEventListener('keydown', handleEscape);
      document.body.style.overflow = 'hidden'; // Prevent background scroll
    }
    return () => {
      document.removeEventListener('keydown', handleEscape);
      document.body.style.overflow = '';
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div
      style={{
        position: 'fixed',
        inset: 0,
        background: 'rgba(0, 0, 0, 0.5)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        zIndex: 1000,
      }}
      onClick={onClose}  // Close on backdrop click
    >
      <div
        style={{
          background: 'white',
          borderRadius: 12,
          padding: 24,
          maxWidth: 500,
          width: '90%',
          maxHeight: '80vh',
          overflow: 'auto',
          boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
        }}
        onClick={(e) => e.stopPropagation()}  // Don't close when clicking content
      >
        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
          <h2 style={{ margin: 0 }}>{title}</h2>
          <button
            onClick={onClose}
            style={{
              background: 'none',
              border: 'none',
              fontSize: 24,
              cursor: 'pointer',
              padding: '0 4px',
            }}
            aria-label="Close modal"
          >
            ×
          </button>
        </div>
        {children}
      </div>
    </div>,
    document.body
  );
}

// Usage
function App() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <button onClick={() => setShowModal(true)}>Open Modal</button>
      <Modal
        isOpen={showModal}
        onClose={() => setShowModal(false)}
        title="Confirm Action"
      >
        <p>Are you sure you want to proceed?</p>
        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 16 }}>
          <button onClick={() => setShowModal(false)}>Cancel</button>
          <button onClick={() => { handleConfirm(); setShowModal(false); }}>
            Confirm
          </button>
        </div>
      </Modal>
    </div>
  );
}

6. Dropdown Menu

function Dropdown({ trigger, items }) {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useRef(null);

  // Close on outside click
  useEffect(() => {
    function handleClickOutside(event) {
      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
        setIsOpen(false);
      }
    }
    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  return (
    <div ref={dropdownRef} style={{ position: 'relative', display: 'inline-block' }}>
      <button onClick={() => setIsOpen(!isOpen)}>
        {trigger} {isOpen ? '▲' : '▼'}
      </button>

      {isOpen && (
        <div style={{
          position: 'absolute',
          top: '100%',
          left: 0,
          marginTop: 4,
          background: 'white',
          border: '1px solid #ddd',
          borderRadius: 8,
          boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
          minWidth: 160,
          zIndex: 100,
          overflow: 'hidden',
        }}>
          {items.map((item, i) => (
            item.divider ? (
              <hr key={i} style={{ margin: '4px 0', border: 'none', borderTop: '1px solid #eee' }} />
            ) : (
              <button
                key={i}
                onClick={() => {
                  item.onClick();
                  setIsOpen(false);
                }}
                style={{
                  display: 'block',
                  width: '100%',
                  padding: '10px 16px',
                  border: 'none',
                  background: 'none',
                  textAlign: 'left',
                  cursor: 'pointer',
                  fontSize: 14,
                }}
                onMouseEnter={(e) => e.target.style.background = '#f5f5f5'}
                onMouseLeave={(e) => e.target.style.background = 'none'}
              >
                {item.icon && <span style={{ marginRight: 8 }}>{item.icon}</span>}
                {item.label}
              </button>
            )
          ))}
        </div>
      )}
    </div>
  );
}

// Usage
<Dropdown
  trigger="Actions"
  items={[
    { label: 'Edit', icon: '✏️', onClick: () => console.log('Edit') },
    { label: 'Duplicate', icon: '📋', onClick: () => console.log('Duplicate') },
    { divider: true },
    { label: 'Delete', icon: '🗑️', onClick: () => console.log('Delete') },
  ]}
/>

7. Notification / Toast System

function useToasts() {
  const [toasts, setToasts] = useState([]);

  function addToast(message, type = 'info', duration = 3000) {
    const id = Date.now();
    setToasts(prev => [...prev, { id, message, type }]);

    if (duration > 0) {
      setTimeout(() => removeToast(id), duration);
    }
  }

  function removeToast(id) {
    setToasts(prev => prev.filter(t => t.id !== id));
  }

  return { toasts, addToast, removeToast };
}

function ToastContainer({ toasts, removeToast }) {
  const typeStyles = {
    success: { bg: '#d4edda', color: '#155724', icon: '✅' },
    error:   { bg: '#f8d7da', color: '#721c24', icon: '❌' },
    warning: { bg: '#fff3cd', color: '#856404', icon: '⚠️' },
    info:    { bg: '#d1ecf1', color: '#0c5460', icon: 'ℹ️' },
  };

  return (
    <div style={{ position: 'fixed', top: 20, right: 20, zIndex: 9999 }}>
      {toasts.map(toast => {
        const s = typeStyles[toast.type] || typeStyles.info;
        return (
          <div
            key={toast.id}
            style={{
              background: s.bg,
              color: s.color,
              padding: '12px 16px',
              borderRadius: 8,
              marginBottom: 8,
              display: 'flex',
              alignItems: 'center',
              gap: 8,
              minWidth: 250,
              boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
            }}
          >
            <span>{s.icon}</span>
            <span style={{ flex: 1 }}>{toast.message}</span>
            <button
              onClick={() => removeToast(toast.id)}
              style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 16 }}
            >
              ×
            </button>
          </div>
        );
      })}
    </div>
  );
}

// Usage
function App() {
  const { toasts, addToast, removeToast } = useToasts();

  return (
    <div>
      <button onClick={() => addToast('Saved!', 'success')}>Success</button>
      <button onClick={() => addToast('Something failed', 'error')}>Error</button>
      <button onClick={() => addToast('Check this out', 'warning')}>Warning</button>
      <ToastContainer toasts={toasts} removeToast={removeToast} />
    </div>
  );
}

8. Dynamic Component Selection

Component Map

const componentMap = {
  text: TextBlock,
  image: ImageBlock,
  video: VideoBlock,
  code: CodeBlock,
  quote: QuoteBlock,
};

function ContentRenderer({ blocks }) {
  return (
    <div>
      {blocks.map(block => {
        const Component = componentMap[block.type];
        if (!Component) return null;
        return <Component key={block.id} {...block.props} />;
      })}
    </div>
  );
}

// Usage
const blocks = [
  { id: 1, type: 'text', props: { content: 'Hello world' } },
  { id: 2, type: 'image', props: { src: '/photo.jpg', alt: 'Photo' } },
  { id: 3, type: 'code', props: { language: 'javascript', code: 'const x = 1;' } },
];

<ContentRenderer blocks={blocks} />

Form Field Factory

const fieldComponents = {
  text: ({ name, label, value, onChange }) => (
    <input type="text" name={name} value={value} onChange={onChange} placeholder={label} />
  ),
  textarea: ({ name, label, value, onChange }) => (
    <textarea name={name} value={value} onChange={onChange} placeholder={label} rows={4} />
  ),
  select: ({ name, label, value, onChange, options }) => (
    <select name={name} value={value} onChange={onChange}>
      <option value="">{label}</option>
      {options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
    </select>
  ),
  checkbox: ({ name, label, value, onChange }) => (
    <label>
      <input type="checkbox" name={name} checked={value} onChange={onChange} />
      {' '}{label}
    </label>
  ),
};

function DynamicForm({ fields, values, onChange }) {
  return (
    <form>
      {fields.map(field => {
        const FieldComponent = fieldComponents[field.type];
        if (!FieldComponent) return null;
        return (
          <div key={field.name} style={{ marginBottom: 12 }}>
            <FieldComponent
              {...field}
              value={values[field.name] || ''}
              onChange={onChange}
            />
          </div>
        );
      })}
    </form>
  );
}

9. Multi-Step Wizard

function Wizard({ steps }) {
  const [currentStep, setCurrentStep] = useState(0);
  const [formData, setFormData] = useState({});

  function handleNext(stepData) {
    setFormData(prev => ({ ...prev, ...stepData }));
    setCurrentStep(prev => Math.min(prev + 1, steps.length - 1));
  }

  function handleBack() {
    setCurrentStep(prev => Math.max(prev - 1, 0));
  }

  function handleSubmit(stepData) {
    const finalData = { ...formData, ...stepData };
    console.log('Complete form data:', finalData);
  }

  const StepComponent = steps[currentStep].component;
  const isFirst = currentStep === 0;
  const isLast = currentStep === steps.length - 1;

  return (
    <div>
      {/* Progress Indicator */}
      <div style={{ display: 'flex', marginBottom: 24 }}>
        {steps.map((step, i) => (
          <div key={i} style={{ flex: 1, textAlign: 'center' }}>
            <div style={{
              width: 32,
              height: 32,
              borderRadius: '50%',
              background: i <= currentStep ? '#0070f3' : '#ddd',
              color: 'white',
              display: 'inline-flex',
              alignItems: 'center',
              justifyContent: 'center',
              fontWeight: 600,
            }}>
              {i < currentStep ? '✓' : i + 1}
            </div>
            <p style={{
              fontSize: 12,
              color: i <= currentStep ? '#0070f3' : '#999',
              marginTop: 4,
            }}>
              {step.title}
            </p>
          </div>
        ))}
      </div>

      {/* Step Content */}
      <StepComponent
        data={formData}
        onNext={isLast ? handleSubmit : handleNext}
        onBack={!isFirst ? handleBack : undefined}
        isLast={isLast}
      />
    </div>
  );
}

// Step Components
function PersonalInfo({ data, onNext, onBack }) {
  const [name, setName] = useState(data.name || '');
  const [email, setEmail] = useState(data.email || '');

  return (
    <div>
      <h2>Personal Information</h2>
      <input value={name} onChange={e => setName(e.target.value)} placeholder="Name" />
      <input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />
      <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
        {onBack && <button onClick={onBack}>Back</button>}
        <button onClick={() => onNext({ name, email })}>Next</button>
      </div>
    </div>
  );
}

// Usage
<Wizard
  steps={[
    { title: 'Personal', component: PersonalInfo },
    { title: 'Address', component: AddressForm },
    { title: 'Payment', component: PaymentForm },
    { title: 'Review', component: ReviewStep },
  ]}
/>

10. Toggle Between Views (Grid vs List)

function ViewToggle({ items }) {
  const [view, setView] = useState('grid');

  return (
    <div>
      {/* View Switcher */}
      <div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
        <button
          onClick={() => setView('grid')}
          style={{
            padding: '8px 16px',
            background: view === 'grid' ? '#0070f3' : '#f0f0f0',
            color: view === 'grid' ? 'white' : '#333',
            border: 'none',
            borderRadius: 6,
            cursor: 'pointer',
          }}
        >
          ▦ Grid
        </button>
        <button
          onClick={() => setView('list')}
          style={{
            padding: '8px 16px',
            background: view === 'list' ? '#0070f3' : '#f0f0f0',
            color: view === 'list' ? 'white' : '#333',
            border: 'none',
            borderRadius: 6,
            cursor: 'pointer',
          }}
        >
          ☰ List
        </button>
      </div>

      {/* Content */}
      {view === 'grid' ? (
        <div style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
          gap: 16,
        }}>
          {items.map(item => (
            <div key={item.id} style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
              <img src={item.image} alt={item.name} style={{ width: '100%', borderRadius: 4 }} />
              <h3>{item.name}</h3>
              <p>{item.price}</p>
            </div>
          ))}
        </div>
      ) : (
        <div>
          {items.map(item => (
            <div key={item.id} style={{
              display: 'flex',
              gap: 16,
              padding: 12,
              borderBottom: '1px solid #eee',
              alignItems: 'center',
            }}>
              <img src={item.image} alt={item.name} style={{ width: 60, height: 60, borderRadius: 4 }} />
              <div>
                <h3 style={{ margin: 0 }}>{item.name}</h3>
                <p style={{ margin: 0, color: '#666' }}>{item.price}</p>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

11. Skeleton Loading Screens

function SkeletonCard() {
  return (
    <div style={{ border: '1px solid #eee', borderRadius: 8, padding: 16, width: 250 }}>
      {/* Image placeholder */}
      <div style={{
        height: 150,
        borderRadius: 4,
        background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
        backgroundSize: '200% 100%',
        animation: 'shimmer 1.5s infinite',
      }} />
      {/* Title placeholder */}
      <div style={{
        height: 20,
        width: '80%',
        marginTop: 12,
        borderRadius: 4,
        background: '#f0f0f0',
      }} />
      {/* Description placeholder */}
      <div style={{
        height: 14,
        width: '60%',
        marginTop: 8,
        borderRadius: 4,
        background: '#f0f0f0',
      }} />
    </div>
  );
}

function ProductList({ isLoading, products }) {
  if (isLoading) {
    return (
      <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
        {Array.from({ length: 6 }).map((_, i) => (
          <SkeletonCard key={i} />
        ))}
      </div>
    );
  }

  return (
    <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  );
}

12. Animated Transitions

CSS Transition on State Change

function FadeToggle() {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>Toggle</button>
      <div style={{
        opacity: isVisible ? 1 : 0,
        transform: isVisible ? 'translateY(0)' : 'translateY(-10px)',
        transition: 'opacity 0.3s ease, transform 0.3s ease',
        pointerEvents: isVisible ? 'auto' : 'none',
      }}>
        <p>I fade in and out!</p>
      </div>
    </div>
  );
}

Slide Transition for Tabs

function SlidingTabs({ tabs }) {
  const [activeTab, setActiveTab] = useState(0);
  const [direction, setDirection] = useState('right');

  function handleTabChange(newIndex) {
    setDirection(newIndex > activeTab ? 'right' : 'left');
    setActiveTab(newIndex);
  }

  return (
    <div>
      <div style={{ display: 'flex', gap: 4 }}>
        {tabs.map((tab, i) => (
          <button key={i} onClick={() => handleTabChange(i)}>
            {tab.label}
          </button>
        ))}
      </div>
      <div style={{ overflow: 'hidden' }}>
        <div
          key={activeTab}
          style={{
            animation: `slide-${direction} 0.3s ease`,
          }}
        >
          {tabs[activeTab].content}
        </div>
      </div>
    </div>
  );
}

13. Accessibility Considerations

Modal Accessibility

function AccessibleModal({ isOpen, onClose, title, children }) {
  const previousFocusRef = useRef(null);

  useEffect(() => {
    if (isOpen) {
      previousFocusRef.current = document.activeElement;
      // Focus will be managed by the dialog
    }
    return () => {
      // Restore focus when modal closes
      previousFocusRef.current?.focus();
    };
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      tabIndex={-1}
    >
      <h2 id="modal-title">{title}</h2>
      {children}
      <button onClick={onClose} aria-label="Close dialog">×</button>
    </div>
  );
}

Accordion Accessibility

function AccessibleAccordion({ items }) {
  const [openIndex, setOpenIndex] = useState(null);

  return (
    <div>
      {items.map((item, index) => {
        const isOpen = openIndex === index;
        const panelId = `panel-${index}`;
        const headerId = `header-${index}`;

        return (
          <div key={index}>
            <h3>
              <button
                id={headerId}
                onClick={() => setOpenIndex(isOpen ? null : index)}
                aria-expanded={isOpen}
                aria-controls={panelId}
                style={{ width: '100%', textAlign: 'left' }}
              >
                {item.title}
              </button>
            </h3>
            <div
              id={panelId}
              role="region"
              aria-labelledby={headerId}
              hidden={!isOpen}
            >
              {item.content}
            </div>
          </div>
        );
      })}
    </div>
  );
}

ARIA Attributes Quick Reference

ComponentRequired ARIAPurpose
Modalrole="dialog", aria-modal="true", aria-labelledbyAnnounces dialog to screen readers
Tabrole="tablist", role="tab", aria-selectedTab navigation pattern
Accordionaria-expanded, aria-controlsExpandable sections
Dropdownaria-haspopup, aria-expandedMenu disclosure
Togglearia-pressed or aria-checkedOn/off state

14. Key Takeaways

  1. All dynamic UI follows the same pattern: user action → state change → re-render → conditional rendering
  2. Tabs: Track activeTab index, render content based on selection. Use display: none to preserve state across tabs.
  3. Modals: Use createPortal, handle Escape key, prevent background scroll, stop propagation on content click
  4. Accordions: Single-open = openIndex state; multi-open = Set state
  5. Dropdowns: Close on outside click with useEffect + contains() check
  6. Component map pattern = most flexible way to render dynamic component types
  7. Skeleton screens provide better UX than spinners — they show layout shape during loading
  8. CSS transitions (opacity, transform, max-height) give smooth animations without libraries
  9. Always consider accessibility — ARIA roles, keyboard navigation, focus management
  10. Preserve vs destroy state: {show && <X/>} destroys state; display: none preserves it

Explain-It Challenge

  1. The Hotel Analogy: Explain the difference between conditional rendering ({show && <Modal />}) and CSS hiding (display: none) using the analogy of hotel rooms. When is a room demolished vs just locked?

  2. The Accessibility Audit: You're given a modal that opens correctly but has no keyboard support. Walk through the minimum accessibility requirements: focus trapping, Escape to close, aria attributes, and focus restoration.

  3. The Performance Discussion: You have a tab interface with 4 heavy tabs. Each tab fetches data on mount. Discuss the trade-offs between mounting/unmounting tabs on switch vs rendering all and using display: none.


Navigation: ← Conditional Rendering Patterns · Next → Practical Build: Dynamic Form