Open Source | The School of Code

Settings

Appearance

Choose a typography theme that suits your style

Open Source

Explore Our Code

Our interactive explainers are free and open source. Copy the code directly into your project.

Flexbox Explainer

Interactive CSS Flexbox layout visualizer. Experiment with flex-direction, justify-content, align-items, and more.

flexbox.tsx 542 lines
import { useState } from 'react';

type FlexDirection = 'row' | 'row-reverse' | 'column' | 'column-reverse';
type JustifyContent = 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
type AlignItems = 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
type FlexWrap = 'nowrap' | 'wrap' | 'wrap-reverse';
type AlignContent = 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'stretch';

interface FlexboxState {
  flexDirection: FlexDirection;
  justifyContent: JustifyContent;
  alignItems: AlignItems;
  flexWrap: FlexWrap;
  alignContent: AlignContent;
  gap: number;
}

const FLEX_DIRECTIONS: FlexDirection[] = ['row', 'row-reverse', 'column', 'column-reverse'];
const JUSTIFY_CONTENT: JustifyContent[] = ['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'space-evenly'];
const ALIGN_ITEMS: AlignItems[] = ['flex-start', 'flex-end', 'center', 'stretch', 'baseline'];
const FLEX_WRAP: FlexWrap[] = ['nowrap', 'wrap', 'wrap-reverse'];
const ALIGN_CONTENT: AlignContent[] = ['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'stretch'];

const ITEM_COLOR = '#6366f1';

export default function FlexboxExplainer() {
  const [state, setState] = useState<FlexboxState>({
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    flexWrap: 'nowrap',
    alignContent: 'stretch',
    gap: 16,
  });

  const [itemCount, setItemCount] = useState(4);

  const updateProperty = <K extends keyof FlexboxState>(property: K, value: FlexboxState[K]) => {
    setState(prev => ({ ...prev, [property]: value }));
  };

  const containerStyle: React.CSSProperties = {
    display: 'flex',
    flexDirection: state.flexDirection,
    justifyContent: state.justifyContent,
    alignItems: state.alignItems,
    flexWrap: state.flexWrap,
    alignContent: state.alignContent,
    gap: `${state.gap}px`,
    width: '100%',
    flex: 1,
    minHeight: '200px',
    background: '#f8f9fa',
    borderRadius: '4px',
    padding: '8px',
    border: '1px dashed #d1d5db',
    boxSizing: 'border-box',
  };

  const generateCSS = () => {
    return `.container {
  display: flex;
  flex-direction: ${state.flexDirection};
  justify-content: ${state.justifyContent};
  align-items: ${state.alignItems};
  flex-wrap: ${state.flexWrap};
  align-content: ${state.alignContent};
  gap: ${state.gap}px;
}`;
  };

  return (
    <div className="flexbox-explainer">
      <div className="explainer-layout">
        {/* Controls Panel */}
        <div className="controls-panel">
          <h3 className="panel-title">Flexbox Properties</h3>
          
          {/* flex-direction */}
          <div className="control-group">
            <label className="control-label">
              flex-direction:
              <span className="property-value">{state.flexDirection}</span>
            </label>
            <div className="button-group">
              {FLEX_DIRECTIONS.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.flexDirection === value ? 'active' : ''}`}
                  onClick={() => updateProperty('flexDirection', value)}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* justify-content */}
          <div className="control-group">
            <label className="control-label">
              justify-content:
              <span className="property-value">{state.justifyContent}</span>
            </label>
            <div className="button-group">
              {JUSTIFY_CONTENT.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.justifyContent === value ? 'active' : ''}`}
                  onClick={() => updateProperty('justifyContent', value)}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* align-items */}
          <div className="control-group">
            <label className="control-label">
              align-items:
              <span className="property-value">{state.alignItems}</span>
            </label>
            <div className="button-group">
              {ALIGN_ITEMS.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.alignItems === value ? 'active' : ''}`}
                  onClick={() => updateProperty('alignItems', value)}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* flex-wrap */}
          <div className="control-group">
            <label className="control-label">
              flex-wrap:
              <span className="property-value">{state.flexWrap}</span>
            </label>
            <div className="button-group">
              {FLEX_WRAP.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.flexWrap === value ? 'active' : ''}`}
                  onClick={() => updateProperty('flexWrap', value)}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* align-content */}
          <div className="control-group">
            <label className="control-label">
              align-content:
              <span className="property-value">{state.alignContent}</span>
            </label>
            <span className="hint">(needs wrap)</span>
            <div className="button-group">
              {ALIGN_CONTENT.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.alignContent === value ? 'active' : ''}`}
                  onClick={() => updateProperty('alignContent', value)}
                  disabled={state.flexWrap === 'nowrap'}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* gap */}
          <div className="control-group">
            <label className="control-label">
              gap:
              <span className="property-value">{state.gap}px</span>
            </label>
            <input
              type="range"
              min="0"
              max="32"
              value={state.gap}
              onChange={(e) => updateProperty('gap', parseInt(e.target.value))}
              className="range-slider"
            />
          </div>

          {/* Item count */}
          <div className="control-group">
            <label className="control-label">
              items:
              <span className="property-value">{itemCount}</span>
            </label>
            <div className="button-group">
              {[3, 4, 5, 6, 8, 10].map(count => (
                <button
                  key={count}
                  className={`control-btn ${itemCount === count ? 'active' : ''}`}
                  onClick={() => setItemCount(count)}
                >
                  {count}
                </button>
              ))}
            </div>
          </div>

        </div>

        {/* Preview Panel */}
        <div className="preview-panel">
          <h3 className="panel-title">Preview</h3>
          
          <div style={containerStyle} className="preview-container">
            {Array.from({ length: itemCount }, (_, i) => {
              const isColumn = state.flexDirection.includes('column');
              const isStretchHeight = state.alignItems === 'stretch' && !isColumn;
              const isStretchWidth = state.alignItems === 'stretch' && isColumn;
              
              return (
                <div
                  key={i}
                  className={`flex-item${isStretchHeight ? ' stretch-height' : ''}${isStretchWidth ? ' stretch-width' : ''}`}
                  style={{
                    backgroundColor: ITEM_COLOR,
                  }}
                >
                  {i + 1}
                </div>
              );
            })}
          </div>
        </div>

        {/* CSS Panel - desktop */}
        <div className="css-panel desktop-only">
          <h3 className="panel-title">Generated CSS</h3>
          <pre className="css-code">{generateCSS()}</pre>
        </div>
      </div>

      {/* Generated CSS - mobile only (below everything) */}
      <div className="css-output mobile-only">
        <h4 className="css-title">Generated CSS</h4>
        <pre className="css-code">{generateCSS()}</pre>
      </div>

      <style>{`
        .flexbox-explainer {
          background: #ffffff;
          border-radius: 8px;
          padding: 12px;
          font-family: system-ui, -apple-system, sans-serif;
          border: 1px solid #e5e7eb;
          box-shadow: 0 1px 3px rgba(0,0,0,0.08);
        }

        .explainer-layout {
          display: grid;
          grid-template-columns: 260px 1fr 220px;
          gap: 12px;
          align-items: stretch;
        }

        .panel-title {
          font-size: 11px;
          font-weight: 600;
          color: #374151;
          margin: 0 0 10px 0;
          padding-bottom: 6px;
          border-bottom: 1px solid #e5e7eb;
          text-transform: uppercase;
          letter-spacing: 0.5px;
        }

        .controls-panel {
          background: #f9fafb;
          border-radius: 6px;
          padding: 10px;
          border: 1px solid #e5e7eb;
        }

        .control-group {
          margin-bottom: 10px;
        }

        .control-group:last-child {
          margin-bottom: 0;
        }

        .control-label {
          display: flex;
          align-items: center;
          gap: 6px;
          font-size: 11px;
          font-weight: 500;
          color: #4f46e5;
          margin-bottom: 5px;
          font-family: 'JetBrains Mono', monospace;
        }

        .property-value {
          font-size: 10px;
          color: #059669;
          background: #ecfdf5;
          padding: 2px 6px;
          border-radius: 3px;
          border: 1px solid #d1fae5;
        }

        .hint {
          font-size: 9px;
          color: #9ca3af;
          font-style: italic;
          font-family: system-ui;
          margin-top: -3px;
          margin-bottom: 3px;
        }

        .button-group {
          display: flex;
          flex-wrap: wrap;
          gap: 4px;
        }

        .control-btn {
          padding: 4px 8px;
          font-size: 10px;
          font-family: 'JetBrains Mono', monospace;
          background: #ffffff;
          color: #4b5563;
          border: 1px solid #d1d5db;
          border-radius: 4px;
          cursor: pointer;
          transition: all 0.1s ease;
          line-height: 1.2;
        }

        .control-btn:hover:not(:disabled) {
          background: #f3f4f6;
          border-color: #9ca3af;
          color: #1f2937;
        }

        .control-btn.active {
          background: #4f46e5;
          color: #ffffff;
          border-color: #4f46e5;
        }

        .control-btn:disabled {
          opacity: 0.4;
          cursor: not-allowed;
        }

        .range-slider {
          width: 100%;
          height: 6px;
          border-radius: 3px;
          background: #e5e7eb;
          outline: none;
          -webkit-appearance: none;
          appearance: none;
        }

        .range-slider::-webkit-slider-thumb {
          -webkit-appearance: none;
          appearance: none;
          width: 14px;
          height: 14px;
          border-radius: 50%;
          background: #4f46e5;
          cursor: pointer;
        }

        .range-slider::-moz-range-thumb {
          width: 14px;
          height: 14px;
          border-radius: 50%;
          background: #4f46e5;
          cursor: pointer;
          border: none;
        }

        .preview-panel {
          display: flex;
          flex-direction: column;
          gap: 8px;
        }

        .preview-panel .panel-title {
          flex-shrink: 0;
        }

        .preview-container {
          overflow: auto;
          flex: 1;
        }

        .flex-item {
          display: flex;
          align-items: center;
          justify-content: center;
          font-weight: 600;
          font-size: 12px;
          color: #ffffff;
          border-radius: 4px;
          transition: all 0.15s ease;
          flex-shrink: 0;
          box-shadow: 0 1px 2px rgba(0,0,0,0.1);
          width: 48px;
          height: 48px;
          min-width: 28px;
          min-height: 28px;
        }

        /* Stretch overrides */
        .flex-item.stretch-height { height: auto; }
        .flex-item.stretch-width { width: auto; }

        /* Desktop/Mobile visibility */
        .desktop-only { display: block; }
        .mobile-only { display: none; }

        /* CSS Panel (desktop - right column) */
        .css-panel {
          background: #ffffff;
          border-radius: 6px;
          padding: 10px;
          border: 1px solid #e5e7eb;
        }

        .css-panel .css-code {
          font-family: 'JetBrains Mono', monospace;
          font-size: 10px;
          line-height: 1.6;
          color: #374151;
          margin: 0;
          white-space: pre-wrap;
          overflow-x: auto;
        }

        /* CSS Output (mobile - below everything) */
        .css-output {
          background: #ffffff;
          border-radius: 4px;
          padding: 8px;
          margin-top: 6px;
          border: 1px solid #e5e7eb;
        }

        .css-title {
          font-size: 9px;
          font-weight: 500;
          color: #6b7280;
          margin: 0 0 6px 0;
          text-transform: uppercase;
          letter-spacing: 0.5px;
        }

        .css-output .css-code {
          font-family: 'JetBrains Mono', monospace;
          font-size: 10px;
          line-height: 1.5;
          color: #374151;
          margin: 0;
          white-space: pre-wrap;
          overflow-x: auto;
        }

        /* Tablet/Mobile - stack vertically, compact */
        @media (max-width: 900px) {
          .flexbox-explainer {
            padding: 6px;
          }

          .explainer-layout {
            grid-template-columns: 1fr;
            gap: 6px;
          }

          .controls-panel {
            order: 2;
            padding: 6px;
          }

          .preview-panel {
            order: 1;
          }

          .panel-title {
            font-size: 10px;
            margin-bottom: 6px;
            padding-bottom: 4px;
          }

          .control-group {
            margin-bottom: 6px;
          }

          .control-label {
            font-size: 10px;
            margin-bottom: 3px;
          }

          .button-group {
            gap: 2px;
          }

          .control-btn {
            padding: 2px 5px;
            font-size: 9px;
          }

          .flex-item {
            width: 32px;
            height: 32px;
            min-width: 24px;
            min-height: 24px;
            font-size: 10px;
          }

          .css-output {
            padding: 6px;
            margin-top: 4px;
          }

          .css-code {
            font-size: 9px;
          }

          .desktop-only { display: none; }
          .mobile-only { display: block; margin-top: 6px; }
        }
      `}</style>
    </div>
  );
}

CSS Grid Explainer

Interactive CSS Grid layout visualizer. Learn grid-template-columns, grid-gap, and placement properties.

grid.tsx 554 lines
import { useState } from 'react';

type JustifyItems = 'start' | 'end' | 'center' | 'stretch';
type AlignItems = 'start' | 'end' | 'center' | 'stretch';
type JustifyContent = 'start' | 'end' | 'center' | 'stretch' | 'space-between' | 'space-around' | 'space-evenly';
type AlignContent = 'start' | 'end' | 'center' | 'stretch' | 'space-between' | 'space-around' | 'space-evenly';

interface GridState {
  columns: string;
  rows: string;
  gap: number;
  justifyItems: JustifyItems;
  alignItems: AlignItems;
  justifyContent: JustifyContent;
  alignContent: AlignContent;
}

const COLUMN_OPTIONS = ['1fr 1fr', '1fr 1fr 1fr', 'repeat(4, 1fr)', '1fr 2fr 1fr', '100px 1fr', 'repeat(3, 100px)'];
const ROW_OPTIONS = ['auto', '1fr 1fr', '100px auto', 'repeat(2, 1fr)', '50px 1fr 50px'];
const JUSTIFY_ITEMS: JustifyItems[] = ['start', 'end', 'center', 'stretch'];
const ALIGN_ITEMS: AlignItems[] = ['start', 'end', 'center', 'stretch'];
const JUSTIFY_CONTENT: JustifyContent[] = ['start', 'end', 'center', 'stretch', 'space-between', 'space-around', 'space-evenly'];
const ALIGN_CONTENT: AlignContent[] = ['start', 'end', 'center', 'stretch', 'space-between', 'space-around', 'space-evenly'];

const ITEM_COLOR = '#6366f1';

export default function GridExplainer() {
  const [state, setState] = useState<GridState>({
    columns: '1fr 1fr 1fr',
    rows: 'auto',
    gap: 16,
    justifyItems: 'stretch',
    alignItems: 'stretch',
    justifyContent: 'start',
    alignContent: 'start',
  });

  const [itemCount, setItemCount] = useState(6);

  const updateProperty = <K extends keyof GridState>(property: K, value: GridState[K]) => {
    setState(prev => ({ ...prev, [property]: value }));
  };

  const containerStyle: React.CSSProperties = {
    display: 'grid',
    gridTemplateColumns: state.columns,
    gridTemplateRows: state.rows,
    gap: `${state.gap}px`,
    justifyItems: state.justifyItems,
    alignItems: state.alignItems,
    justifyContent: state.justifyContent,
    alignContent: state.alignContent,
    width: '100%',
    flex: 1,
    minHeight: '200px',
    background: '#f8f9fa',
    borderRadius: '4px',
    padding: '8px',
    border: '1px dashed #d1d5db',
    boxSizing: 'border-box',
  };

  const generateCSS = () => {
    return `.container {
  display: grid;
  grid-template-columns: ${state.columns};
  grid-template-rows: ${state.rows};
  gap: ${state.gap}px;
  justify-items: ${state.justifyItems};
  align-items: ${state.alignItems};
  justify-content: ${state.justifyContent};
  align-content: ${state.alignContent};
}`;
  };

  return (
    <div className="grid-explainer">
      <div className="explainer-layout">
        {/* Controls Panel */}
        <div className="controls-panel">
          <h3 className="panel-title">Grid Properties</h3>
          
          {/* grid-template-columns */}
          <div className="control-group">
            <label className="control-label">
              grid-template-columns:
              <span className="property-value">{state.columns}</span>
            </label>
            <div className="button-group">
              {COLUMN_OPTIONS.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.columns === value ? 'active' : ''}`}
                  onClick={() => updateProperty('columns', value)}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* grid-template-rows */}
          <div className="control-group">
            <label className="control-label">
              grid-template-rows:
              <span className="property-value">{state.rows}</span>
            </label>
            <div className="button-group">
              {ROW_OPTIONS.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.rows === value ? 'active' : ''}`}
                  onClick={() => updateProperty('rows', value)}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* gap */}
          <div className="control-group">
            <label className="control-label">
              gap:
              <span className="property-value">{state.gap}px</span>
            </label>
            <input
              type="range"
              min="0"
              max="32"
              value={state.gap}
              onChange={(e) => updateProperty('gap', parseInt(e.target.value))}
              className="range-slider"
            />
          </div>

          {/* justify-items */}
          <div className="control-group">
            <label className="control-label">
              justify-items:
              <span className="property-value">{state.justifyItems}</span>
            </label>
            <span className="hint">(horizontal within cell)</span>
            <div className="button-group">
              {JUSTIFY_ITEMS.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.justifyItems === value ? 'active' : ''}`}
                  onClick={() => updateProperty('justifyItems', value)}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* align-items */}
          <div className="control-group">
            <label className="control-label">
              align-items:
              <span className="property-value">{state.alignItems}</span>
            </label>
            <span className="hint">(vertical within cell)</span>
            <div className="button-group">
              {ALIGN_ITEMS.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.alignItems === value ? 'active' : ''}`}
                  onClick={() => updateProperty('alignItems', value)}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* justify-content */}
          <div className="control-group">
            <label className="control-label">
              justify-content:
              <span className="property-value">{state.justifyContent}</span>
            </label>
            <span className="hint">(grid horizontal)</span>
            <div className="button-group">
              {JUSTIFY_CONTENT.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.justifyContent === value ? 'active' : ''}`}
                  onClick={() => updateProperty('justifyContent', value)}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* align-content */}
          <div className="control-group">
            <label className="control-label">
              align-content:
              <span className="property-value">{state.alignContent}</span>
            </label>
            <span className="hint">(grid vertical)</span>
            <div className="button-group">
              {ALIGN_CONTENT.map(value => (
                <button
                  key={value}
                  className={`control-btn ${state.alignContent === value ? 'active' : ''}`}
                  onClick={() => updateProperty('alignContent', value)}
                >
                  {value}
                </button>
              ))}
            </div>
          </div>

          {/* Item count */}
          <div className="control-group">
            <label className="control-label">
              items:
              <span className="property-value">{itemCount}</span>
            </label>
            <div className="button-group">
              {[3, 4, 6, 8, 9, 12].map(count => (
                <button
                  key={count}
                  className={`control-btn ${itemCount === count ? 'active' : ''}`}
                  onClick={() => setItemCount(count)}
                >
                  {count}
                </button>
              ))}
            </div>
          </div>

        </div>

        {/* Preview Panel */}
        <div className="preview-panel">
          <h3 className="panel-title">Preview</h3>
          
          <div style={containerStyle} className="preview-container">
            {Array.from({ length: itemCount }, (_, i) => (
              <div
                key={i}
                className="grid-item"
                style={{
                  backgroundColor: ITEM_COLOR,
                }}
              >
                {i + 1}
              </div>
            ))}
          </div>
        </div>

        {/* CSS Panel - desktop */}
        <div className="css-panel desktop-only">
          <h3 className="panel-title">Generated CSS</h3>
          <pre className="css-code">{generateCSS()}</pre>
        </div>
      </div>

      {/* Generated CSS - mobile only (below everything) */}
      <div className="css-output mobile-only">
        <h4 className="css-title">Generated CSS</h4>
        <pre className="css-code">{generateCSS()}</pre>
      </div>

      <style>{`
        .grid-explainer {
          background: #ffffff;
          border-radius: 8px;
          padding: 12px;
          font-family: system-ui, -apple-system, sans-serif;
          border: 1px solid #e5e7eb;
          box-shadow: 0 1px 3px rgba(0,0,0,0.08);
        }

        .explainer-layout {
          display: grid;
          grid-template-columns: 280px 1fr 240px;
          gap: 12px;
          align-items: stretch;
        }

        .panel-title {
          font-size: 11px;
          font-weight: 600;
          color: #374151;
          margin: 0 0 10px 0;
          padding-bottom: 6px;
          border-bottom: 1px solid #e5e7eb;
          text-transform: uppercase;
          letter-spacing: 0.5px;
        }

        .controls-panel {
          background: #f9fafb;
          border-radius: 6px;
          padding: 10px;
          border: 1px solid #e5e7eb;
        }

        .control-group {
          margin-bottom: 10px;
        }

        .control-group:last-child {
          margin-bottom: 0;
        }

        .control-label {
          display: flex;
          align-items: center;
          gap: 6px;
          font-size: 11px;
          font-weight: 500;
          color: #4f46e5;
          margin-bottom: 5px;
          font-family: 'JetBrains Mono', monospace;
        }

        .property-value {
          font-size: 10px;
          color: #059669;
          background: #ecfdf5;
          padding: 2px 6px;
          border-radius: 3px;
          border: 1px solid #d1fae5;
        }

        .hint {
          font-size: 9px;
          color: #9ca3af;
          font-style: italic;
          font-family: system-ui;
          margin-top: -3px;
          margin-bottom: 3px;
        }

        .button-group {
          display: flex;
          flex-wrap: wrap;
          gap: 4px;
        }

        .control-btn {
          padding: 4px 8px;
          font-size: 10px;
          font-family: 'JetBrains Mono', monospace;
          background: #ffffff;
          color: #4b5563;
          border: 1px solid #d1d5db;
          border-radius: 4px;
          cursor: pointer;
          transition: all 0.1s ease;
          line-height: 1.2;
        }

        .control-btn:hover:not(:disabled) {
          background: #f3f4f6;
          border-color: #9ca3af;
          color: #1f2937;
        }

        .control-btn.active {
          background: #4f46e5;
          color: #ffffff;
          border-color: #4f46e5;
        }

        .control-btn:disabled {
          opacity: 0.4;
          cursor: not-allowed;
        }

        .range-slider {
          width: 100%;
          height: 6px;
          border-radius: 3px;
          background: #e5e7eb;
          outline: none;
          -webkit-appearance: none;
          appearance: none;
        }

        .range-slider::-webkit-slider-thumb {
          -webkit-appearance: none;
          appearance: none;
          width: 14px;
          height: 14px;
          border-radius: 50%;
          background: #4f46e5;
          cursor: pointer;
        }

        .range-slider::-moz-range-thumb {
          width: 14px;
          height: 14px;
          border-radius: 50%;
          background: #4f46e5;
          cursor: pointer;
          border: none;
        }

        .preview-panel {
          display: flex;
          flex-direction: column;
          gap: 8px;
        }

        .preview-panel .panel-title {
          flex-shrink: 0;
        }

        .preview-container {
          overflow: auto;
          flex: 1;
        }

        .grid-item {
          display: flex;
          align-items: center;
          justify-content: center;
          font-weight: 600;
          font-size: 12px;
          color: #ffffff;
          border-radius: 4px;
          transition: all 0.15s ease;
          box-shadow: 0 1px 2px rgba(0,0,0,0.1);
          min-width: 40px;
          min-height: 40px;
          padding: 8px;
        }

        /* Desktop/Mobile visibility */
        .desktop-only { display: block; }
        .mobile-only { display: none; }

        /* CSS Panel (desktop - right column) */
        .css-panel {
          background: #ffffff;
          border-radius: 6px;
          padding: 10px;
          border: 1px solid #e5e7eb;
        }

        .css-panel .css-code {
          font-family: 'JetBrains Mono', monospace;
          font-size: 10px;
          line-height: 1.6;
          color: #374151;
          margin: 0;
          white-space: pre-wrap;
          overflow-x: auto;
        }

        /* CSS Output (mobile - below everything) */
        .css-output {
          background: #ffffff;
          border-radius: 4px;
          padding: 8px;
          margin-top: 6px;
          border: 1px solid #e5e7eb;
        }

        .css-title {
          font-size: 9px;
          font-weight: 500;
          color: #6b7280;
          margin: 0 0 6px 0;
          text-transform: uppercase;
          letter-spacing: 0.5px;
        }

        .css-output .css-code {
          font-family: 'JetBrains Mono', monospace;
          font-size: 10px;
          line-height: 1.5;
          color: #374151;
          margin: 0;
          white-space: pre-wrap;
          overflow-x: auto;
        }

        /* Tablet/Mobile - stack vertically, compact */
        @media (max-width: 900px) {
          .grid-explainer {
            padding: 6px;
          }

          .explainer-layout {
            grid-template-columns: 1fr;
            gap: 6px;
          }

          .controls-panel {
            order: 2;
            padding: 6px;
          }

          .preview-panel {
            order: 1;
          }

          .panel-title {
            font-size: 10px;
            margin-bottom: 6px;
            padding-bottom: 4px;
          }

          .control-group {
            margin-bottom: 6px;
          }

          .control-label {
            font-size: 10px;
            margin-bottom: 3px;
          }

          .button-group {
            gap: 2px;
          }

          .control-btn {
            padding: 2px 5px;
            font-size: 9px;
          }

          .grid-item {
            min-width: 28px;
            min-height: 28px;
            padding: 4px;
            font-size: 10px;
          }

          .css-output {
            padding: 6px;
            margin-top: 4px;
          }

          .css-code {
            font-size: 9px;
          }

          .desktop-only { display: none; }
          .mobile-only { display: block; margin-top: 6px; }
        }
      `}</style>
    </div>
  );
}

Sorting Algorithms

Visual sorting algorithm comparisons. Watch bubble sort, merge sort, quick sort, and more in action.

sorting.tsx 907 lines
import { useState, useEffect, useRef, useCallback } from 'react';

type SortAlgorithm = 'bubble' | 'selection' | 'insertion' | 'merge' | 'quick';

interface ArrayBar {
  value: number;
  state: 'default' | 'comparing' | 'swapping' | 'sorted' | 'pivot';
}

const ALGORITHMS: { id: SortAlgorithm; name: string; complexity: string }[] = [
  { id: 'bubble', name: 'Bubble Sort', complexity: 'O(n²)' },
  { id: 'selection', name: 'Selection Sort', complexity: 'O(n²)' },
  { id: 'insertion', name: 'Insertion Sort', complexity: 'O(n²)' },
  { id: 'merge', name: 'Merge Sort', complexity: 'O(n log n)' },
  { id: 'quick', name: 'Quick Sort', complexity: 'O(n log n)' },
];

const ARRAY_SIZES = [10, 15, 20, 30, 50];
const SPEEDS = [
  { label: 'Slow', value: 200 },
  { label: 'Medium', value: 100 },
  { label: 'Fast', value: 50 },
  { label: 'Instant', value: 10 },
];

function generateArray(size: number): ArrayBar[] {
  const arr: ArrayBar[] = [];
  for (let i = 0; i < size; i++) {
    arr.push({
      value: Math.floor(Math.random() * 100) + 5,
      state: 'default',
    });
  }
  return arr;
}

export default function SortingExplainer() {
  const [algorithm, setAlgorithm] = useState<SortAlgorithm>('bubble');
  const [arraySize, setArraySize] = useState(20);
  const [speed, setSpeed] = useState(100);
  const [array, setArray] = useState<ArrayBar[]>(() => generateArray(20));
  const [isSorting, setIsSorting] = useState(false);
  const [comparisons, setComparisons] = useState(0);
  const [swaps, setSwaps] = useState(0);
  const stopRef = useRef(false);

  const resetArray = useCallback(() => {
    stopRef.current = true;
    setIsSorting(false);
    setArray(generateArray(arraySize));
    setComparisons(0);
    setSwaps(0);
  }, [arraySize]);

  useEffect(() => {
    resetArray();
  }, [arraySize]);

  const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

  const updateArray = (newArray: ArrayBar[]) => {
    setArray([...newArray]);
  };

  // Bubble Sort
  const bubbleSort = async () => {
    const arr = [...array];
    const n = arr.length;
    let compCount = 0;
    let swapCount = 0;

    for (let i = 0; i < n - 1; i++) {
      for (let j = 0; j < n - i - 1; j++) {
        if (stopRef.current) return;

        arr[j].state = 'comparing';
        arr[j + 1].state = 'comparing';
        updateArray(arr);
        compCount++;
        setComparisons(compCount);
        await sleep(speed);

        if (arr[j].value > arr[j + 1].value) {
          arr[j].state = 'swapping';
          arr[j + 1].state = 'swapping';
          updateArray(arr);
          await sleep(speed);

          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
          swapCount++;
          setSwaps(swapCount);
        }

        arr[j].state = 'default';
        arr[j + 1].state = 'default';
      }
      arr[n - i - 1].state = 'sorted';
      updateArray(arr);
    }
    arr[0].state = 'sorted';
    updateArray(arr);
  };

  // Selection Sort
  const selectionSort = async () => {
    const arr = [...array];
    const n = arr.length;
    let compCount = 0;
    let swapCount = 0;

    for (let i = 0; i < n - 1; i++) {
      if (stopRef.current) return;

      let minIdx = i;
      arr[i].state = 'comparing';
      updateArray(arr);

      for (let j = i + 1; j < n; j++) {
        if (stopRef.current) return;

        arr[j].state = 'comparing';
        updateArray(arr);
        compCount++;
        setComparisons(compCount);
        await sleep(speed);

        if (arr[j].value < arr[minIdx].value) {
          if (minIdx !== i) arr[minIdx].state = 'default';
          minIdx = j;
          arr[minIdx].state = 'pivot';
        } else {
          arr[j].state = 'default';
        }
        updateArray(arr);
      }

      if (minIdx !== i) {
        arr[i].state = 'swapping';
        arr[minIdx].state = 'swapping';
        updateArray(arr);
        await sleep(speed);

        [arr[i], arr[minIdx]] = [arr[minIdx], arr[i]];
        swapCount++;
        setSwaps(swapCount);
      }

      arr[i].state = 'sorted';
      if (minIdx !== i) arr[minIdx].state = 'default';
      updateArray(arr);
    }
    arr[n - 1].state = 'sorted';
    updateArray(arr);
  };

  // Insertion Sort
  const insertionSort = async () => {
    const arr = [...array];
    const n = arr.length;
    let compCount = 0;
    let swapCount = 0;

    arr[0].state = 'sorted';
    updateArray(arr);

    for (let i = 1; i < n; i++) {
      if (stopRef.current) return;

      const key = arr[i];
      key.state = 'comparing';
      updateArray(arr);
      await sleep(speed);

      let j = i - 1;
      while (j >= 0 && arr[j].value > key.value) {
        if (stopRef.current) return;

        compCount++;
        setComparisons(compCount);

        arr[j + 1] = arr[j];
        arr[j + 1].state = 'swapping';
        updateArray(arr);
        swapCount++;
        setSwaps(swapCount);
        await sleep(speed);

        arr[j + 1].state = 'sorted';
        j--;
      }
      compCount++;
      setComparisons(compCount);

      arr[j + 1] = key;
      arr[j + 1].state = 'sorted';
      updateArray(arr);
    }
  };

  // Merge Sort
  const mergeSort = async () => {
    const arr = [...array];
    let compCount = 0;
    let swapCount = 0;

    const merge = async (left: number, mid: number, right: number) => {
      const leftArr = arr.slice(left, mid + 1);
      const rightArr = arr.slice(mid + 1, right + 1);

      let i = 0, j = 0, k = left;

      while (i < leftArr.length && j < rightArr.length) {
        if (stopRef.current) return;

        arr[k].state = 'comparing';
        updateArray(arr);
        compCount++;
        setComparisons(compCount);
        await sleep(speed);

        if (leftArr[i].value <= rightArr[j].value) {
          arr[k] = { ...leftArr[i], state: 'swapping' };
          i++;
        } else {
          arr[k] = { ...rightArr[j], state: 'swapping' };
          j++;
        }
        swapCount++;
        setSwaps(swapCount);
        updateArray(arr);
        await sleep(speed);
        arr[k].state = 'default';
        k++;
      }

      while (i < leftArr.length) {
        if (stopRef.current) return;
        arr[k] = { ...leftArr[i], state: 'default' };
        i++;
        k++;
        updateArray(arr);
        await sleep(speed / 2);
      }

      while (j < rightArr.length) {
        if (stopRef.current) return;
        arr[k] = { ...rightArr[j], state: 'default' };
        j++;
        k++;
        updateArray(arr);
        await sleep(speed / 2);
      }
    };

    const sort = async (left: number, right: number) => {
      if (left < right) {
        const mid = Math.floor((left + right) / 2);
        await sort(left, mid);
        await sort(mid + 1, right);
        await merge(left, mid, right);
      }
    };

    await sort(0, arr.length - 1);

    for (let i = 0; i < arr.length; i++) {
      arr[i].state = 'sorted';
    }
    updateArray(arr);
  };

  // Quick Sort
  const quickSort = async () => {
    const arr = [...array];
    let compCount = 0;
    let swapCount = 0;

    const partition = async (low: number, high: number): Promise<number> => {
      const pivot = arr[high];
      pivot.state = 'pivot';
      updateArray(arr);

      let i = low - 1;

      for (let j = low; j < high; j++) {
        if (stopRef.current) return -1;

        arr[j].state = 'comparing';
        updateArray(arr);
        compCount++;
        setComparisons(compCount);
        await sleep(speed);

        if (arr[j].value < pivot.value) {
          i++;
          if (i !== j) {
            arr[i].state = 'swapping';
            arr[j].state = 'swapping';
            updateArray(arr);
            await sleep(speed);

            [arr[i], arr[j]] = [arr[j], arr[i]];
            swapCount++;
            setSwaps(swapCount);
          }
        }

        arr[j].state = 'default';
        if (i >= low) arr[i].state = 'default';
        updateArray(arr);
      }

      arr[high].state = 'swapping';
      arr[i + 1].state = 'swapping';
      updateArray(arr);
      await sleep(speed);

      [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
      swapCount++;
      setSwaps(swapCount);

      arr[i + 1].state = 'sorted';
      arr[high].state = 'default';
      updateArray(arr);

      return i + 1;
    };

    const sort = async (low: number, high: number) => {
      if (low < high && !stopRef.current) {
        const pi = await partition(low, high);
        if (pi === -1) return;
        await sort(low, pi - 1);
        await sort(pi + 1, high);
      }
    };

    await sort(0, arr.length - 1);

    for (let i = 0; i < arr.length; i++) {
      arr[i].state = 'sorted';
    }
    updateArray(arr);
  };

  const startSort = async () => {
    if (isSorting) {
      stopRef.current = true;
      setIsSorting(false);
      return;
    }

    stopRef.current = false;
    setIsSorting(true);
    setComparisons(0);
    setSwaps(0);

    // Reset all states
    const arr = array.map(item => ({ ...item, state: 'default' as const }));
    setArray(arr);

    switch (algorithm) {
      case 'bubble':
        await bubbleSort();
        break;
      case 'selection':
        await selectionSort();
        break;
      case 'insertion':
        await insertionSort();
        break;
      case 'merge':
        await mergeSort();
        break;
      case 'quick':
        await quickSort();
        break;
    }

    setIsSorting(false);
  };

  const selectedAlgo = ALGORITHMS.find(a => a.id === algorithm);

  return (
    <div className="sorting-explainer">
      <div className="explainer-layout">
        {/* Controls Panel */}
        <div className="controls-panel">
          <h3 className="panel-title">Sort Settings</h3>

          {/* Algorithm */}
          <div className="control-group">
            <label className="control-label">
              algorithm:
              <span className="property-value">{selectedAlgo?.name}</span>
            </label>
            <div className="button-group vertical">
              {ALGORITHMS.map(algo => (
                <button
                  key={algo.id}
                  className={`control-btn algo-btn ${algorithm === algo.id ? 'active' : ''}`}
                  onClick={() => !isSorting && setAlgorithm(algo.id)}
                  disabled={isSorting}
                >
                  <span>{algo.name}</span>
                  <span className="complexity">{algo.complexity}</span>
                </button>
              ))}
            </div>
          </div>

          {/* Array Size */}
          <div className="control-group">
            <label className="control-label">
              array size:
              <span className="property-value">{arraySize}</span>
            </label>
            <div className="button-group">
              {ARRAY_SIZES.map(size => (
                <button
                  key={size}
                  className={`control-btn ${arraySize === size ? 'active' : ''}`}
                  onClick={() => !isSorting && setArraySize(size)}
                  disabled={isSorting}
                >
                  {size}
                </button>
              ))}
            </div>
          </div>

          {/* Speed */}
          <div className="control-group">
            <label className="control-label">
              speed:
              <span className="property-value">{SPEEDS.find(s => s.value === speed)?.label}</span>
            </label>
            <div className="button-group">
              {SPEEDS.map(s => (
                <button
                  key={s.value}
                  className={`control-btn ${speed === s.value ? 'active' : ''}`}
                  onClick={() => setSpeed(s.value)}
                >
                  {s.label}
                </button>
              ))}
            </div>
          </div>

          {/* Actions */}
          <div className="control-group">
            <div className="action-buttons">
              <button
                className={`action-btn ${isSorting ? 'stop' : 'start'}`}
                onClick={startSort}
              >
                {isSorting ? 'Stop' : 'Start Sort'}
              </button>
              <button
                className="action-btn reset"
                onClick={resetArray}
                disabled={isSorting}
              >
                New Array
              </button>
            </div>
          </div>

          {/* Stats */}
          <div className="stats">
            <div className="stat">
              <span className="stat-label">Comparisons:</span>
              <span className="stat-value">{comparisons}</span>
            </div>
            <div className="stat">
              <span className="stat-label">Swaps:</span>
              <span className="stat-value">{swaps}</span>
            </div>
          </div>
        </div>

        {/* Preview Panel */}
        <div className="preview-panel">
          <h3 className="panel-title">Visualization</h3>
          
          <div className="array-container">
            {array.map((bar, idx) => (
              <div
                key={idx}
                className={`array-bar ${bar.state}`}
                style={{
                  height: `${bar.value}%`,
                  width: `${100 / array.length - 1}%`,
                }}
              >
                {array.length <= 20 && <span className="bar-value">{bar.value}</span>}
              </div>
            ))}
          </div>

          <div className="legend">
            <div className="legend-item">
              <div className="legend-color default"></div>
              <span>Default</span>
            </div>
            <div className="legend-item">
              <div className="legend-color comparing"></div>
              <span>Comparing</span>
            </div>
            <div className="legend-item">
              <div className="legend-color swapping"></div>
              <span>Swapping</span>
            </div>
            <div className="legend-item">
              <div className="legend-color sorted"></div>
              <span>Sorted</span>
            </div>
          </div>
        </div>

        {/* Info Panel - desktop */}
        <div className="info-panel desktop-only">
          <h3 className="panel-title">Algorithm Info</h3>
          <div className="algo-info">
            <h4>{selectedAlgo?.name}</h4>
            <p className="algo-complexity">Time: {selectedAlgo?.complexity}</p>
            <p className="algo-description">
              {algorithm === 'bubble' && 'Repeatedly steps through the list, compares adjacent elements and swaps them if they are in the wrong order.'}
              {algorithm === 'selection' && 'Finds the minimum element from the unsorted part and puts it at the beginning.'}
              {algorithm === 'insertion' && 'Builds the sorted array one item at a time by inserting each element into its correct position.'}
              {algorithm === 'merge' && 'Divides the array into halves, sorts them recursively, then merges the sorted halves.'}
              {algorithm === 'quick' && 'Picks a pivot element and partitions the array around it, then recursively sorts the partitions.'}
            </p>
          </div>
        </div>
      </div>

      <style>{`
        .sorting-explainer {
          background: #ffffff;
          border-radius: 8px;
          padding: 12px;
          font-family: system-ui, -apple-system, sans-serif;
          border: 1px solid #e5e7eb;
          box-shadow: 0 1px 3px rgba(0,0,0,0.08);
        }

        .explainer-layout {
          display: grid;
          grid-template-columns: 240px 1fr 200px;
          gap: 12px;
          align-items: stretch;
        }

        .panel-title {
          font-size: 11px;
          font-weight: 600;
          color: #374151;
          margin: 0 0 10px 0;
          padding-bottom: 6px;
          border-bottom: 1px solid #e5e7eb;
          text-transform: uppercase;
          letter-spacing: 0.5px;
        }

        .controls-panel {
          background: #f9fafb;
          border-radius: 6px;
          padding: 10px;
          border: 1px solid #e5e7eb;
        }

        .control-group {
          margin-bottom: 12px;
        }

        .control-group:last-child {
          margin-bottom: 0;
        }

        .control-label {
          display: flex;
          align-items: center;
          gap: 6px;
          font-size: 11px;
          font-weight: 500;
          color: #4f46e5;
          margin-bottom: 5px;
          font-family: 'JetBrains Mono', monospace;
        }

        .property-value {
          font-size: 10px;
          color: #059669;
          background: #ecfdf5;
          padding: 2px 6px;
          border-radius: 3px;
          border: 1px solid #d1fae5;
        }

        .button-group {
          display: flex;
          flex-wrap: wrap;
          gap: 4px;
        }

        .button-group.vertical {
          flex-direction: column;
        }

        .control-btn {
          padding: 4px 8px;
          font-size: 10px;
          font-family: 'JetBrains Mono', monospace;
          background: #ffffff;
          color: #4b5563;
          border: 1px solid #d1d5db;
          border-radius: 4px;
          cursor: pointer;
          transition: all 0.1s ease;
          line-height: 1.2;
        }

        .control-btn:hover:not(:disabled) {
          background: #f3f4f6;
          border-color: #9ca3af;
          color: #1f2937;
        }

        .control-btn.active {
          background: #4f46e5;
          color: #ffffff;
          border-color: #4f46e5;
        }

        .control-btn:disabled {
          opacity: 0.4;
          cursor: not-allowed;
        }

        .algo-btn {
          display: flex;
          justify-content: space-between;
          align-items: center;
          width: 100%;
        }

        .complexity {
          font-size: 9px;
          color: #9ca3af;
          font-family: 'JetBrains Mono', monospace;
        }

        .algo-btn.active .complexity {
          color: rgba(255,255,255,0.7);
        }

        .action-buttons {
          display: flex;
          gap: 6px;
        }

        .action-btn {
          flex: 1;
          padding: 8px 12px;
          font-size: 11px;
          font-weight: 600;
          border: none;
          border-radius: 4px;
          cursor: pointer;
          transition: all 0.15s ease;
        }

        .action-btn.start {
          background: #10b981;
          color: white;
        }

        .action-btn.start:hover {
          background: #059669;
        }

        .action-btn.stop {
          background: #ef4444;
          color: white;
        }

        .action-btn.stop:hover {
          background: #dc2626;
        }

        .action-btn.reset {
          background: #6b7280;
          color: white;
        }

        .action-btn.reset:hover:not(:disabled) {
          background: #4b5563;
        }

        .action-btn:disabled {
          opacity: 0.5;
          cursor: not-allowed;
        }

        .stats {
          margin-top: 12px;
          padding: 8px;
          background: #ffffff;
          border-radius: 4px;
          border: 1px solid #e5e7eb;
        }

        .stat {
          display: flex;
          justify-content: space-between;
          font-size: 10px;
          margin-bottom: 4px;
        }

        .stat:last-child {
          margin-bottom: 0;
        }

        .stat-label {
          color: #6b7280;
        }

        .stat-value {
          font-weight: 600;
          color: #374151;
          font-family: 'JetBrains Mono', monospace;
        }

        .preview-panel {
          display: flex;
          flex-direction: column;
        }

        .array-container {
          flex: 1;
          display: flex;
          align-items: flex-end;
          gap: 2px;
          padding: 12px;
          background: #f8f9fa;
          border-radius: 4px;
          border: 1px dashed #d1d5db;
          min-height: 250px;
        }

        .array-bar {
          flex: 1;
          background: #6366f1;
          border-radius: 2px 2px 0 0;
          transition: all 0.1s ease;
          display: flex;
          justify-content: center;
          align-items: flex-start;
          padding-top: 4px;
          min-width: 8px;
        }

        .bar-value {
          font-size: 8px;
          color: white;
          font-weight: 600;
        }

        .array-bar.comparing {
          background: #f59e0b;
        }

        .array-bar.swapping {
          background: #ef4444;
        }

        .array-bar.sorted {
          background: #10b981;
        }

        .array-bar.pivot {
          background: #8b5cf6;
        }

        .legend {
          display: flex;
          gap: 12px;
          margin-top: 8px;
          justify-content: center;
          flex-wrap: wrap;
        }

        .legend-item {
          display: flex;
          align-items: center;
          gap: 4px;
          font-size: 9px;
          color: #6b7280;
        }

        .legend-color {
          width: 12px;
          height: 12px;
          border-radius: 2px;
        }

        .legend-color.default { background: #6366f1; }
        .legend-color.comparing { background: #f59e0b; }
        .legend-color.swapping { background: #ef4444; }
        .legend-color.sorted { background: #10b981; }

        .info-panel {
          background: #ffffff;
          border-radius: 6px;
          padding: 10px;
          border: 1px solid #e5e7eb;
        }

        .algo-info h4 {
          font-size: 12px;
          font-weight: 600;
          color: #374151;
          margin: 0 0 4px 0;
        }

        .algo-complexity {
          font-size: 10px;
          color: #4f46e5;
          font-family: 'JetBrains Mono', monospace;
          margin: 0 0 8px 0;
        }

        .algo-description {
          font-size: 10px;
          color: #6b7280;
          line-height: 1.5;
          margin: 0;
        }

        /* Desktop/Mobile visibility */
        .desktop-only { display: block; }
        .mobile-only { display: none; }

        /* Tablet/Mobile */
        @media (max-width: 900px) {
          .sorting-explainer {
            padding: 6px;
          }

          .explainer-layout {
            grid-template-columns: 1fr;
            gap: 6px;
          }

          .controls-panel {
            order: 2;
            padding: 6px;
          }

          .preview-panel {
            order: 1;
          }

          .panel-title {
            font-size: 10px;
            margin-bottom: 6px;
            padding-bottom: 4px;
          }

          .control-group {
            margin-bottom: 8px;
          }

          .button-group.vertical {
            flex-direction: row;
            flex-wrap: wrap;
          }

          .algo-btn {
            width: auto;
            flex: none;
          }

          .algo-btn .complexity {
            display: none;
          }

          .array-container {
            min-height: 180px;
            padding: 8px;
          }

          .bar-value {
            display: none;
          }

          .desktop-only { display: none; }
          .mobile-only { display: block; }
        }
      `}</style>
    </div>
  );
}

MIT License — Free to use in personal and commercial projects.