Skip to main content

shade-inputsurface-recipe

Use the inputSurface() recipe for form-control chrome (border, background, radius, focus ring, invalid state) — don't roll your own. Trigger when editing form-control-shaped files in Shade.

Source facts

Repository
TryGhost/Ghost
Last source activity
October 11, 2026 at 08:53
Detected SKILL.md language
English
Stars
55,544
Forks
11,995

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
Shade inputSurface recipe
description
Use the inputSurface() recipe for form-control chrome (border, background, radius, focus ring, invalid state) — don't roll your own. Trigger when editing form-control-shaped files in Shade.
autoTrigger
[{"fileEdit":"apps/shade/src/components/ui/{input,textarea,input-group,select,combobox,multi-select-combobox,dropzone,calendar,command}.tsx"},{"fileEdit":"apps/shade/src/components/ui/{input-group,dropzone}-variants.ts"}]
# Shade — `inputSurface` recipe Form controls share one visual rule: border, background, radius, transition, focus ring, invalid state. That rule lives in **`inputSurface`** (`apps/shade/src/components/ui/input-surface.ts`) and is used by `Input`, `Textarea`, `InputGroup`, and the `Select` trigger. Don't duplicate the chrome — compose the recipe. ```ts import {inputSurface, inputSurfaceClasses} from '@/components/ui/input-surface'; import {cn} from '@/lib/utils'; ``` ## Two common modes ### `inputSurface('self')` — directly on the focusable element Use on `<input>`, `<textarea>`, or any element that itself receives focus. ```tsx <input className={cn( inputSurface('self'), 'flex h-9 w-full px-3 py-1 text-control placeholder:text-muted-foreground', className )} {...props} /> ``` Covers: base chrome + `focus-visible:` ring + `aria-[invalid=true]` styling + `disabled:` opacity. ### `inputSurface('within')` — on a wrapper containing a focusable child Use when the styled element is the wrapper (e.g. `InputGroup`) and focus state should react to **any** focusable descendant via `:has()`. ```tsx <div className={cn( inputSurface('within'), 'flex h-9 items-center gap-2 px-3', className )}> <Icon /> <input className="bg-transparent outline-hidden focus:outline-hidden" /> </div> ``` Covers: base chrome + `has-[:focus-visible]:` ring + `has-[[aria-invalid=true]]:` styling. (No disabled atom — wrappers don't get a `disabled` HTML attribute.) ## Edge case — only one specific child triggers focus state When `'within'` is too broad (e.g. a wrapper with multiple focusables but only one should drive chrome), compose atoms manually so Tailwind's JIT can statically detect the class string: ```tsx import {inputSurfaceClasses} from '@/components/ui/input-surface'; <div className={cn( inputSurfaceClasses.base, inputSurfaceClasses.invalidWithin, // Literal class string — Tailwind needs to see it as text 'has-[[data-slot=control]:focus-visible]:border-focus-ring', 'has-[[data-slot=control]:focus-visible]:ring-2', 'has-[[data-slot=control]:focus-visible]:ring-focus-ring/25' )} /> ``` Available atoms: - `inputSurfaceClasses.base` — border, background, radius, transition - `inputSurfaceClasses.focusSelf` — focus chrome for self mode - `inputSurfaceClasses.focusWithin` — focus chrome for within mode - `inputSurfaceClasses.invalidSelf` — `aria-[invalid=true]` styling on self - `inputSurfaceClasses.invalidWithin` — `aria-[invalid=true]` styling on a descendant - `inputSurfaceClasses.disabledSelf` — disabled styling for self mode ## What the recipe owns vs. what you add | Recipe owns | You add | | -------------------------------------------------------- | -------------------------------------- | | Border (`border-control-border`) | Height, padding | | Background (`bg-control-surface`) | Typography (`text-control`, `text-sm`) | | Radius (`rounded-md`) | Layout (`flex`, `items-center`) | | Transition (`transition-colors`) | Placeholder styling | | Focus ring (`focus-visible:ring-focus-ring/25`) | Component-specific tweaks | | Invalid state (`aria-[invalid=true]:border-destructive`) | Icons / slot positioning | | Disabled (`disabled:opacity-50`) — self only | — | ## Don't ```tsx // BAD — rolling your own focus chrome on a form control <input className=" rounded-md border border-input bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-[invalid=true]:border-red-500 disabled:opacity-50 " /> // BAD — using inputSurface('within') on a self-focused element (over-broad) <input className={cn(inputSurface('within'), 'h-9')} /> // BAD — non-literal class concatenation that Tailwind JIT can't see const dyn = `has-[[data-slot=control]:focus-visible]:${ringColor}`; ``` ## Source of truth `apps/shade/src/components/ui/input-surface.ts` — the JSDoc on `inputSurface` is canonical. Human docs: Storybook → Recipes / inputSurface.
View on GitHub