The Skill-Signaling Gap
About This Sketch
A generative visualization exploring the tension between building real expertise and signaling competence.
The sketch shows two populations over time: builders who invest in genuine skill development (slow progress, solid cores) and signalers who invest in appearing skilled (fast rise, hollow centers). Despite starting from the same baseline, their trajectories diverge dramatically.
Watch how signalers become larger and rise faster (reflecting their visibility and perceived expertise), while builders remain smaller but develop dense, solid cores (representing actual competence). The visualization captures a central tension in modern professional life: the market rewards signals more than substance in the short term, creating perverse incentives.
This accompanies the blog post "The Skill-Signaling Gap," which explores why performing expertise often has better ROI than developing it, and what this means for individual careers and collective competence.
Algorithm
Pseudocode
SETUP:
Create two populations of people
- Builders (30%): Start with low visibility
- Signalers (70%): Start with higher visibility
All start with zero skill and zero perceived expertise
DRAW (every frame = time passing):
For each builder:
- Actual skill increases significantly (+0.15/frame)
- Perceived skill increases minimally (+0.02/frame)
- Visibility increases very slowly
- Display as solid circle with dense core
For each signaler:
- Actual skill increases minimally (+0.03/frame)
- Perceived skill increases rapidly (+0.25/frame)
- Visibility increases quickly
- Display as hollow circle with tiny core
Position vertically based on perceived skill (not actual skill)
Size based on visibility
Core density based on actual skill
RESULT:
Signalers rise faster, become more visible, but remain hollow
Builders rise slowly, stay smaller, but develop solid competence
The gap between appearance and substance becomes visible over time
Source Code
let sketch = function(p) {
let builders = [];
let signalers = [];
let numPeople = 40;
class Person {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type; // 'builder' or 'signaler'
this.actualSkill = 0;
this.perceivedSkill = 0;
this.timeInvested = 0;
this.visibility = type === 'signaler' ? 0.8 : 0.1;
this.size = 4;
this.targetY = y;
}
develop() {
this.timeInvested += 1;
if (this.type === 'builder') {
// Builders: slow skill growth, very slow perception growth
this.actualSkill += 0.15; // Steady skill gains
this.perceivedSkill += 0.02; // Slow recognition
this.visibility = p.min(0.3, this.visibility + 0.001);
} else {
// Signalers: fast perception growth, slow skill growth
this.actualSkill += 0.03; // Minimal skill gains
this.perceivedSkill += 0.25; // Rapid appearance of competence
this.visibility = p.min(1.0, this.visibility + 0.015);
}
// Position based on perceived skill (what people see)
this.targetY = p.map(this.perceivedSkill, 0, 50, 280, 50);
this.y = p.lerp(this.y, this.targetY, 0.05);
// Size based on visibility
this.size = p.map(this.visibility, 0, 1, 3, 10);
}
display(colors) {
// Opacity based on visibility
let alpha = p.map(this.visibility, 0, 1, 50, 255);
// Color intensity based on actual skill vs perceived skill
let gap = this.perceivedSkill - this.actualSkill;
if (this.type === 'builder') {
// Builders: solid, consistent color (substance)
p.fill(...colors.accent1, alpha);
p.noStroke();
p.circle(this.x, this.y, this.size);
// Inner core showing true skill
let coreAlpha = p.map(this.actualSkill, 0, 50, 50, 200);
p.fill(...colors.accent2, coreAlpha);
p.circle(this.x, this.y, this.size * 0.5);
} else {
// Signalers: hollow, flashy (appearance over substance)
p.noFill();
p.stroke(...colors.accent3, alpha);
p.strokeWeight(2);
p.circle(this.x, this.y, this.size);
// Tiny core showing limited actual skill
p.fill(...colors.accent3, p.map(this.actualSkill, 0, 50, 30, 100));
p.noStroke();
p.circle(this.x, this.y, this.size * 0.2);
}
}
}
p.setup = function() {
p.createCanvas(400, 300);
// Create builders (30% of people)
for (let i = 0; i < numPeople * 0.3; i++) {
let x = p.random(30, 180);
let y = 280;
builders.push(new Person(x, y, 'builder'));
}
// Create signalers (70% of people - reflects reality)
for (let i = 0; i < numPeople * 0.7; i++) {
let x = p.random(220, 370);
let y = 280;
signalers.push(new Person(x, y, 'signaler'));
}
};
p.draw = function() {
const colors = getThemeColors();
p.background(...colors.bg);
// Draw axes
p.stroke(...colors.accent3, 60);
p.strokeWeight(1);
// Horizontal line for starting point
p.line(20, 280, 380, 280);
// Vertical line for skill axis
p.line(200, 40, 200, 285);
// Labels
p.noStroke();
p.fill(...colors.accent3);
p.textAlign(p.CENTER);
p.textSize(10);
p.text('The Skill-Signaling Gap', 200, 20);
// Axis labels
p.textSize(8);
p.textAlign(p.LEFT);
p.fill(...colors.accent3, 180);
p.text('BUILDERS', 30, 295);
p.text('(skill > signal)', 30, 305);
p.textAlign(p.RIGHT);
p.text('SIGNALERS', 370, 295);
p.text('(signal > skill)', 370, 305);
// Y-axis label
p.push();
p.translate(15, 150);
p.rotate(-p.PI/2);
p.textAlign(p.CENTER);
p.text('Perceived Expertise', 0, 0);
p.pop();
// Update and display everyone
for (let person of builders) {
person.develop();
person.display(colors);
}
for (let person of signalers) {
person.develop();
person.display(colors);
}
// Legend
p.textAlign(p.LEFT);
p.textSize(7);
// Builder indicator
p.fill(...colors.accent1, 200);
p.circle(15, 45, 6);
p.fill(...colors.accent2, 150);
p.circle(15, 45, 3);
p.fill(...colors.accent3);
p.text('Builders: slow rise, solid skill', 25, 48);
// Signaler indicator
p.noFill();
p.stroke(...colors.accent3, 200);
p.strokeWeight(1.5);
p.circle(15, 60, 6);
p.fill(...colors.accent3, 100);
p.noStroke();
p.circle(15, 60, 2);
p.fill(...colors.accent3);
p.text('Signalers: fast rise, hollow core', 25, 63);
// Note at bottom
p.textSize(7);
p.fill(...colors.accent3, 150);
p.textAlign(p.CENTER);
p.text('Size = visibility. Position = perceived skill. Core density = actual skill.', 200, 260);
};
};