Zine Photocopy
Third-generation photocopy: blown-out contrast, dashed cuts, shouty labels.
Construction · Halftone
Every tone is a dot screen coarse enough to count, printed inside a heavy ink outline. No smooth fills anywhere.

Concept shot — generated from this style's terms, the way a designer would present it. The specimen below is the contract: rendered from the exact tokens on this page.
Every style renders this same screen, so the only thing changing between two of them is the style. The component above is the file below, running.
Prompt
Design this interface in the Zine Photocopy style. Everything looks photocopied one time too many — pure black on grey, dashed cut lines, labels shouting in uppercase. Nothing is subtle and nothing is smooth. Build every surface the Halftone way: Every tone is a dot screen coarse enough to count, printed inside a heavy ink outline. No smooth fills anywhere. Use exactly these values: background #e8e8e6, surfaces #f7f7f5, text #0d0d0d, secondary text #4f4f4f, accent #111111 with #f7f7f5 on it; corner radius 0/0/0px; 2px dashed borders; no shadows of any kind; Helvetica Neue for body at 14px, line height 1.35; 4px spacing grid, compact rows; labels in uppercase with wide letter spacing. Never: gradients or soft shadows; rounded corners; pastel color; delicate hairlines. Dark mode uses background #0c0c0c, surfaces #151515, text #f7f7f5, accent #f7f7f5 — the same style, not an inverted one.
The numbers in this prompt are printed from the tokens below, so it cannot drift from what the specimen renders.
What carries the look
- photocopy contrast
- dashed cut lines
- shouting uppercase
- no grey subtlety
What this style refuses
- gradients or soft shadows
- rounded corners
- pastel color
- delicate hairlines
The refusals are the half that keeps generated interfaces from converging — a prompt that only says what to add gets the default look.
Tokens
Palette
#e8e8e6#0c0c0c#f7f7f5#151515#111111#f0f0f0#0d0d0d#f7f7f5#4f4f4f#9e9e9e#111111#f7f7f5#f7f7f5#0c0c0cShape and type
Measured contrast — body 15.84:1 light, 18.24:1 dark; secondary text 6.68:1 and 7.3:1. Every pair clears WCAG AA in both themes, checked on each build.
Specimen · Specimen.tsx
import type { CSSProperties } from "react";
/**
* Vibary · Zine Photocopy
*
* Third-generation photocopy: blown-out contrast, dashed cuts, shouty labels.
*
* One complete product screen, rendered from the token object below —
* the same screen every other Vibary specimen renders, so the style
* is the only variable. Self-contained: React only, no CSS file, no
* assets, no chart library.
*
* `theme` defaults to following the system setting; pass "light" or
* "dark" to pin one.
*/
export const TOKENS = {
"material": "halftone",
"color": {
"light": {
"bg": "#e8e8e6",
"surface": "#f7f7f5",
"border": "#111111",
"text": "#0d0d0d",
"muted": "#4f4f4f",
"accent": "#111111",
"accentText": "#f7f7f5"
},
"dark": {
"bg": "#0c0c0c",
"surface": "#151515",
"border": "#f0f0f0",
"text": "#f7f7f5",
"muted": "#9e9e9e",
"accent": "#f7f7f5",
"accentText": "#0c0c0c"
}
},
"radius": {
"sm": 0,
"md": 0,
"lg": 0,
"pill": 999
},
"border": {
"width": 2,
"style": "dashed"
},
"shadow": {
"level": "none",
"value": "none"
},
"type": {
"family": {
"display": "Impact, \"Haettenschweiler\", \"Arial Black\", sans-serif",
"body": "\"Helvetica Neue\", Helvetica, Arial, \"Liberation Sans\", sans-serif",
"mono": "\"Courier New\", Courier, \"Nimbus Mono PS\", monospace"
},
"scale": [
10,
12,
14,
17,
24,
36
],
"weight": {
"body": 500,
"strong": 800,
"display": 900
},
"lineHeight": 1.35,
"tracking": 0
},
"space": {
"unit": 4,
"density": "compact"
},
"effects": {
"surface": "grain",
"accentStyle": "block",
"separator": "dashed",
"labelCase": "upper",
"blur": 0
}
};
type Palette = typeof TOKENS.color.light;
export type ZinePhotocopySpecimenProps = {
theme?: "system" | "light" | "dark";
/** Scopes the custom properties; only matters if you render two. */
scope?: string;
};
function vars(palette: Palette): string {
return [
`--sp-bg: ${palette.bg}`,
`--sp-surface: ${palette.surface}`,
`--sp-border: ${palette.border}`,
`--sp-text: ${palette.text}`,
`--sp-muted: ${palette.muted}`,
`--sp-accent: ${palette.accent}`,
`--sp-accent-text: ${palette.accentText}`,
].join("; ");
}
/** Fixed content: identical in every specimen, on purpose. */
const NAV: readonly { label: string; path: string }[] = [
{ label: "Overview", path: "M3 9.5 9 4.5l6 5V15a1 1 0 0 1-1 1h-2.5v-4h-5v4H4a1 1 0 0 1-1-1z" },
{ label: "Orders", path: "M4 5h10l-1 9H5zM7 5V3.5h4V5" },
{ label: "Customers", path: "M9 9a2.6 2.6 0 1 0 0-5.2A2.6 2.6 0 0 0 9 9M3.6 15.4c0-2.4 2.4-3.8 5.4-3.8s5.4 1.4 5.4 3.8" },
{ label: "Reports", path: "M4.5 15V8M9 15V4.5M13.5 15v-4.5" },
{ label: "Settings", path: "M9 11.4A2.4 2.4 0 1 0 9 6.6a2.4 2.4 0 0 0 0 4.8M9 2.6v1.4M9 14v1.4M15.4 9H14M4 9H2.6M13.5 4.5l-1 1M5.5 12.5l-1 1M13.5 13.5l-1-1M5.5 5.5l-1-1" },
];
const STATS: readonly { label: string; value: string; delta: string; up: boolean }[] = [
{ label: "Revenue", value: "$48,290", delta: "+12.4%", up: true },
{ label: "Orders", value: "1,284", delta: "+3.1%", up: true },
{ label: "Refunds", value: "42", delta: "-1.8%", up: false },
];
const BARS: readonly { day: string; value: number }[] = [
{ day: "M", value: 0.42 },
{ day: "T", value: 0.61 },
{ day: "W", value: 0.38 },
{ day: "T", value: 0.74 },
{ day: "F", value: 0.93 },
{ day: "S", value: 0.55 },
{ day: "S", value: 0.29 },
];
const ROWS: readonly { name: string; meta: string; state: string; amount: string }[] = [
{ name: "Kyoto Print Co.", meta: "Invoice 2841", state: "Paid", amount: "420.00" },
{ name: "Aurora Supply", meta: "Invoice 2839", state: "Pending", amount: "186.50" },
{ name: "Northwind Studio", meta: "Invoice 2836", state: "Overdue", amount: "678.00" },
];
const SWITCHES: readonly { label: string; on: boolean }[] = [
{ label: "Auto-pay invoices", on: true },
{ label: "Email receipts", on: false },
];
/**
* The material: how this style paints an element, as opposed to what
* color it paints it. This is the part that makes Halftone
* a different design language rather than a different theme —
* Every tone is a dot screen coarse enough to count, printed inside a heavy ink outline. No smooth fills anywhere.
*/
const MATERIAL = {
"surface": {
"background": "radial-gradient(color-mix(in srgb, var(--sp-text) 12%, transparent) 1px, transparent 1.2px) 0 0 / 5px 5px, var(--sp-surface)",
"boxShadow": "none",
"border": "2px solid var(--sp-text)"
},
"button": {
"background": "radial-gradient(color-mix(in srgb, #ffffff 45%, transparent) 1px, transparent 1.2px) 0 0 / 4px 4px, var(--sp-accent)",
"color": "var(--sp-accent-text)",
"boxShadow": "none",
"border": "2px solid var(--sp-text)"
},
"switchOn": {
"background": "var(--sp-accent)",
"boxShadow": "none",
"border": "2px solid var(--sp-text)"
},
"switchOff": {
"background": "radial-gradient(color-mix(in srgb, var(--sp-text) 30%, transparent) 1px, transparent 1.2px) 0 0 / 4px 4px, var(--sp-surface)",
"boxShadow": "none",
"border": "2px solid var(--sp-text)"
},
"knob": {
"background": "var(--sp-text)",
"boxShadow": "none"
},
"navActive": {
"background": "var(--sp-accent)",
"boxShadow": "none"
},
"bar": {
"background": "radial-gradient(color-mix(in srgb, #ffffff 40%, transparent) 1px, transparent 1.2px) 0 0 / 4px 4px, var(--sp-accent)",
"boxShadow": "none",
"border": "2px solid var(--sp-text)"
},
"texture": "radial-gradient(color-mix(in srgb, #000000 14%, transparent) 1px, transparent 1.2px) 0 0 / 5px 5px"
};
const SURFACE_BORDER = `${TOKENS.border.width}px dashed var(--sp-border)`;
const ROW_SEPARATOR = "1px dashed var(--sp-border)";
const RING = 0.68;
export function ZinePhotocopySpecimen({ theme = "system", scope = "sp-zine-photocopy" }: ZinePhotocopySpecimenProps) {
const css =
theme === "light"
? `.${scope}{${vars(TOKENS.color.light)}}`
: theme === "dark"
? `.${scope}{${vars(TOKENS.color.dark)}}`
: `.${scope}{${vars(TOKENS.color.light)}}@media(prefers-color-scheme:dark){.${scope}{${vars(TOKENS.color.dark)}}}`;
const label: CSSProperties = {
font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.body}`,
letterSpacing: "0.08em",
textTransform: "uppercase",
color: "var(--sp-muted)",
};
const meta: CSSProperties = {
font: `${TOKENS.type.weight.body} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.body}`,
color: "var(--sp-muted)",
};
const card: CSSProperties = {
...MATERIAL.surface,
borderRadius: TOKENS.radius.md,
padding: 12,
};
const heading: CSSProperties = {
font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[2]}px ${TOKENS.type.family.display}`,
textTransform: "uppercase",
letterSpacing: "0.05em",
};
const primary: CSSProperties = { ...MATERIAL.button, borderRadius: TOKENS.radius.sm };
const pill = (active: boolean): CSSProperties => ({
padding: "2px 8px",
borderRadius: TOKENS.radius.pill,
background: active
? "var(--sp-accent)"
: "color-mix(in srgb, var(--sp-text) 10%, transparent)",
color: active ? "var(--sp-accent-text)" : "var(--sp-muted)",
font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.body}`,
textTransform: "uppercase",
letterSpacing: "0.07em",
});
return (
<div
className={scope}
style={{
width: 760,
display: "flex",
background: "radial-gradient(color-mix(in srgb, var(--sp-text) 10%, transparent) 0.5px, transparent 0.5px) 0 0 / 3px 3px, radial-gradient(color-mix(in srgb, var(--sp-text) 7%, transparent) 0.5px, transparent 0.5px) 1.5px 1.5px / 4px 4px, var(--sp-bg)",
color: "var(--sp-text)",
font: `${TOKENS.type.weight.body} ${TOKENS.type.scale[1]}px/${TOKENS.type.lineHeight} ${TOKENS.type.family.body}`,
letterSpacing: `${TOKENS.type.tracking}em`,
overflow: "hidden",
position: "relative",
}}
>
<style>{css}</style>
<span
aria-hidden="true"
style={{
position: "absolute",
inset: 0,
pointerEvents: "none",
background: MATERIAL.texture,
}}
/>
{/* Sidebar */}
<div
style={{
width: 176,
flex: "none",
display: "flex",
flexDirection: "column",
gap: 4,
padding: 14,
borderRight: `${TOKENS.border.width}px dashed var(--sp-border)`,
background: "color-mix(in srgb, var(--sp-surface) 70%, transparent)",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
<span
style={{
width: 22,
height: 22,
borderRadius: TOKENS.radius.sm,
background: "var(--sp-accent)",
display: "grid",
placeItems: "center",
color: "var(--sp-accent-text)",
font: `${TOKENS.type.weight.display} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.display}`,
}}
>
N
</span>
<span
style={{
font: `${TOKENS.type.weight.display} ${TOKENS.type.scale[2]}px ${TOKENS.type.family.display}`,
textTransform: "uppercase",
letterSpacing: "0.06em",
}}
>
Northwind
</span>
</div>
{NAV.map((item, index) => (
<span
key={item.label}
style={{
display: "flex",
alignItems: "center",
gap: 9,
height: 30,
padding: "0 9px",
borderRadius: TOKENS.radius.sm,
...(index === 0 ? MATERIAL.navActive : null),
color: index === 0 ? "var(--sp-accent)" : "var(--sp-muted)",
font: `${index === 0 ? TOKENS.type.weight.strong : TOKENS.type.weight.body} ${TOKENS.type.scale[1]}px ${TOKENS.type.family.body}`,
textTransform: "uppercase",
letterSpacing: "0.07em",
}}
>
<svg width="17" height="17" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<path
d={item.path}
stroke="currentColor"
strokeWidth={index === 0 ? 1.7 : 1.3}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{item.label}
</span>
))}
<span style={{ flex: 1 }} />
<span
style={{
display: "flex",
alignItems: "center",
gap: 8,
paddingTop: 10,
borderTop: `${TOKENS.border.width}px dashed var(--sp-border)`,
}}
>
<span
style={{
width: 26,
height: 26,
flex: "none",
display: "grid",
placeItems: "center",
borderRadius: TOKENS.radius.pill,
background: "color-mix(in srgb, var(--sp-text) 12%, transparent)",
font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.body}`,
}}
>
MK
</span>
<span style={{ minWidth: 0 }}>
<span style={{ display: "block", font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.body}` }}>
Mika Ito
</span>
<span style={{ ...meta, display: "block" }}>Finance</span>
</span>
</span>
</div>
{/* Main */}
<div
style={{
flex: 1,
minWidth: 0,
display: "flex",
flexDirection: "column",
gap: 10,
padding: 14,
}}
>
{/* Header */}
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ flex: 1, minWidth: 0 }}>
<span style={{ ...label, display: "block" }}>March 2026</span>
<span
style={{
display: "block",
font: `${TOKENS.type.weight.display} ${TOKENS.type.scale[4]}px ${TOKENS.type.family.display}`,
textTransform: "uppercase",
letterSpacing: "0.04em",
}}
>
Overview
</span>
</span>
<span
style={{
display: "flex",
alignItems: "center",
gap: 7,
padding: "7px 11px",
background: "var(--sp-surface)",
border: `${TOKENS.border.width}px dashed var(--sp-border)`,
borderRadius: TOKENS.radius.sm,
color: "var(--sp-muted)",
font: `${TOKENS.type.weight.body} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.body}`,
}}
>
<svg width="13" height="13" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<circle cx="8" cy="8" r="5.4" stroke="currentColor" strokeWidth="1.5" />
<path d="M12.4 12.4 16 16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
Search
</span>
<span
style={{
...primary,
padding: "8px 13px",
font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.body}`,
textTransform: "uppercase",
letterSpacing: "0.07em",
}}
>
New report
</span>
</div>
{/* Stats */}
<div style={{ display: "flex", gap: 10 }}>
{STATS.map((stat) => (
<div key={stat.label} style={{ ...card, flex: 1, minWidth: 0 }}>
<span style={{ ...label, display: "block" }}>{stat.label}</span>
<span
style={{
display: "block",
marginTop: 3,
fontVariantNumeric: "tabular-nums",
font: `${TOKENS.type.weight.display} ${TOKENS.type.scale[4]}px ${TOKENS.type.family.display}`,
}}
>
{stat.value}
</span>
<span style={{ display: "inline-flex", marginTop: 6, ...pill(stat.up) }}>
{stat.delta}
</span>
</div>
))}
</div>
{/* Chart + ring */}
<div style={{ display: "flex", gap: 10 }}>
<div style={{ ...card, flex: 1.6, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between" }}>
<span style={heading}>Revenue by day</span>
<span style={meta}>7 days</span>
</div>
<div
style={{
display: "flex",
alignItems: "flex-end",
gap: 7,
height: 78,
marginTop: 10,
}}
>
{BARS.map((bar, index) => (
<span key={index} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 5 }}>
<span
style={{
width: "100%",
height: bar.value * 60,
borderRadius: `${TOKENS.radius.sm}px ${TOKENS.radius.sm}px 0 0`,
...(index === 4
? MATERIAL.bar
: {
background: "color-mix(in srgb, var(--sp-accent) 32%, var(--sp-surface))",
border: SURFACE_BORDER,
}),
}}
/>
<span style={meta}>{bar.day}</span>
</span>
))}
</div>
</div>
<div style={{ ...card, flex: 1, minWidth: 0, display: "flex", alignItems: "center", gap: 12 }}>
<span
style={{
width: 74,
height: 74,
flex: "none",
borderRadius: TOKENS.radius.pill,
display: "grid",
placeItems: "center",
background: `conic-gradient(var(--sp-accent) 0turn ${RING}turn, color-mix(in srgb, var(--sp-text) 12%, transparent) ${RING}turn 1turn)`,
}}
>
<span
style={{
width: 52,
height: 52,
borderRadius: TOKENS.radius.pill,
background: "var(--sp-surface)",
display: "grid",
placeItems: "center",
fontVariantNumeric: "tabular-nums",
font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[2]}px ${TOKENS.type.family.mono}`,
}}
>
68%
</span>
</span>
<span style={{ minWidth: 0 }}>
<span style={{ ...heading, display: "block" }}>Budget</span>
<span style={{ ...meta, display: "block", marginTop: 4 }}>Spent $34,120</span>
<span style={{ ...meta, display: "block" }}>Left $16,170</span>
</span>
</div>
</div>
{/* Table + controls */}
<div style={{ display: "flex", gap: 10 }}>
<div style={{ ...card, flex: 1.6, minWidth: 0, padding: 0, overflow: "hidden" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: `${12}px ${12}px 8px`,
}}
>
<span style={heading}>Recent orders</span>
<span style={{ color: "var(--sp-accent)", font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.body}` }}>
View all
</span>
</div>
{ROWS.map((row, index) => (
<div
key={row.name}
style={{
display: "flex",
alignItems: "center",
gap: 10,
height: 38,
padding: `0 ${12}px`,
borderTop: ROW_SEPARATOR,
}}
>
<span
style={{
width: 24,
height: 24,
flex: "none",
display: "grid",
placeItems: "center",
borderRadius: TOKENS.radius.sm,
background: "color-mix(in srgb, var(--sp-text) 9%, transparent)",
font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.mono}`,
}}
>
{row.name.slice(0, 1)}
</span>
<span style={{ flex: 1, minWidth: 0 }}>
<span style={{ display: "block", font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[1]}px ${TOKENS.type.family.body}` }}>
{row.name}
</span>
<span style={{ ...meta, display: "block" }}>{row.meta}</span>
</span>
<span style={pill(index === 0)}>{row.state}</span>
<span
style={{
width: 66,
textAlign: "right",
fontVariantNumeric: "tabular-nums",
font: `${TOKENS.type.weight.strong} ${TOKENS.type.scale[1]}px ${TOKENS.type.family.mono}`,
}}
>
{row.amount}
</span>
</div>
))}
</div>
<div style={{ ...card, flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 10 }}>
<span style={heading}>Preferences</span>
{SWITCHES.map((item) => (
<span key={item.label} style={{ display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ flex: 1, minWidth: 0, font: `${TOKENS.type.weight.body} ${TOKENS.type.scale[0]}px ${TOKENS.type.family.body}` }}>
{item.label}
</span>
<span
style={{
width: 34,
height: 20,
flex: "none",
position: "relative",
borderRadius: TOKENS.radius.pill,
...(item.on ? MATERIAL.switchOn : MATERIAL.switchOff),
}}
>
<span
style={{
position: "absolute",
top: 3,
left: item.on ? 17 : 3,
width: 14,
height: 14,
borderRadius: TOKENS.radius.pill,
...MATERIAL.knob,
}}
/>
</span>
</span>
))}
<span style={{ marginTop: 2 }}>
<span style={{ display: "flex", justifyContent: "space-between" }}>
<span style={meta}>Alert threshold</span>
<span style={{ ...meta, fontVariantNumeric: "tabular-nums" }}>62%</span>
</span>
<span
style={{
display: "block",
position: "relative",
height: 5,
marginTop: 7,
borderRadius: TOKENS.radius.pill,
background: "color-mix(in srgb, var(--sp-text) 14%, transparent)",
}}
>
<span
style={{
position: "absolute",
inset: "0 38% 0 0",
borderRadius: TOKENS.radius.pill,
background: "var(--sp-accent)",
}}
/>
<span
style={{
position: "absolute",
top: -5,
left: "62%",
width: 15,
height: 15,
marginLeft: -7,
borderRadius: TOKENS.radius.pill,
background: "var(--sp-surface)",
border: `2px solid var(--sp-accent)`,
}}
/>
</span>
</span>
</div>
</div>
</div>
</div>
);
}
export default ZinePhotocopySpecimen;