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.
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.
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.
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.