👋 Let’s Connect! Follow me on GitHub for new projects and tips.
Introduction
“Agency-level” frontend isn’t magic; it’s repeatable constraints: consistent spacing/typography, predictable components, fast interactions, and zero visual regressions. On real budgets, you don’t buy polish with more meetings, you buy it with a small set of enforceable standards, automated checks, and a workflow that makes the “right” thing the easiest thing.
This article focuses on the highest ROI moves: tokens, component contracts, accessibility defaults, performance budgets, and regression tooling that prevents slow decay.
Define “Craft” as Measurable Budgets (Not Opinions)
Treat craftsmanship like SLOs:
- Visual consistency: spacing/typography/colors come from tokens; ad-hoc values are exceptions with justification.
- Accessibility: keyboard support and focus states are non-negotiable; color contrast is validated.
- Performance: set budgets for JS/CSS size, LCP/INP targets, and enforce them in CI.
- Regression resistance: screenshots and lint rules catch drift before it ships.
Practical budgets to start with (tune later):
- LCP: < 2.5s on mid-tier mobile (throttled)
- INP: < 200ms
- Total JS (initial route): < 200–300KB gzip (depends on app)
- CSS: < 50–100KB gzip
- A11y: no critical violations; focus visible everywhere
Build a Small Design System That Enforces Defaults
You don’t need a full design system. You need:
- Tokens (CSS variables) for spacing, type scale, radii, shadows, colors.
- A component boundary: example - buttons/inputs/modals/cards are the only way to build UI.
- A11y defaults baked into components (focus rings, aria attributes, keyboard behavior).
- A “no raw hex/no random spacing” rule enforced by linting and code review.
Example 1: Tokenized UI + Component Contracts (tokens.css + Button.tsx)
Tokenize first, then build components that only accept token-driven variants. This prevents “one-off” styling from multiplying.
Step 1: Create tokens and a button contract (src/styles/tokens.css)
/* src/styles/tokens.css */
:root {
/* Spacing scale (4px base) */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
/* Type */
--font-sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--leading-tight: 1.2;
--leading-normal: 1.5;
/* Radii + shadows */
--radius-2: 0.5rem;
--radius-3: 0.75rem;
--shadow-1: 0 1px 2px rgba(0,0,0,0.08);
--shadow-2: 0 8px 24px rgba(0,0,0,0.12);
/* Colors (use HSL for easier theming) */
--bg: 0 0% 100%;
--fg: 222 22% 12%;
--muted: 220 14% 96%;
--border: 220 13% 90%;
--primary: 222 89% 56%;
--primary-fg: 0 0% 100%;
--danger: 0 84% 60%;
/* Focus ring */
--ring: 222 89% 56%;
--ring-offset: 0 0% 100%;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: 222 22% 10%;
--fg: 0 0% 98%;
--muted: 222 18% 16%;
--border: 222 16% 22%;
--ring-offset: 222 22% 10%;
}
}
* { box-sizing: border-box; }
html { font-family: var(--font-sans); }
body {
margin: 0;
background: hsl(var(--bg));
color: hsl(var(--fg));
line-height: var(--leading-normal);
}
:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* src/ui/button.css */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-2);
border: 1px solid hsl(var(--border));
background: hsl(var(--muted));
color: hsl(var(--fg));
box-shadow: var(--shadow-1);
font-size: var(--text-sm);
line-height: var(--leading-tight);
font-weight: 600;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.btn--primary {
background: hsl(var(--primary));
border-color: hsl(var(--primary));
color: hsl(var(--primary-fg));
}
.btn--danger {
background: hsl(var(--danger));
border-color: hsl(var(--danger));
color: hsl(var(--primary-fg));
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn__spinner {
width: 1em;
height: 1em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 999px;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
Step 2: Use the contract in a component (src/ui/Button.tsx)
# Example: React + TypeScript component boundary
# src/ui/Button.tsx
Expected Output
A Button component that only allows approved variants/sizes, always has a visible focus state,
and prevents ad-hoc styling from leaking into product code.
Notes:
- The “contract” is the key: product code should not pass raw colors, random padding, or custom shadows.
- If you must allow overrides, allow token-based overrides (e.g.,
tone="primary" | "danger"), not arbitrary CSS.
Example 2: CI Quality Gates (lint + typecheck + a11y + perf budgets)
Automate the boring parts. If “craft” isn’t enforced, it will regress under deadlines.
Add a single command that fails the build when quality drops
{
"scripts": {
"check": "npm run typecheck && npm run lint && npm run test && npm run a11y && npm run perf",
"typecheck": "tsc -p tsconfig.json --noEmit",
"lint": "eslint . --max-warnings=0",
"test": "vitest run",
"a11y": "node ./scripts/a11y-smoke.mjs",
"perf": "node ./scripts/perf-budgets.mjs"
}
}
Output
check
âś“ typecheck
âś“ lint (0 warnings)
âś“ test
âś“ a11y (0 critical violations)
âś“ perf (budgets met)
Notes:
- Keep it simple: one
npm run checkthat devs can run locally and CI can enforce. - Don’t boil the ocean. Start with a11y smoke checks on key routes/components and a small set of performance budgets.
Solution: A “Craft Stack” You Can Afford
Ship agency-grade UI by standardizing the stack and enforcing it:
- Tokens: CSS variables (or a token pipeline) + strict usage rules.
- Components: small, well-typed primitives (Button, Input, Select, Modal, Tooltip, Card, Stack).
-
Layout primitives:
Stack,Inline,Gridcomponents to eliminate random spacing. - A11y defaults: focus-visible, reduced motion, keyboard interactions.
- Performance hygiene: route-level code splitting, image sizing, font loading strategy, and bundle budgets.
- Regression tooling: Chromatic/Percy (paid) or Playwright screenshots (self-hosted) for critical flows.
# Minimal “craft stack” install (example)
npm i -D eslint typescript vitest @playwright/test lighthouse
Solution notes:
- If budget is tight, prioritize Playwright screenshots + Lighthouse budgets over fancy tooling.
- The biggest ROI is preventing drift: tokens + component boundary + CI gates.
Key Takeaways
- Define craftsmanship as enforceable budgets (a11y, performance, consistency), not subjective taste.
- Use tokens + component contracts to eliminate one-off styling and reduce UI entropy.
- Add CI gates early (typecheck, lint, a11y smoke, perf budgets) to keep quality stable under deadlines.
Conclusion
Agency-level polish on real budgets comes from constraints that scale: tokenized design decisions, components that encode best practices, and automated checks that stop regressions. Start small, enforce relentlessly, and expand only when the system is paying for itself.
Meta Description
A pragmatic playbook for shipping polished, agency-grade UI with limited time and money: design tokens, component contracts, performance budgets, and automated quality gates.
TLDR - Highlights for Skimmers
- Tokenize spacing/type/color and forbid ad-hoc values outside exceptions.
- Build a small set of primitives with strict props and baked-in a11y defaults.
- Enforce quality with one CI command: lint + typecheck + a11y smoke + perf budgets.
What’s the one UI regression you keep re-fixing that should become a token, component, or CI gate?
Top comments (0)