---
title: "Can You Beat the Stroop Effect? An Interactive Look at Cognitive Interference"
author: "Example Student"
date: "2026-03-23"
description: "Explore simulated Stroop task data with interactive visualisations — tweak the experiment parameters and watch the data respond."
categories: [neuropsychology]
image: cover.svg
toc: true
---
::: {.tldr}
- The Stroop effect is one of the most robust findings in cognitive psychology — naming the ink colour of a colour word is slower when word and ink clash
- The interference effect is visible in both reaction times and error rates
- Below you can tweak the experiment and watch the data change in real time
:::
The Stroop task is beautifully simple. You see a word printed in coloured ink and your job is to name the ink colour, not read the word. When the word RED appears in green ink, saying "green" takes measurably longer than when the word GREEN appears in green ink. That delay — typically 80 to 120 ms — is the Stroop effect, and it reveals something fundamental about how automatic reading interferes with controlled colour naming.
## Experience the interference
The words below cycle through Stroop-like stimuli. Notice how your brain stumbles on the **incongruent** trials — where the word and the ink colour clash — compared to the **congruent** ones.
```{=html}
<div id="stroop-demo" style="
background: #1a1a1a;
border-radius: 12px;
padding: 40px 20px;
text-align: center;
margin: 1.5em 0;
min-height: 180px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
overflow: hidden;
">
<div id="stroop-label" style="
font-size: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: #888;
font-family: 'Fira Code', monospace;
transition: color 0.3s;
">congruent</div>
<div id="stroop-word" style="
font-size: 4.5rem;
font-weight: 800;
letter-spacing: 0.04em;
font-family: 'Source Sans 3', sans-serif;
transition: color 0.15s, opacity 0.15s;
text-shadow: 0 2px 20px rgba(0,0,0,0.5);
">RED</div>
<div id="stroop-hint" style="
font-size: 0.8rem;
color: #555;
font-family: 'Source Sans 3', sans-serif;
margin-top: 4px;
">Try to name the <em>ink colour</em>, not read the word</div>
</div>
<script>
(function() {
const colours = [
{ name: "RED", hex: "#e53935" },
{ name: "GREEN", hex: "#43a047" },
{ name: "BLUE", hex: "#1e88e5" },
{ name: "YELLOW", hex: "#fdd835" },
{ name: "PURPLE", hex: "#8e24aa" }
];
const wordEl = document.getElementById("stroop-word");
const labelEl = document.getElementById("stroop-label");
let step = 0;
function nextTrial() {
// Alternate congruent / incongruent
const isCongruent = step % 2 === 0;
const wordIdx = step % colours.length;
const word = colours[wordIdx];
let ink;
if (isCongruent) {
ink = word;
} else {
// Pick a different colour for the ink
let inkIdx = (wordIdx + 1 + Math.floor(step / 2)) % colours.length;
if (inkIdx === wordIdx) inkIdx = (inkIdx + 1) % colours.length;
ink = colours[inkIdx];
}
// Quick fade
wordEl.style.opacity = "0";
setTimeout(() => {
wordEl.textContent = word.name;
wordEl.style.color = ink.hex;
labelEl.textContent = isCongruent ? "congruent" : "incongruent";
labelEl.style.color = isCongruent ? "#43a047" : "#e53935";
wordEl.style.opacity = "1";
}, 150);
step++;
}
nextTrial();
setInterval(nextTrial, 1800);
})();
</script>
```
Rather than summarise a single paper, this post lets you play with the parameters of a simulated experiment and see how the underlying distributions respond.
## Seeing distributions shift
The plot below shows two probability density curves — one for each condition. Use the sliders to change the separation between them, the spread of each distribution, and the sample size. Watch the curves slide apart, overlap, and sharpen as you adjust.
```{ojs}
//| echo: false
//| panel: input
viewof separation = Inputs.range(
[0, 250],
{ value: 120, step: 5, label: "Effect size (ms separation)" }
)
viewof spread = Inputs.range(
[30, 150],
{ value: 70, step: 5, label: "Variability (SD)" }
)
viewof sampleN = Inputs.range(
[10, 200],
{ value: 80, step: 5, label: "Sample size per condition" }
)
```
```{ojs}
//| echo: false
// Generate density curve points for a normal distribution
densityCurves = {
const baseMean = 620;
const congruentMean = baseMean;
const incongruentMean = baseMean + separation;
const sd = spread;
function gaussian(x, mu, sigma) {
return (1 / (sigma * Math.sqrt(2 * Math.PI))) *
Math.exp(-0.5 * ((x - mu) / sigma) ** 2);
}
const lo = Math.min(congruentMean, incongruentMean) - 4 * sd;
const hi = Math.max(congruentMean, incongruentMean) + 4 * sd;
const step = (hi - lo) / 200;
const points = [];
for (let x = lo; x <= hi; x += step) {
points.push({ x: Math.round(x), y: gaussian(x, congruentMean, sd), condition: "Congruent" });
points.push({ x: Math.round(x), y: gaussian(x, incongruentMean, sd), condition: "Incongruent" });
}
return points;
}
```
```{ojs}
//| echo: false
{
// Split curves by condition so area/line don't connect across them
const congruent = densityCurves.filter(d => d.condition === "Congruent");
const incongruent = densityCurves.filter(d => d.condition === "Incongruent");
return Plot.plot({
height: 400,
marginLeft: 50,
marginBottom: 50,
style: { fontSize: "13px", fontFamily: "Source Sans 3, sans-serif" },
color: {
domain: ["Congruent", "Incongruent"],
range: ["#28a745", "#9B1B30"],
legend: true
},
x: { label: "Reaction time (ms)" },
y: { label: null, axis: null },
marks: [
// Filled areas — one per condition to avoid cross-connection
Plot.areaY(congruent, {
x: "x", y: "y", fill: "#28a745", fillOpacity: 0.2, curve: "basis"
}),
Plot.areaY(incongruent, {
x: "x", y: "y", fill: "#9B1B30", fillOpacity: 0.2, curve: "basis"
}),
// Curve outlines
Plot.lineY(congruent, {
x: "x", y: "y", stroke: "#28a745", strokeWidth: 2.5, curve: "basis"
}),
Plot.lineY(incongruent, {
x: "x", y: "y", stroke: "#9B1B30", strokeWidth: 2.5, curve: "basis"
}),
Plot.ruleY([0])
]
});
}
```
```{ojs}
//| echo: false
// Compute overlap and live stats
overlapStats = {
const baseMean = 620;
const mu1 = baseMean;
const mu2 = baseMean + separation;
const sd = spread;
const d = separation / sd;
// Overlap coefficient (approximate via normal CDF)
function normalCDF(x) {
const t = 1 / (1 + 0.2316419 * Math.abs(x));
const d = 0.3989422804014327;
const p = d * Math.exp(-x * x / 2) * (t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.8212560 + t * 1.3302744)))));
return x > 0 ? 1 - p : p;
}
// OVL for two normals with equal variance
const ovl = 2 * normalCDF(-Math.abs(mu2 - mu1) / (2 * sd));
const ovlPct = (ovl * 100).toFixed(0);
// SE and t-stat
const se = sd * Math.sqrt(2 / sampleN);
const tStat = (separation / se).toFixed(2);
return { d: d.toFixed(2), ovlPct, tStat, mu1, mu2, sd, n: sampleN };
}
```
```{ojs}
//| echo: false
html`<table class="fancy-table">
<thead>
<tr>
<th>Statistic</th>
<th>Value</th>
<th>What it means</th>
</tr>
</thead>
<tbody>
<tr class="congruent">
<td><strong>Cohen's <em>d</em></strong></td>
<td>${overlapStats.d}</td>
<td>${overlapStats.d < 0.2 ? "Negligible" : overlapStats.d < 0.5 ? "Small effect" : overlapStats.d < 0.8 ? "Medium effect" : "Large effect"} — the distance between means in SD units</td>
</tr>
<tr class="incongruent">
<td><strong>Overlap</strong></td>
<td>${overlapStats.ovlPct}%</td>
<td>How much the two distributions share common ground</td>
</tr>
<tr class="neutral">
<td><strong><em>t</em>-statistic</strong></td>
<td>${overlapStats.tStat}</td>
<td>${Math.abs(overlapStats.tStat) > 1.96 ? "Significant at p < .05" : "Not significant"} — signal relative to noise</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3"><em>All values update live. N = ${overlapStats.n} per condition, SD = ${overlapStats.sd} ms.</em></td>
</tr>
</tfoot>
</table>`
```
::: {.callout-tip}
## Things to try
- **Set effect size to 0** — the two curves sit right on top of each other. Overlap is 100%. The *t*-statistic hovers around zero. This is what "no difference" looks like.
- **Crank effect size to 250** — the curves pull apart and the overlap shrinks. Cohen's *d* climbs past 1.0.
- **Increase variability** — even with a large separation, wide distributions overlap more. The signal drowns in the noise.
- **Raise sample size to 200** — the *t*-statistic climbs even though the curves do not move. More data means more certainty, not a bigger effect.
:::
::: {.pullquote}
"Nearly every participant shows a positive interference effect — the Stroop phenomenon is remarkably consistent across individuals."
:::
## Why this matters
The Stroop effect endures because it taps into a fundamental tension in the cognitive system: reading is so automatic that it intrudes even when you are explicitly trying to ignore it. That involuntary interference has made the Stroop task a standard tool for studying attention, executive function, and inhibitory control across clinical and experimental settings.
The interactive above also illustrates something broader: the relationship between effect size, variability, and sample size is not just a statistical abstraction. You can *see* the signal emerge from (or disappear into) the noise as you adjust the sliders. That intuition is harder to build from formulas alone.
---
::: {.author-card}
<div>
<div class="author-card-name">Example Student</div>
<div class="author-card-bio">
Psychology student at Goldsmiths, University of London.
Interested in cognitive psychology and interactive data.
</div>
</div>
:::
*This post is published under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).*