Skip to content

fix(theme): give the field surface a 3:1 border - #340

Merged
alexgrozav merged 2 commits into
mainfrom
field-surface-contrast-uxf-379
Oct 19, 2026
Merged

alexgrozav merged 2 commits into
mainfrom
field-surface-contrast-uxf-379

Conversation

@alexgrozav

Copy link
Copy Markdown
Contributor

Description

People need to see where a field is before they can use it, and WCAG 1.4.11 asks for 3:1 between a control's boundary and the colours next to it. The shared field surface fell short in three places: the border was gray-200 (1.58:1 on white), the ghost variant had no edge at rest and the read-only state differed only by a gray-50 tint (1.09:1). This changes fieldSurfaceCompoundVariants, which reaches useInputRecipe, useTextareaRecipe, useInputDateRecipe and useOtpCellRecipe.

  • Borders. The light colour uses gray-450 at rest and gray-500 on hover in both modes, dark uses gray-500 and gray-450, and neutral follows the mode. Every pair is at least 3.20:1 against the field's fill, the read-only fills and the light and dark page backgrounds.
  • Ghost. The fill and the top, left and right borders stay transparent. The bottom border takes the rest colour, and the hover colour on hover, in the plain and dark slots, and the bottom corners are square.
  • Invalid. The invalid compound also sets borderBottomColor: @color.error in all four slots. It comes after the ghost compounds, so a ghost field that is invalid gets the error underline. In the compiled CSS every border-bottom-color rule follows every border-color rule, so it wins in both scopes.
  • Read-only. Adds borderStyle: @border-style.dashed and keeps the gray-50 fill (gray-800 in dark mode), ghost included.
  • Contrast test. fieldSurfaceContrast.test.ts reads every border token from the recipe, resolves it through useDesignTokensPreset and checks it with culori's wcagContrast. Putting one border back to gray-200 fails it. The culori shim gains the wcagContrast declaration.
  • Docs. The Input, Textarea, Input Date and OTP pages give the new borders in their colour tables and describe the ghost underline and the dashed read-only border. The Input and Textarea Accessibility sections say the border meets 3:1 in both modes.

Related issue

None in this repository.

Type of change

  • 🐞 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that changes existing behavior)
  • 📖 Documentation
  • ♻️ Refactor / internal (no functional change)
  • 🔧 Build / tooling / CI

Checklist

  • My commits follow Conventional Commits with a package scope (e.g. feat(theme): …)
  • I ran pnpm build:nodocs && pnpm lint && pnpm typecheck && pnpm test and everything passes
  • I added a changeset (pnpm changeset) for changes to publishable packages, or this change only touches docs/storybook/app/playground/tests
  • I added or updated tests where relevant
  • I updated documentation where relevant
  • I did not edit generated files (dist/, .styleframe/)
  • My PR targets main and stays focused in scope

Notes / screenshots

What ran: pnpm build:nodocs, pnpm --filter @styleframe/theme test (3409 passed), pnpm typecheck, oxlint and oxfmt --check on the touched TypeScript files, pnpm build:docs and pnpm --filter @styleframe/docs test:build, and the Storybook build. The full pnpm lint and pnpm test didn't run, so that box stays unticked.

In the built Storybook, light and dark, every field computes a visible border at rest. Ghost has a transparent top edge, a gray-450 or gray-500 bottom edge that swaps on hover and a 0px bottom-left radius. Read-only computes a dashed border on the gray-50 or gray-800 fill, and a ghost read-only field computes the fill with a dashed bottom edge. A ghost invalid field computes --color--error on its bottom edge, at rest and on hover.

A ghost field that is invalid shows the error colour on all four sides, as it did before, because the invalid compound also replaces the ghost's transparent borderColor. With the new square bottom corners, that box has rounded top corners and square bottom ones.

🤖 Generated with Claude Code

WCAG 1.4.11 asks for 3:1 between a control's boundary and the colours
next to it. The field border was gray-200 (1.58:1 on white), the ghost
variant had no edge at rest and the read-only state differed only by a
gray-50 tint (1.09:1).

The shared field surface now uses gray-450 and gray-500 for the rest and
hover borders, at least 3.20:1 against each fill, the read-only fills
and the light and dark page backgrounds. Ghost shows a bottom border
with square bottom corners, the invalid state sets the bottom colour so
a ghost field's underline turns to the error colour, and read-only adds
a dashed border. A new contrast test reads the tokens from the recipe
and fails if any border drops below 3:1.

The Input, Textarea, InputDate and OTP pages describe the new surface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
styleframe-dev Ready Ready Preview Oct 19, 2026 7:56am UTC
styleframe-dev-storybook Ready Ready Preview Oct 19, 2026 7:56am UTC

Request Review

The shared surface test now checks all nine color and variant compounds
with toEqual, so a wrong rest or hover token on any of them, or an extra
declaration in a ghost slot, fails it. The Input Date and OTP Ghost
sentences no longer open with inline code, and the Input and Textarea
Ghost sections say "low-chrome layouts" like the other two pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexgrozav
alexgrozav marked this pull request as ready for review October 19, 2026 07:55
@alexgrozav
alexgrozav merged commit dafd9ac into main Oct 19, 2026
14 checks passed
@alexgrozav
alexgrozav deleted the field-surface-contrast-uxf-379 branch October 19, 2026 07:57

This branch was successfully deployed

2 active deployments
Preview – styleframe-dev — 8666880a Deployed Oct 19, 2026 by vercel[bot]
Preview – styleframe-dev-storybook — 8666880a Deployed Oct 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant