DEV Community

Cover image for Agency Level UI and Frontend Craftsmanship on Real World Budgets
Austin Welsh
Austin Welsh

Posted on • Edited on

Agency Level UI and Frontend Craftsmanship on Real World Budgets

👋 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:

  1. Tokens (CSS variables) for spacing, type scale, radii, shadows, colors.
  2. A component boundary: example - buttons/inputs/modals/cards are the only way to build UI.
  3. A11y defaults baked into components (focus rings, aria attributes, keyboard behavior).
  4. 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); } }
Enter fullscreen mode Exit fullscreen mode

Step 2: Use the contract in a component (src/ui/Button.tsx)

# Example: React + TypeScript component boundary
# src/ui/Button.tsx
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Output

check
  âś“ typecheck
  âś“ lint (0 warnings)
  âś“ test
  âś“ a11y (0 critical violations)
  âś“ perf (budgets met)
Enter fullscreen mode Exit fullscreen mode

Notes:

  • Keep it simple: one npm run check that 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, Grid components 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
Enter fullscreen mode Exit fullscreen mode

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)