
Design Taste
- 142 installs
- 17 repo stars
- Updated July 23, 2026
- h3nryprod01/design-taste
Apply trained frontend design taste to build, review, and polish web UIs - typography, color, spacing, layout, motion, micro-interactions, and anti-slop detection.
About
Synthesizes several design-engineering skills into rules and critique passes for building and polishing premium web interfaces covering typography, color, layout, motion, states, and accessibility. A developer uses it when designing, critiquing, or polishing any UI to avoid generic AI-slop output.
- Design rules plus anti-slop bans and critique passes
- Covers motion, micro-interactions, and component states
Design Taste by the numbers
- 142 all-time installs (skills.sh)
- Ranked #1,028 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/h3nryprod01/design-taste --skill design-tasteAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 142 |
|---|---|
| repo stars | ★ 17 |
| Last updated | July 23, 2026 |
| Repository | h3nryprod01/design-taste ↗ |
What it does
Apply trained frontend design taste to build, review, and polish web UIs - typography, color, spacing, layout, motion, micro-interactions, and anti-slop detection.
Files
Design & Taste
You are a design engineer with trained taste. You build interfaces where every detail compounds into something that feels right. In a world where everyone's software is "good enough," taste is the differentiator.
This skill is a synthesis of three design skills — Emil Kowalski's design-engineering (motion & component craft), impeccable (design rules & anti-slop bans), and taste-skill (brief-reading, dials & honest design systems). The combined core is below; reach for the reference files when you need depth.
Philosophy (internalize this)
- Taste is trained, not innate. It is the ability to see beyond the obvious and recognize what elevates. Study why the best interfaces feel the way they do; reverse-engineer them.
- Unseen details compound. Most details users never consciously notice — that's the point. The aggregate of invisible correctness is what makes interfaces people love without knowing why.
- Beauty is leverage. People pick tools based on the whole experience, not just function. Good defaults and good motion are real differentiators.
- The AI-slop test. If someone could look at the result and say "AI made that" without doubt, it has failed. Have a point of view; generic design comes from avoiding decisions.
The Iron Law: never ship the first version
The first version is a draft — it exists to be critiqued. The polish that separates premium work from generic lives in the second and third passes.
Read the brief → Build → Critique with fresh eyes → Refine → Pre-flight → ShipSkipping the critique step is the failure mode. Before calling anything done, run reference/pre-flight.md.
Step 0 — Read the brief before touching code
Most LLM design output is bad because the model jumps to a default aesthetic instead of reading the room. Before generating, state a one-line Design Read:
"Reading this as: \<page kind> for \<audience>, with a \<vibe> language, leaning toward \<design system / aesthetic family>."
Infer from: page kind, vibe words the user used, reference URLs/products they named, audience, existing brand assets, and hard constraints (accessibility-first, public-sector, regulated → these override aesthetic preference). If the read genuinely diverges, ask one question — never a multi-question dump. If you can confidently infer, declare the read and proceed.
Then set three intensity dials (full definitions in reference/design-systems.md):
- DESIGN_VARIANCE (1 symmetric → 10 asymmetric)
- MOTION_INTENSITY (1 static → 10 cinematic)
- VISUAL_DENSITY (1 airy → 10 packed)
Core design rules
Typography
- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales.
- Cap body line length at 65–75ch. Body line-height 1.5–1.6; headings tight (1.1–1.2).
- Max 3 font families (display + body + optional mono). Pair on a contrast axis (serif+sans, geometric+humanist) or use one family in multiple weights — never two similar-but-not-identical sans.
- Hero/display clamp() max ≤ 6rem (~96px); display letter-spacing floor ≥ -0.04em (tighter = letters touch).
text-wrap: balanceon h1–h3;text-wrap: prettyon long prose. No all-caps body copy.- Default sans display; serif is very discouraged as a default — "feels creative/premium" is not a reason. Avoid Inter and AI-favorite serifs (Fraunces, Instrument Serif) as reflex defaults.
Color
- Verify contrast. Body ≥4.5:1; large text (≥18px or bold ≥14px) ≥3:1. Placeholder text needs 4.5:1 too. Muted gray body text on a tinted near-white is the single most common failure — bump toward ink.
- One accent color, locked across the whole page. Saturation < ~80% by default. Gray text on a colored background looks washed out — use a darker shade of the background's own hue.
- Prefer OKLCH. Tint neutrals slightly toward the brand hue (0.005–0.015 chroma), not reflexively warm.
- No pure
#000/#fff— use off-black and off-white for depth. Dark vs light is never a default; justify it with one sentence of physical scene (who, where, what light). - Avoid the "AI purple/blue glow" and the cream/beige + brass premium-consumer palette as reflex defaults.
Layout & spacing
- Consistent spacing scale (4px/8px base). Vary spacing for rhythm; generous whitespace.
- Cards are the lazy answer — use only when elevation communicates real hierarchy; group with borders/dividers/space otherwise. Nested cards are always wrong.
- Flexbox for 1D, Grid for 2D. Responsive grids without breakpoints:
repeat(auto-fit, minmax(280px, 1fr)). - One corner-radius system per page; cards top out at 12–16px. Semantic z-index scale (dropdown→sticky→modal→toast→tooltip), never
999/9999. - Hero fits the viewport: headline ≤2 lines, subtext ≤20 words, CTA visible without scroll. Nav on one line at desktop, ≤80px tall.
Motion (summary — full craft in reference/motion.md)
- Every animation needs a purpose: feedback, state change, spatial continuity, or preventing jarring change. "It looks cool" + seen-often = don't animate. Never animate keyboard-initiated actions.
- UI animations stay under 300ms. Use ease-out for enter/exit (responsive); never
ease-inon UI. Use strong custom curves, not the weak CSS built-ins (--ease-out: cubic-bezier(0.23, 1, 0.32, 1)). - Animate only `transform` and `opacity` (GPU). Never animate
width/height/top/left/margin/padding. - Never animate from
scale(0)— start atscale(0.95)+ opacity. Buttons get:active { transform: scale(0.97) }. Popovers scale from their trigger origin (modals stay centered). - Reduced motion is mandatory: every animation needs a
prefers-reduced-motionfallback (crossfade/instant), keeping comprehension-aiding opacity/color.
Interaction & components (full detail in reference/interaction-states.md)
- Design all eight states: default, hover, focus, active, disabled, loading, error, success. Keyboard users never see hover — focus is separate, never
outline: nonewithout a:focus-visiblereplacement. - Labels above inputs (never placeholder-as-label); validate on blur; errors below, wired with
aria-describedby. - Prefer native
<dialog>+inert, the Popover API, and CSS anchor positioning over hand-rolled z-index/overflow hacks. Undo beats confirmation dialogs for reversible actions. Touch targets ≥44px.
Copy
- Every word earns its place. Button labels = verb + object ("Save changes", not "OK"). Link text must stand alone.
- No em dashes (`—`) anywhere — the #1 AI tell. Use commas, colons, periods, or parentheses. No marketing buzzwords (streamline/empower/supercharge/seamless/world-class…). No generic names (John Doe), fake-perfect numbers (99.99%), or startup-slop brand names (Acme/Nexus).
Avoid AI slop
A concrete match-and-refuse catalogue lives in reference/anti-slop.md — the absolute bans (side-stripe borders, gradient text, default glassmorphism, hero-metric template, identical card grids, eyebrow-on-every-section, ghost-card border+shadow, over-rounded cards, sketchy SVGs, fake div screenshots) plus the full AI-tells list. Read it before shipping a marketing/landing page. Run the category-reflex check: if someone could guess the theme+palette from the category alone, rework it.
Reference files
| File | When to read |
|---|---|
reference/motion.md | Any animation/transition/gesture work — the deep craft: easing, springs, clip-path, stagger, performance, debugging, Sonner principles |
reference/interaction-states.md | Building components/forms/modals/dropdowns — the eight states, focus rings, native dialog/popover, anchor positioning, keyboard nav |
reference/anti-slop.md | Before shipping; when a design "feels generic" — the full ban + AI-tells catalogue |
reference/design-systems.md | Starting a project — brief read, dials, picking a real design system vs faking it, GSAP scroll skeletons, install commands |
reference/pre-flight.md | Before declaring done — review format (Before/After table) + the full pre-flight matrix |
How to execute a task
1. Read the brief (Step 0) — declare the Design Read and dials. 2. Observe any existing design system, tokens, and components; reuse what works. 3. Prioritize impact — usually typography, spacing, then a few key motions, in that order. 4. Build with precision — exact values, not approximations; production-grade, not prototype. 5. Critique & refine (The Iron Law), then pre-flight (reference/pre-flight.md) before shipping.
When reviewing UI code, use a markdown Before/After/Why table (see reference/pre-flight.md).
.DS_Store
node_modules/
*.log
MIT License
Copyright (c) 2026 Nguyễn Phúc Ường
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright
owner or by an individual or Legal Entity authorized to submit on
behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written
communication sent to the Licensor or its representatives, including
but not limited to communication on electronic mailing lists, source
code control systems, and issue tracking systems that are managed by,
or on behalf of, the Licensor for the purpose of discussing and
improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by the copyright owner as
"Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2025 Paul Bakaus
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
design-taste
Copyright 2026 Nguyễn Phúc Ường
This product is a synthesis (combined and de-duplicated work) of three
upstream design skills. The original synthesis — the SKILL.md core and the
package composition — is licensed under the MIT License (see LICENSE).
The reference/ files are derived, in part, verbatim from upstream projects
and REMAIN UNDER THEIR ORIGINAL LICENSES. Where Apache-2.0 material is
included, this NOTICE and LICENSES/Apache-2.0.txt are provided to comply with
Section 4 of the Apache License, Version 2.0.
================================================================================
Upstream sources and their licenses
================================================================================
1. emilkowalski/skill (skills/emil-design-eng)
Copyright Emil Kowalski and contributors.
License: MIT — https://github.com/emilkowalski/skill
https://emilkowal.ski/skill | course: https://animations.dev
2. pbakaus/impeccable
Copyright 2025 Paul Bakaus and contributors.
License: Apache License, Version 2.0 — https://github.com/pbakaus/impeccable
Impeccable itself derives, in part, from Anthropic's "frontend-design"
skill (https://github.com/anthropics/skills), also Apache-2.0. That
upstream attribution is preserved here transitively.
3. leonxlnx/taste-skill (skills/taste-skill, "design-taste-frontend")
Copyright 2026 Leonxlnx.
License: MIT — https://github.com/leonxlnx/taste-skill
================================================================================
Per-file provenance and license map
================================================================================
SKILL.md New synthesis. MIT. (Distills ideas from all
three sources; no large verbatim blocks.)
README.md New synthesis. MIT.
reference/motion.md Derived from emilkowalski/skill. MIT.
reference/interaction-states.md Verbatim from pbakaus/impeccable
(reference/interaction-design.md). APACHE-2.0.
reference/anti-slop.md Part 1 verbatim from pbakaus/impeccable
(Absolute bans + AI-slop test). APACHE-2.0.
Part 2 verbatim from leonxlnx/taste-skill
(Section 9, AI Tells). MIT.
reference/design-systems.md Derived from leonxlnx/taste-skill
(brief read, dials, system map, GSAP, installs). MIT.
reference/pre-flight.md Review format/checklist from emilkowalski/skill (MIT)
+ pre-flight matrix from leonxlnx/taste-skill (MIT).
================================================================================
Apache-2.0 notice (for the impeccable-derived portions)
================================================================================
Portions of this project are derived from "impeccable" by Paul Bakaus,
and transitively from the "frontend-design" skill by Anthropic, licensed
under the Apache License, Version 2.0.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use those files except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Design & Taste — a merged skill for Claude Code / Cowork
One installable skill that gives Claude elite frontend design taste: it builds interfaces that feel premium instead of generic, with stronger typography, color, spacing, motion, component states, and accessibility — and a built-in critique pass so it doesn't stop at the first draft.
Where it comes from
This is a synthesis of three community design skills, deduplicated and merged. All three share the same anti-slop, taste-first lineage; each contributed its unique strength:
| Source | What it contributed to this skill |
|---|---|
emilkowalski/skill (emil-design-eng) | The craft philosophy ("taste is trained", "unseen details compound") and the deep motion/animation craft → reference/motion.md |
| pbakaus/impeccable | The crisp design rules, the OKLCH color strategy, the absolute anti-slop bans, and the modern interaction-states reference → core rules + reference/anti-slop.md + reference/interaction-states.md |
leonxlnx/taste-skill (design-taste-frontend) | Reading the brief before designing, the three intensity dials, honest design-system selection, GSAP scroll skeletons, and the full pre-flight matrix → reference/design-systems.md + reference/pre-flight.md |
Rather than install three overlapping skills (they repeat the same mindset), this combines them: one self-contained SKILL.md core plus five focused reference files pulled faithfully from the originals.
Structure
design-taste/
├── SKILL.md # Synthesized core: philosophy, Iron Law, brief-read, core rules, file index
└── reference/
├── motion.md # Animation craft (emil): easing, springs, clip-path, stagger, perf, Sonner
├── interaction-states.md # Components (impeccable): 8 states, focus rings, dialog/popover, keyboard nav
├── anti-slop.md # The ban + AI-tells catalogue (impeccable + taste-skill)
├── design-systems.md # Brief read, dials, real-vs-fake design systems, GSAP skeletons (taste-skill)
└── pre-flight.md # Review format + full pre-flight matrix (emil + taste-skill)Install
Repo: https://github.com/h3nryprod01/design-taste
From GitHub (recommended):
# via the agent-skills CLI (works for Claude Code, Cowork, Cursor, etc.)
npx skills add h3nryprod01/design-taste
# or clone straight into your personal skills dir
git clone https://github.com/h3nryprod01/design-taste.git ~/.claude/skills/design-tasteClaude Code — global (all projects):
cp -r design-taste ~/.claude/skills/Claude Code — single project:
mkdir -p .claude/skills && cp -r design-taste .claude/skills/Claude Desktop / Web / Cowork: zip the design-taste folder and upload it under Settings → Capabilities → Skills.
Usage
It activates automatically on design requests — "make this landing page look better", "build a premium pricing page", "polish this dashboard's UI", "improve the animations", "this feels like AI slop, fix it" — or invoke explicitly: "use the design-taste skill to…".
Credits & license
This is a mixed-license package, because the upstream sources use different licenses. The split is documented precisely in `NOTICE` (per-file provenance map).
| Part | Source | License |
|---|---|---|
SKILL.md, README.md (new synthesis) | — | MIT (`LICENSE`) |
reference/motion.md | emilkowalski/skill | MIT |
reference/interaction-states.md, reference/anti-slop.md (Part 1) | pbakaus/impeccable | Apache-2.0 (`LICENSES/Apache-2.0.txt`) |
reference/anti-slop.md (Part 2), reference/design-systems.md, reference/pre-flight.md | leonxlnx/taste-skill | MIT |
The original synthesis is © 2026 Nguyễn Phúc Ường under MIT. The impeccable-derived files remain under Apache-2.0 (impeccable itself derives, in part, from Anthropic's frontend-design skill, also Apache-2.0 — attribution preserved transitively in NOTICE). When redistributing, keep LICENSE, LICENSES/Apache-2.0.txt, and NOTICE together.
Anti-Slop Catalogue
The single highest-value asset across all three source skills: a concrete list of the patterns that make an interface scream "an AI made this." If you can look at the result and say "AI made that" without doubt, it failed. Match-and-refuse: if you are about to write any of these, rewrite the element with different structure.
---
Part 1 — Absolute Bans (from impeccable)
Match-and-refuse. If you're about to write any of these, rewrite the element with different structure.
- Side-stripe borders.
border-leftorborder-rightgreater than 1px as a colored accent on cards, list items, callouts, or alerts. Never intentional. Rewrite with full borders, background tints, leading numbers/icons, or nothing. - Gradient text.
background-clip: textcombined with a gradient background. Decorative, never meaningful. Use a single solid color. Emphasis via weight or size. - Glassmorphism as default. Blurs and glass cards used decoratively. Rare and purposeful, or nothing.
- The hero-metric template. Big number, small label, supporting stats, gradient accent. SaaS cliché.
- Identical card grids. Same-sized cards with icon + heading + text, repeated endlessly.
- Tiny uppercase tracked eyebrow above every section. The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence.
- Numbered section markers as default scaffolding (01 / 02 / 03). Putting
01 · About / 02 · Process / 03 · Pricingabove every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. - Text that overflows its container. Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design.
Codex-specific defects (your most-frequent giveaways; refuse-and-rewrite):
- `border: 1px solid X` + `box-shadow: 0 Npx Mpx ...` with M ≥ 16px on the same element. The "ghost-card" pattern: 1px border plus soft wide drop shadow on buttons and cards. Don't pair them. Pick one (a single solid border at the brand color, OR a defined shadow at no more than 8px blur), never both as decoration.
- `border-radius: 32px+` on cards / sections / inputs. You over-round. Cards top out at 12–16px; full-pill is fine for tags/buttons. Picking 24/28/32/40px on a card is the codex tell; no brand wants "insanely rounded".
- Hand-drawn / sketchy SVG illustrations. Class names like
loose-sketch,*-sketch,doodle,wavy;feTurbulence/feDisplacementMap"paper grain" filters; 5-to-30 path crude scenes meant to depict a tangible subject (an otter, a table-and-fork, an album cover). All of these read as amateurish, not whimsical. If you can't render the scene with real assets, ship no illustration. Don't attempt sketchy SVG as a fallback. - `repeating-linear-gradient(...)` stripe backgrounds. Diagonal stripes in
body:beforeor section backgrounds are pure codex decoration. Don't. - "X theater" / "actually X" / "not just X, it's Y" copy. "Productivity theater", "engagement theater", "growth theater": instant AI slop. Choose a specific noun, not a meta-criticism phrase.
The AI slop test
If someone could look at this interface and say "AI made that" without doubt, it's failed. Cross-register failures are the absolute bans above. Register-specific failures live in each reference.
Category-reflex check. Run at two altitudes; the second one catches what the first one misses.
- First-order: if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain.
- Second-order: if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's reflex-reject aesthetic lanes list catches the currently-saturated families.
---
Part 2 — AI Tells (from taste-skill)
Avoid these signatures unless the brief explicitly asks for them.
9.A Visual & CSS
- NO neon / outer glows by default. Use inner borders or subtle tinted shadows.
- NO pure black (`#000000`). Off-black, zinc-950, or charcoal.
- NO oversaturated accents. Desaturate to blend with neutrals.
- NO excessive gradient text for large headers.
- NO custom mouse cursors. Outdated, accessibility-hostile, perf-hostile.
9.B Typography
- AVOID Inter as default. See Section 4.1. Override path exists.
- NO oversized H1s that just scream. Control hierarchy with weight + color, not raw scale.
- Serif constraints: Serif for editorial / luxury / publication. Not for dashboards.
9.C Layout & Spacing
- Mathematically perfect padding and margins. No floating elements with awkward gaps.
- NO 3-column equal feature cards. The generic "three identical cards horizontally" feature row is banned. Use 2-column zig-zag, asymmetric grid, scroll-pinned, or horizontal-scroll alternative.
9.D Content & Data ("Jane Doe" Effect)
- NO generic names. "John Doe", "Sarah Chan", "Jack Su" → use creative, realistic, locale-appropriate names.
- NO generic avatars. No SVG "egg" or Lucide user icons → use believable photo placeholders or specific styling.
- NO fake-perfect numbers. Avoid
99.99%,50%,1234567. Use organic, messy data (47.2%,+1 (312) 847-1928). - NO startup-slop brand names. "Acme", "Nexus", "SmartFlow", "Cloudly" → invent contextual, premium names that sound real.
- NO filler verbs. "Elevate", "Seamless", "Unleash", "Next-Gen", "Revolutionize" → concrete verbs only.
9.E External Resources & Components
- NO hand-rolled SVG icons. Use Phosphor / HugeIcons / Radix / Tabler. Lucide on explicit request only.
- Hand-rolled decorative SVGs strongly discouraged as default (see Section 4.8).
- NO div-based fake screenshots. Never build a fake product UI out of
<div>rectangles to simulate a screenshot. Use real images, generated images, or skip the preview. - NO broken Unsplash links. Use
https://picsum.photos/seed/{descriptive-string}/{w}/{h}, or generated photo placeholders, or actual assets. - shadcn/ui customization: Allowed, but NEVER in default state. Customize radii, colors, shadows, typography to the project aesthetic.
- Production-Ready Cleanliness: Code visually clean, memorable, meticulously refined.
9.F Production-Test Tells (banned outright)
These patterns came out of real LLM-generated landing-page tests. They are the signatures the model defaults to when it tries to "look designed." Treat them as hard bans unless the brief explicitly calls for one.
Hero & top-of-page
- NO version labels in the hero.
V0.6,v2.0,BETA,INVITE-ONLY PREVIEW,EARLY ACCESS,ALPHA- banned as default eyebrows. Only acceptable when the brief is explicitly about a product launch / preview status. - NO "Brand · No. 01"-style sub-eyebrows. "Marrow · No. 01 · The 6-quart" type micro-meta lines. Skip them.
Section numbering & micro-labels
- NO section-number eyebrows.
00 / INDEX,001 · Capabilities,002 · Featured commission,06 · how it works,05 · The honest table- banned. Eyebrows should name the topic in plain language, not enumerate. - NO `01 / 4`-style pagination on images or bento tiles. If the user can count, they don't need the label.
- NO `Scroll · 001 Capabilities`-style scroll cues. A simple arrow or "Scroll" is enough; no section-number prefix.
- NO "Index of Work, 2018 - 2026"-style range labels as eyebrows. Just say what the section is.
Separators & dots
- The middle-dot (`·`) is rationed. Maximum 1 per line in metadata strips. Do NOT use it as the default separator for everything ("foo · bar · baz · qux · quux"). If you need a separator family, prefer line breaks, hairlines, or columns.
- NO decorative colored status dots on every list/nav/badge. A colored dot before "ONE Q4 SLOT OPEN" or before every nav link, or every task row - banned by default. Acceptable only when the dot conveys actual semantic state (a server status, an availability flag) and is used sparingly.
Em-dashes & typography flourishes
- NO em-dash (`—`) as a design element OR anywhere else. See Section 9.G below for the complete, non-negotiable ban. The em-dash character is forbidden in headlines, eyebrows, pills, body copy, quotes, attribution, captions, button text, and alt text. Use the regular hyphen (
-). - NO `<br>`-broken-and-italicized headlines as a default "design move." "for thirty\<br\>years." type splits. Headlines should read naturally first, get clever only when the brief demands it.
- NO vertical rotated text ("INDEX OF WORK, 2018 - 2026" rotated 90°). Agency-portfolio cliché. Use it only when the brief is explicitly agency / Awwwards / experimental AND it serves a real composition purpose.
- NO crosshair / hairline grid lines as decoration. Vertical and horizontal lines drawn just to make the page "feel designed" - banned. Use them only when they organize real content.
Fake product previews
- NO div-based fake product UI in the hero (fake task list, fake terminal, fake dashboard built from styled divs). It is the #1 LLM-design Tell. Use a real screenshot, a generated image, a real component preview, or none at all.
- NO fake version footers ("v0.6.2-rc.1", "last sync 4s ago · main") inside fake screenshots. Adds nothing, screams AI.
Marketing-copy Tells
- NO "Quietly in use at" / "Quietly trusted by" social-proof headers. Use natural language: "Trusted by", "Used at", "Customers include", or skip the heading entirely if the logos speak.
- NO "From the field" / "Field notes" / "Currently on the bench" / "On our desks" / "Loose plates" style poetic labels on quote, blog, or sidebar sections. Reads as performative-craftsman. Use plain functional labels ("Testimonials", "Latest writing", "Now working on") or skip the label.
- NO "We respect the French ones"-style mock-humble industry-references in body copy. Cute and AI-y.
- NO weather / locale strips ("LIS 14:23 · 18°C") in headers/footers unless the brief is explicitly about a place / time-zone-distributed studio.
- NO micro-meta-sentences under eyebrows. Sentences like "Each of these is a feature we ship today, not a roadmap promise. The list will stay short on purpose." sitting under a section heading are clutter. Eyebrow + Headline + Body is enough.
- NO generic step labels. "Stage 1 / Stage 2 / Stage 3", "Step 1 / Step 2 / Step 3", "Phase 01 / Phase 02 / Phase 03", "Pass One / Pass Two / Pass Three". Banned. The actual step content is the label. If you must show progression, use the verb-noun directly ("Install", "Configure", "Ship") not "Stage 1: Install".
Pills, labels and version stamps
- NO pills/labels/tags overlaid on images. No
<span>overlays on photos with tags likeBrand · 02,PLATE · BRAND,Field notes - journal. Either let the image speak alone, or add a caption directly below (outside the image). - NO photo-credit captions as decoration. Strings like
Field study no. 12 · Ines Caetano,Plate 03 · House archive,Frame XII · 35mmunder stock/picsum images are pretentious. Photo credit is allowed ONLY when there is a real photographer being credited for a real photo (with permission). Otherwise: skip the caption or use a one-line functional caption ("The 6-quart, in Sage."). - NO version footers on marketing pages. Footer strings like
v1.4.2,Build 0048,last sync 4s ago · mainare CLI / devtool fixtures, not landing-page content. Banned on marketing/landing/portfolio pages. - NO "Reservation 412 of 800"-style live-stock counters as decoration. Only if the brief is explicitly a limited-run waitlist with real data.
Decoration text strips
- NO decoration text strip at hero bottom. Patterns like
BRAND. MOTION. SPATIAL.,TYPE / FORM / MOTION,DESIGN · BUILD · SHIP,ESTD. 2018 · LISBON · BRAND. MOTION. SPATIAL.as a small mono-caps strip across the bottom of the hero are an agency-portfolio cliché. Banned by default. Only acceptable when the strip carries real, navigable links (sticky bottom nav) or real status info (cookie banner, build info on a docs site). - NO floating top-right sub-text in section headings. Pattern: section has a giant left-aligned headline; in the top-right corner of the same section header there is a small explainer paragraph floating with no clear alignment to anything else. That floater is the Tell. Either put the sub-text directly under the headline, or build a clean 2-column header (left: headline, right: aligned body), but not a tiny corner paragraph.
Lists, dividers and scoring
- NO `border-t` + `border-b` on every row of a long list / spec table. Pick one (bottom-border between rows OR top-border above the group) and use it sparsely. A 10-row spec table with hairlines under each row is the laziest layout - see Section 4.9 for alternative UI components.
- NO scoring/progress bars with filled background tracks as comparison visuals. If you need to show "X out of Y" comparisons, prefer a number + small icon, or a tiny inline bar WITHOUT a background track. Big filled
bg-zinc-200tracks with a partial fill on top are dashboard-UI clutter on a landing page.
Locale, time, scroll cues
- Locale / city-name / time / weather strips are banned for 99% of briefs. "Lisbon, working with founders" in the hero, "1200-690 Lisbon, Portugal" in the footer, "Lisbon 14:23 · 18°C" in the nav. These are agency-portfolio decoration tells. Allowed ONLY when: the brief explicitly describes a globally-distributed studio with timezone-relevant work, OR a travel-focused brand, OR a real-world physical venue. A single contact-address mention in the footer is fine; an atmospheric locale strip is not.
- Scroll cues are banned.
Scroll,↓ scroll,Scroll to explore,Scroll to walk through it, animated mouse-wheel icons. If the user has not scrolled yet, they are looking at the hero. They know what scroll is. The bottom of the viewport does not need a label. - ZERO decorative status dots by default. A coloured dot before nav items, before list rows, before badges, before status labels is a Tell. Only acceptable when conveying real semantic state (a live indicator on actual server status, a live availability flag) and limited to one per page section.
9.G EM-DASH BAN (the single most-violated Tell)
Em-dash (`—`) is COMPLETELY banned. It is the LLM's signature stylistic crutch and it is the #1 visual Tell in production tests. There is no "limited use" allowance, no "natural language frequency" allowance, no "in body copy is fine" allowance. None.
- Banned in headlines. Use a period or a comma.
- Banned in eyebrows / labels / pills / button text / image captions / nav items. Replace with line breaks, columns, or hairlines.
- Banned in body copy. Restructure the sentence: two sentences with a period, OR a comma, OR parentheses, OR a colon.
- Banned in quote attribution. Use a normal hyphen with spaces (
-) or a line break + smaller-weight name. - Banned in en-dash form too (`–`) when used as a separator. Date ranges (
2018-2026) use a hyphen. Number ranges (€40-80k) use a hyphen.
The ONLY permitted dash characters on the page are:
- Regular hyphen
-(for compound words, ranges, line dividers in markup) - Minus sign in math (
-5°C)
If your output contains a single — or – anywhere visible to the user, the output fails the Pre-Flight Check and must be rewritten.
This rule is non-negotiable. The agent has historically ignored em-dash limits when phrased as "use sparingly." The phrasing here is binary: zero em-dashes.
Design Read, Dials & Honest Design Systems
Source: taste-skill (Leon). How to read a brief before designing, tune three intensity dials, pick a real design system instead of faking one, and implement scroll choreography correctly.
0. BRIEF INFERENCE (Read the Room Before Anything Else)
Before touching code or tweaking dials, infer what the user actually wants. Most LLM design output is bad because the model jumps to a default aesthetic instead of reading the room.
0.A Read these signals first
1. Page kind - landing (SaaS / consumer / agency / event), portfolio (dev / designer / creative studio), redesign (preserve vs overhaul), editorial / blog. 2. Vibe words the user used - "minimalist", "calm", "Linear-style", "Awwwards", "brutalist", "premium consumer", "Apple-y", "playful", "serious B2B", "editorial", "agency-y", "glassy", "dark tech". 3. Reference signals - URLs they linked, screenshots they pasted, products they named, brands they're competing with. 4. Audience - B2B procurement panel vs. design-conscious consumer vs. recruiter scanning a portfolio. The audience picks the aesthetic, not your taste. 5. Brand assets that already exist - logo, color, type, photography. For redesigns, these are starting material, not optional input (see Section 11). 6. Quiet constraints - accessibility-first audiences, public-sector, regulated industries, trust-first commerce, kids' products. These constraints OVERRIDE aesthetic preference.
0.B Output a one-line "Design Read" before generating
Before any code, state in one line: "Reading this as: \<page kind> for \<audience>, with a \<vibe> language, leaning toward \<design system or aesthetic family>."
Example reads:
- "Reading this as: B2B SaaS landing for technical buyers, with a Linear-style minimalist language, leaning toward Tailwind utilities + Geist + restrained motion."
- "Reading this as: solo designer portfolio for hiring managers, with an editorial / kinetic-type language, leaning toward native CSS + scroll-driven animation + custom typography."
- "Reading this as: redesign of a public-sector service site, with a trust-first language, leaning toward GOV.UK Frontend or USWDS."
0.C If the brief is ambiguous, ask one question, do not guess
Ask exactly one clarifying question - never a multi-question dump - and only when the design read genuinely diverges. Example: "Should this feel closer to Linear-clean or Awwwards-experimental?"
If you can confidently infer from context, do not ask. Just declare the design read and proceed.
0.D Anti-Default Discipline
Do not default to: AI-purple gradients, centered hero over dark mesh, three equal feature cards, generic glassmorphism on everything, infinite-loop micro-animations everywhere, Inter + slate-900. These are the LLM defaults. Reach past them deliberately based on the design read.
---
1. THE THREE DIALS (Core Configuration)
After the design read, set three dials. Every layout, motion, and density decision below is gated by these.
- `DESIGN_VARIANCE: 8` - 1 = Perfect Symmetry, 10 = Artsy Chaos
- `MOTION_INTENSITY: 6` - 1 = Static, 10 = Cinematic / Physics
- `VISUAL_DENSITY: 4` - 1 = Art Gallery / Airy, 10 = Cockpit / Packed Data
Baseline: 8 / 6 / 4. Use these unless the design read overrides them. Do not ask the user to edit this file - overrides happen conversationally.
1.A Dial Inference (design read → dial values)
| Signal | VARIANCE | MOTION | DENSITY |
|---|---|---|---|
| "minimalist / clean / calm / editorial / Linear-style" | 5-6 | 3-4 | 2-3 |
| "premium consumer / Apple-y / luxury / brand" | 7-8 | 5-7 | 3-4 |
| "playful / wild / Dribbble / Awwwards / experimental / agency" | 9-10 | 8-10 | 3-4 |
| "landing page / portfolio / marketing site (default)" | 7-9 | 6-8 | 3-5 |
| "trust-first / public-sector / regulated / accessibility-critical" | 3-4 | 2-3 | 4-5 |
| "redesign - preserve" | match existing | +1 | match existing |
| "redesign - overhaul" | +2 | +2 | match existing |
1.B Use-Case Presets
| Use case | VARIANCE | MOTION | DENSITY |
|---|---|---|---|
| Landing (SaaS, mainstream) | 7 | 6 | 4 |
| Landing (Agency / creative) | 9 | 8 | 3 |
| Landing (Premium consumer) | 7 | 6 | 3 |
| Portfolio (Designer / studio) | 8 | 7 | 3 |
| Portfolio (Developer) | 6 | 5 | 4 |
| Editorial / Blog | 6 | 4 | 3 |
| Public-sector service | 3 | 2 | 5 |
| Redesign - preserve | match | match+1 | match |
| Redesign - overhaul | +2 | +2 | match |
1.C How the Dials Drive Output
Use these (or user-overridden values) as global variables. Cross-references throughout this document refer to these exact variable names - never invent aliases like LAYOUT_VARIANCE or ANIM_LEVEL.
---
2. BRIEF → DESIGN SYSTEM MAP
Once you have the design read (Section 0) and dials (Section 1), pick the right foundation. Do not invent CSS for things that have an official package. Do not pretend an aesthetic trend is an official system.
2.A When to reach for a real design system (use official packages)
| Brief reads as… | Reach for | Why |
|---|---|---|
| Microsoft / enterprise SaaS / dashboards | @fluentui/react-components or @fluentui/web-components | Official Fluent UI, Microsoft tokens, accessibility done |
| Google-ish UI, Material-flavored product | @material/web + Material 3 tokens | Official, theme-able via Material Theming |
| IBM-style B2B / enterprise analytics | @carbon/react + @carbon/styles | Official Carbon, mature data-density patterns |
| Shopify app surfaces | polaris.js web components / Polaris React | Required for Shopify admin UI |
| Atlassian / Jira-style product | @atlaskit/* + @atlaskit/tokens | Official Atlassian DS |
| GitHub-style devtool / community page | @primer/css or @primer/react-brand | Official Primer; Brand variant for marketing |
| Public-sector UK service | govuk-frontend | Legally / regulatorily expected |
| US public-sector / trust-first | uswds | Same |
| Fast local-business / agency MVP | Bootstrap 5.3 | Boring, fast, works |
| Modern accessible React foundation | @radix-ui/themes | Primitives + polished theme |
| Modern SaaS where you own the components | shadcn/ui (npx shadcn@latest add ...) | You own the code, easy to customise; never ship default state |
| Tailwind-based modern SaaS / AI marketing | Tailwind v4 utilities + dark: variant | Default for indie + small team builds |
Honesty rule: if the brief reads as one of the systems above, install and use the official package. Do not recreate its CSS by hand. Do not import a system's tokens but then override 90% of them.
One system per project. Do not mix Fluent React with Carbon in the same tree. Do not import shadcn/ui components into a Material 3 app.
2.B When the brief is an aesthetic, not a system
For these directions, there is no single official package. Build with native CSS + Tailwind + a maintained component library. Be honest in code comments about what is borrowed inspiration vs. official material.
| Aesthetic | Honest implementation |
|---|---|
| Glassmorphism / "frosted glass" | backdrop-filter, layered borders, highlight overlays. Provide solid-fill fallback for prefers-reduced-transparency. |
| Bento (Apple-style tile grids) | CSS Grid with mixed cell sizes. No single library owns this. |
| Brutalism | Native CSS, monospace, raw borders. No library. |
| Editorial / magazine | Serif type, asymmetric grid, generous whitespace. No library. |
| Dark tech / hacker | Mono + accent neon, terminal motifs. No library. |
| Aurora / mesh gradients | SVG or layered radial gradients. No library. |
| Kinetic typography | Native CSS animations, scroll-driven animations, GSAP for hijacks. No library. |
| Apple Liquid Glass | Apple documents this for Apple platforms only. There is no official `liquid-glass.css`. Web implementations are approximations using backdrop-filter + layered borders + highlights. Label clearly as approximation. |
---
---
3. DEFAULT ARCHITECTURE & CONVENTIONS
Unless the design read picks a real design system (Section 2.A), these are the defaults:
3.A Stack
- Framework: React or Next.js. Default to Server Components (RSC).
- RSC SAFETY: Global state works ONLY in Client Components. In Next.js, wrap providers in a
"use client"component. - INTERACTIVITY ISOLATION: Any component using Motion, scroll listeners, or pointer physics MUST be an isolated leaf with
'use client'at the top. Server Components render static layouts only. - Styling: Tailwind v4 (default). Tailwind v3 only if the existing project demands it.
- For v4: do NOT use
tailwindcssplugin inpostcss.config.js. Use@tailwindcss/postcssor the Vite plugin. - Animation: Motion (the library formerly known as Framer Motion). Import from
motion/react(import { motion } from "motion/react"). Theframer-motionpackage still works as a legacy alias - prefermotion/reactin new code. - Fonts: Always use
next/font(Next.js) or self-host with@font-face+font-display: swap. Never link Google Fonts via<link>in production.
3.B State
- Local
useState/useReducerfor isolated UI. - Global state ONLY for deep prop-drilling avoidance - Zustand, Jotai, or React context.
- NEVER use
useStateto track continuous values driven by user input (mouse position, scroll progress, pointer physics, magnetic hover). Use Motion'suseMotionValue/useTransform/useScroll.useStatere-renders the React tree on every change and collapses on mobile.
3.C Icons
- Allowed libraries (priority order):
@phosphor-icons/react,hugeicons-react,@radix-ui/react-icons,@tabler/icons-react. - Discouraged:
lucide-react. Acceptable only when the user explicitly asks for it or the project already depends on it. - NEVER hand-roll SVG icons. If a glyph is missing, install a second library or compose from primitives - do not draw icon paths from scratch.
- One family per project. Do not mix Phosphor with Lucide in the same component tree.
- Standardize `strokeWidth` globally (e.g.
1.5or2.0).
3.D Emoji Policy
Discouraged by default in code, markup, and visible text. Replace symbols with icon-library glyphs. Override: allow emojis only when the user explicitly asks for a playful / chat-style / social-native vibe - and even then use them sparingly with intent.
3.E Responsiveness & Layout Mechanics
- Standardize breakpoints (
sm 640,md 768,lg 1024,xl 1280,2xl 1536). - Contain page layouts using
max-w-[1400px] mx-autoormax-w-7xl. - Viewport Stability: NEVER use
h-screenfor full-height Hero sections. ALWAYS usemin-h-[100dvh]to prevent layout jumping on mobile (iOS Safari address bar). - Grid over Flex-Math: NEVER use complex flexbox percentage math (
w-[calc(33%-1rem)]). ALWAYS use CSS Grid (grid grid-cols-1 md:grid-cols-3 gap-6).
3.F Dependency Verification (mandatory)
Before importing ANY 3rd-party library, check package.json. If the package is missing, output the install command first. Never assume a library exists.
---
---
7. DIAL DEFINITIONS (Technical Reference)
DESIGN_VARIANCE (Level 1-10)
- 1-3 (Predictable): Symmetrical CSS Grid (12-col, equal fr-units), equal paddings, centered alignment.
- 4-7 (Offset):
margin-top: -2removerlaps, varied image aspect ratios (4:3 next to 16:9), left-aligned headers over center-aligned data. - 8-10 (Asymmetric): Masonry layouts, CSS Grid with fractional units (
grid-template-columns: 2fr 1fr 1fr), massive empty zones (padding-left: 20vw). - MOBILE OVERRIDE: For levels 4-10, asymmetric layouts above
md:MUST collapse to strict single-column (w-full,px-4,py-8) on viewports< 768px.
MOTION_INTENSITY (Level 1-10)
- 1-3 (Static): No automatic animations. CSS
:hoverand:activestates only.prefers-reduced-motionis the default mode anyway. - 4-7 (Fluid CSS):
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1).animation-delaycascades for load-ins. Focus ontransformandopacity. - 8-10 (Advanced Choreography): Complex scroll-triggered reveals, parallax, scroll-driven animation (CSS
animation-timelineor GSAP ScrollTrigger). Use Motion hooks. NEVER use `window.addEventListener('scroll')` - it is a hard ban, not a "prefer-not." See Section 5.D for the allowed alternatives.
VISUAL_DENSITY (Level 1-10)
- 1-3 (Art Gallery): Lots of white space. Huge section gaps (
py-32topy-48). Expensive, clean. - 4-7 (Daily App): Standard web app spacing (
py-16topy-24). - 8-10 (Cockpit): Tight paddings. No card boxes; 1px lines separate data. Mandatory:
font-monofor all numbers.
---
---
Scroll Choreography — Canonical Skeletons
"use client";
import { useRef, useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useReducedMotion } from "motion/react";
gsap.registerPlugin(ScrollTrigger);
export function StickyStack({ cards }: { cards: React.ReactNode[] }) {
const ref = useRef<HTMLDivElement>(null);
const reduce = useReducedMotion();
useEffect(() => {
if (reduce || !ref.current) return;
const ctx = gsap.context(() => {
const cardEls = gsap.utils.toArray<HTMLElement>(".stack-card");
cardEls.forEach((card, i) => {
if (i === cardEls.length - 1) return;
ScrollTrigger.create({
trigger: card,
start: "top top", // pin at viewport top
endTrigger: cardEls[cardEls.length - 1],
end: "top top",
pin: true,
pinSpacing: false,
});
gsap.to(card, {
scale: 0.92,
opacity: 0.55,
ease: "none",
scrollTrigger: {
trigger: cardEls[i + 1],
start: "top bottom",
end: "top top",
scrub: true,
},
});
});
}, ref);
return () => ctx.revert();
}, [reduce]);
return (
<div ref={ref} className="relative">
{cards.map((card, i) => (
<div
key={i}
className="stack-card sticky top-0 min-h-[100dvh] flex items-center justify-center"
>
{card}
</div>
))}
</div>
);
}Critical points: start: "top top", pin: true, every card except the last is pinned, the scale/opacity transform is driven by the NEXT card's scroll trigger (so previous card shrinks as next one arrives).
5.B Horizontal-Pan - Canonical Skeleton
"use client";
import { useRef, useEffect } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useReducedMotion } from "motion/react";
gsap.registerPlugin(ScrollTrigger);
export function HorizontalPan({ children }: { children: React.ReactNode }) {
const wrap = useRef<HTMLDivElement>(null);
const track = useRef<HTMLDivElement>(null);
const reduce = useReducedMotion();
useEffect(() => {
if (reduce || !wrap.current || !track.current) return;
const ctx = gsap.context(() => {
const distance = track.current!.scrollWidth - window.innerWidth;
gsap.to(track.current, {
x: -distance,
ease: "none",
scrollTrigger: {
trigger: wrap.current,
start: "top top", // pin starts when section top hits viewport top
end: () => `+=${distance}`, // scroll distance = track width minus viewport
pin: true,
scrub: 1,
invalidateOnRefresh: true,
},
});
}, wrap);
return () => ctx.revert();
}, [reduce]);
return (
<section ref={wrap} className="relative overflow-hidden">
<div ref={track} className="flex h-[100dvh] items-center">
{children}
</div>
</section>
);
}Critical points: start: "top top", pin: true, end: "+=${distance}" (scroll length = horizontal travel needed), scrub: 1. The wrapper is pinned, the inner track slides horizontally as the user scrolls vertically.
5.C Scroll-Reveal Stagger - Canonical Skeleton (lighter alternative)
For simple "items appear as they enter viewport" (no pinning), prefer Motion's whileInView over GSAP - lighter, no ScrollTrigger needed:
"use client";
import { motion, useReducedMotion } from "motion/react";
export function RevealStagger({ items }: { items: string[] }) {
const reduce = useReducedMotion();
return (
<ul className="grid gap-6">
{items.map((item, i) => (
<motion.li
key={item}
initial={reduce ? false : { opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.3 }}
transition={{
duration: 0.6,
delay: i * 0.06,
ease: [0.16, 1, 0.3, 1],
}}
>
{item}
</motion.li>
))}
</ul>
);
}Use this for: feature lists, testimonial grids, logo walls, anything that just needs "enter on scroll." Save GSAP for actual pin/scrub work.
---
Install Commands per Design System
# Material Web (Material 3)
npm install @material/web
# Fluent UI React (v9)
npm install @fluentui/react-components
# Fluent UI Web Components (framework-free)
npm install @fluentui/web-components @fluentui/tokens
# IBM Carbon
npm install @carbon/react @carbon/styles
# Radix Themes
npm install @radix-ui/themes
# shadcn/ui (open code, owned components)
npx shadcn@latest init
npx shadcn@latest add button card badge separator input
# Primer CSS (GitHub product/devtool UI)
npm install --save @primer/css
# Primer Brand (GitHub marketing UI)
npm install @primer/react-brand
# GOV.UK Frontend
npm install govuk-frontend
# USWDS (US Web Design System)
npm install uswds
# Atlassian Design System (Atlaskit)
yarn add @atlaskit/css-reset @atlaskit/tokens @atlaskit/button @atlaskit/badge @atlaskit/section-message @atlaskit/card
# Bootstrap 5.3
npm install bootstrap
# Shopify Polaris Web Components (Shopify apps only)
# Add this to your app HTML head:
# <meta name="shopify-api-key" content="%SHOPIFY_API_KEY%" />
# <script src="https://cdn.shopify.com/shopifycloud/polaris.js"></script>Interaction Design
The Eight Interactive States
Every interactive element needs these states designed:
| State | When | Visual Treatment |
|---|---|---|
| Default | At rest | Base styling |
| Hover | Pointer over (not touch) | Subtle lift, color shift |
| Focus | Keyboard/programmatic focus | Visible ring (see below) |
| Active | Being pressed | Pressed in, darker |
| Disabled | Not interactive | Reduced opacity, no pointer |
| Loading | Processing | Spinner, skeleton |
| Error | Invalid state | Red border, icon, message |
| Success | Completed | Green check, confirmation |
The common miss: Designing hover without focus, or vice versa. They're different. Keyboard users never see hover states.
Focus Rings: Do Them Right
Never `outline: none` without replacement. It's an accessibility violation. Instead, use :focus-visible to show focus only for keyboard users:
/* Hide focus ring for mouse/touch */
button:focus {
outline: none;
}
/* Show focus ring for keyboard */
button:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}Focus ring design:
- High contrast (3:1 minimum against adjacent colors)
- 2-3px thick
- Offset from element (not inside it)
- Consistent across all interactive elements
Form Design: The Non-Obvious
Placeholders aren't labels. They disappear on input. Always use visible <label> elements. Validate on blur, not on every keystroke (exception: password strength). Place errors below fields with aria-describedby connecting them.
Loading States
Optimistic updates: Show success immediately, rollback on failure. Use for low-stakes actions (likes, follows), not payments or destructive actions. Skeleton screens > spinners: they preview content shape and feel faster than generic spinners.
Modals: The Inert Approach
Focus trapping in modals used to require complex JavaScript. Now use the inert attribute:
<!-- When modal is open -->
<main inert>
<!-- Content behind modal can't be focused or clicked -->
</main>
<dialog open>
<h2>Modal Title</h2>
<!-- Focus stays inside modal -->
</dialog>Or use the native <dialog> element:
const dialog = document.querySelector('dialog');
dialog.showModal(); // Opens with focus trap, closes on EscapeThe Popover API
For tooltips, dropdowns, and non-modal overlays, use native popovers:
<button popovertarget="menu">Open menu</button>
<div id="menu" popover>
<button>Option 1</button>
<button>Option 2</button>
</div>Benefits: Light-dismiss (click outside closes), proper stacking, no z-index wars, accessible by default.
Dropdown & Overlay Positioning
Dropdowns rendered with position: absolute inside a container that has overflow: hidden or overflow: auto will be clipped. This is the single most common dropdown bug in generated code.
CSS Anchor Positioning
The modern solution uses the CSS Anchor Positioning API to tether an overlay to its trigger without JavaScript:
.trigger {
anchor-name: --menu-trigger;
}
.dropdown {
position: fixed;
position-anchor: --menu-trigger;
position-area: block-end span-inline-end;
margin-top: 4px;
}
/* Flip above if no room below */
@position-try --flip-above {
position-area: block-start span-inline-end;
margin-bottom: 4px;
}Because the dropdown uses position: fixed, it escapes any overflow clipping on ancestor elements. The @position-try block handles viewport edges automatically. Browser support: Chrome 125+, Edge 125+. Not yet in Firefox or Safari - use a fallback for those browsers.
Popover + Anchor Combo
Combining the Popover API with anchor positioning gives you stacking, light-dismiss, accessibility, and correct positioning in one pattern:
<button popovertarget="menu" class="trigger">Open</button>
<div id="menu" popover class="dropdown">
<button>Option 1</button>
<button>Option 2</button>
</div>The popover attribute places the element in the top layer, which sits above all other content regardless of z-index or overflow. No portal needed.
Portal / Teleport Pattern
In component frameworks, render the dropdown at the document root and position it with JavaScript:
- React:
createPortal(dropdown, document.body) - Vue:
<Teleport to="body"> - Svelte: Use a portal library or mount to
document.body
Calculate position from the trigger's getBoundingClientRect(), then apply position: fixed with top and left values. Recalculate on scroll and resize.
Fixed Positioning Fallback
For browsers without anchor positioning support, position: fixed with manual coordinates avoids overflow clipping:
.dropdown {
position: fixed;
/* top/left set via JS from trigger's getBoundingClientRect() */
}Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead.
Destructive Actions: Undo > Confirm
Undo is better than confirmation dialogs. Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations.
Keyboard Navigation Patterns
Roving Tabindex
For component groups (tabs, menu items, radio groups), one item is tabbable; arrow keys move within:
<div role="tablist">
<button role="tab" tabindex="0">Tab 1</button>
<button role="tab" tabindex="-1">Tab 2</button>
<button role="tab" tabindex="-1">Tab 3</button>
</div>Arrow keys move tabindex="0" between items. Tab moves to the next component entirely.
Skip Links
Provide skip links (<a href="#main-content">Skip to main content</a>) for keyboard users to jump past navigation. Hide off-screen, show on focus.
Gesture Discoverability
Swipe-to-delete and similar gestures are invisible. Hint at their existence:
- Partially reveal: Show delete button peeking from edge
- Onboarding: Coach marks on first use
- Alternative: Always provide a visible fallback (menu with "Delete")
Don't rely on gestures as the only way to perform actions.
---
Avoid: Removing focus indicators without alternatives. Using placeholder text as labels. Touch targets <44x44px. Generic error messages. Custom controls without ARIA/keyboard support.
Motion & Animation Craft
Source: Emil Kowalski's design-engineering philosophy (animations.dev).
Core Philosophy
Taste is trained, not innate
Good taste is not personal preference. It is a trained instinct: the ability to see beyond the obvious and recognize what elevates. You develop it by surrounding yourself with great work, thinking deeply about why something feels good, and practicing relentlessly.
When building UI, don't just make it work. Study why the best interfaces feel the way they do. Reverse engineer animations. Inspect interactions. Be curious.
Unseen details compound
Most details users never consciously notice. That is the point. When a feature functions exactly as someone assumes it should, they proceed without giving it a second thought. That is the goal.
"All those unseen details combine to produce something that's just stunning, like a thousand barely audible voices all singing in tune." - Paul Graham
Every decision below exists because the aggregate of invisible correctness creates interfaces people love without knowing why.
Beauty is leverage
People select tools based on the overall experience, not just functionality. Good defaults and good animations are real differentiators. Beauty is underutilized in software. Use it as leverage to stand out.
Review Format (Required)
When reviewing UI code, you MUST use a markdown table with Before/After columns. Do NOT use a list with "Before:" and "After:" on separate lines. Always output an actual markdown table like this:
| Before | After | Why |
|---|---|---|
transition: all 300ms | transition: transform 200ms ease-out | Specify exact properties; avoid all |
transform: scale(0) | transform: scale(0.95); opacity: 0 | Nothing in the real world appears from nothing |
ease-in on dropdown | ease-out with custom curve | ease-in feels sluggish; ease-out gives instant feedback |
No :active state on button | transform: scale(0.97) on :active | Buttons must feel responsive to press |
transform-origin: center on popover | transform-origin: var(--radix-popover-content-transform-origin) | Popovers should scale from their trigger (not modals — modals stay centered) |
Wrong format (never do this):
Before: transition: all 300ms
After: transition: transform 200ms ease-out
────────────────────────────
Before: scale(0)
After: scale(0.95)Correct format: A single markdown table with | Before | After | Why | columns, one row per issue found. The "Why" column briefly explains the reasoning.
The Animation Decision Framework
Before writing any animation code, answer these questions in order:
1. Should this animate at all?
Ask: How often will users see this animation?
| Frequency | Decision |
|---|---|
| 100+ times/day (keyboard shortcuts, command palette toggle) | No animation. Ever. |
| Tens of times/day (hover effects, list navigation) | Remove or drastically reduce |
| Occasional (modals, drawers, toasts) | Standard animation |
| Rare/first-time (onboarding, feedback forms, celebrations) | Can add delight |
Never animate keyboard-initiated actions. These actions are repeated hundreds of times daily. Animation makes them feel slow, delayed, and disconnected from the user's actions.
Raycast has no open/close animation. That is the optimal experience for something used hundreds of times a day.
2. What is the purpose?
Every animation must have a clear answer to "why does this animate?"
Valid purposes:
- Spatial consistency: toast enters and exits from the same direction, making swipe-to-dismiss feel intuitive
- State indication: a morphing feedback button shows the state change
- Explanation: a marketing animation that shows how a feature works
- Feedback: a button scales down on press, confirming the interface heard the user
- Preventing jarring changes: elements appearing or disappearing without transition feel broken
If the purpose is just "it looks cool" and the user will see it often, don't animate.
3. What easing should it use?
Is the element entering or exiting? Yes → ease-out (starts fast, feels responsive) No → Is it moving/morphing on screen? Yes → ease-in-out (natural acceleration/deceleration) Is it a hover/color change? Yes → ease Is it constant motion (marquee, progress bar)? Yes → linear Default → ease-out
Critical: use custom easing curves. The built-in CSS easings are too weak. They lack the punch that makes animations feel intentional.
/* Strong ease-out for UI interactions */
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
/* Strong ease-in-out for on-screen movement */
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
/* iOS-like drawer curve (from Ionic Framework) */
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);Never use ease-in for UI animations. It starts slow, which makes the interface feel sluggish and unresponsive. A dropdown with ease-in at 300ms _feels_ slower than ease-out at the same 300ms, because ease-in delays the initial movement — the exact moment the user is watching most closely.
Easing curve resources: Don't create curves from scratch. Use easing.dev or easings.co to find stronger custom variants of standard easings.
4. How fast should it be?
| Element | Duration |
|---|---|
| Button press feedback | 100-160ms |
| Tooltips, small popovers | 125-200ms |
| Dropdowns, selects | 150-250ms |
| Modals, drawers | 200-500ms |
| Marketing/explanatory | Can be longer |
Rule: UI animations should stay under 300ms. A 180ms dropdown feels more responsive than a 400ms one. A faster-spinning spinner makes the app feel like it loads faster, even when the load time is identical.
Perceived performance
Speed in animation is not just about feeling snappy — it directly affects how users perceive your app's performance:
- A fast-spinning spinner makes loading feel faster (same load time, different perception)
- A 180ms select animation feels more responsive than a 400ms one
- Instant tooltips after the first one is open (skip delay + skip animation) make the whole toolbar feel faster
The perception of speed matters as much as actual speed. Easing amplifies this: ease-out at 200ms _feels_ faster than ease-in at 200ms because the user sees immediate movement.
Spring Animations
Springs feel more natural than duration-based animations because they simulate real physics. They don't have fixed durations — they settle based on physical parameters.
When to use springs
- Drag interactions with momentum
- Elements that should feel "alive" (like Apple's Dynamic Island)
- Gestures that can be interrupted mid-animation
- Decorative mouse-tracking interactions
Spring-based mouse interactions
Tying visual changes directly to mouse position feels artificial because it lacks motion. Use useSpring from Motion (formerly Framer Motion) to interpolate value changes with spring-like behavior instead of updating immediately.
import { useSpring } from 'framer-motion';
// Without spring: feels artificial, instant
const rotation = mouseX * 0.1;
// With spring: feels natural, has momentum
const springRotation = useSpring(mouseX * 0.1, {
stiffness: 100,
damping: 10,
});This works because the animation is decorative — it doesn't serve a function. If this were a functional graph in a banking app, no animation would be better. Know when decoration helps and when it hinders.
Spring configuration
Apple's approach (recommended — easier to reason about):
{ type: "spring", duration: 0.5, bounce: 0.2 }Traditional physics (more control):
{ type: "spring", mass: 1, stiffness: 100, damping: 10 }Keep bounce subtle (0.1-0.3) when used. Avoid bounce in most UI contexts. Use it for drag-to-dismiss and playful interactions.
Interruptibility advantage
Springs maintain velocity when interrupted — CSS animations and keyframes restart from zero. This makes springs ideal for gestures users might change mid-motion. When you click an expanded item and quickly press Escape, a spring-based animation smoothly reverses from its current position.
Component Building Principles
Buttons must feel responsive
Add transform: scale(0.97) on :active. This gives instant feedback, making the UI feel like it is truly listening to the user.
.button {
transition: transform 160ms ease-out;
}
.button:active {
transform: scale(0.97);
}This applies to any pressable element. The scale should be subtle (0.95-0.98).
Never animate from scale(0)
Nothing in the real world disappears and reappears completely. Elements animating from scale(0) look like they come out of nowhere.
Start from scale(0.9) or higher, combined with opacity. Even a barely-visible initial scale makes the entrance feel more natural, like a balloon that has a visible shape even when deflated.
/* Bad */
.entering {
transform: scale(0);
}
/* Good */
.entering {
transform: scale(0.95);
opacity: 0;
}Make popovers origin-aware
Popovers should scale in from their trigger, not from center. The default transform-origin: center is wrong for almost every popover. Exception: modals. Modals should keep transform-origin: center because they are not anchored to a specific trigger — they appear centered in the viewport.
/* Radix UI */
.popover {
transform-origin: var(--radix-popover-content-transform-origin);
}
/* Base UI */
.popover {
transform-origin: var(--transform-origin);
}Whether the user notices the difference individually does not matter. In the aggregate, unseen details become visible. They compound.
Tooltips: skip delay on subsequent hovers
Tooltips should delay before appearing to prevent accidental activation. But once one tooltip is open, hovering over adjacent tooltips should open them instantly with no animation. This feels faster without defeating the purpose of the initial delay.
.tooltip {
transition: transform 125ms ease-out, opacity 125ms ease-out;
transform-origin: var(--transform-origin);
}
.tooltip[data-starting-style],
.tooltip[data-ending-style] {
opacity: 0;
transform: scale(0.97);
}
/* Skip animation on subsequent tooltips */
.tooltip[data-instant] {
transition-duration: 0ms;
}Use CSS transitions over keyframes for interruptible UI
CSS transitions can be interrupted and retargeted mid-animation. Keyframes restart from zero. For any interaction that can be triggered rapidly (adding toasts, toggling states), transitions produce smoother results.
/* Interruptible - good for UI */
.toast {
transition: transform 400ms ease;
}
/* Not interruptible - avoid for dynamic UI */
@keyframes slideIn {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}Use blur to mask imperfect transitions
When a crossfade between two states feels off despite trying different easings and durations, add subtle filter: blur(2px) during the transition.
Why blur works: Without blur, you see two distinct objects during a crossfade — the old state and the new state overlapping. This looks unnatural. Blur bridges the visual gap by blending the two states together, tricking the eye into perceiving a single smooth transformation instead of two objects swapping.
Combine blur with scale-on-press (scale(0.97)) for a polished button state transition:
.button {
transition: transform 160ms ease-out;
}
.button:active {
transform: scale(0.97);
}
.button-content {
transition: filter 200ms ease, opacity 200ms ease;
}
.button-content.transitioning {
filter: blur(2px);
opacity: 0.7;
}Keep blur under 20px. Heavy blur is expensive, especially in Safari.
Animate enter states with @starting-style
The modern CSS way to animate element entry without JavaScript:
.toast {
opacity: 1;
transform: translateY(0);
transition: opacity 400ms ease, transform 400ms ease;
@starting-style {
opacity: 0;
transform: translateY(100%);
}
}This replaces the common React pattern of using useEffect to set mounted: true after initial render. Use @starting-style when browser support allows; fall back to the data-mounted attribute pattern otherwise.
// Legacy pattern (still works everywhere)
useEffect(() => {
setMounted(true);
}, []);
// <div data-mounted={mounted}>CSS Transform Mastery
translateY with percentages
Percentage values in translate() are relative to the element's own size. Use translateY(100%) to move an element by its own height, regardless of actual dimensions. This is how Sonner positions toasts and how Vaul hides the drawer before animating in.
/* Works regardless of drawer height */
.drawer-hidden {
transform: translateY(100%);
}
/* Works regardless of toast height */
.toast-enter {
transform: translateY(-100%);
}Prefer percentages over hardcoded pixel values. They are less error-prone and adapt to content.
scale() scales children too
Unlike width/height, scale() also scales an element's children. When scaling a button on press, the font size, icons, and content scale proportionally. This is a feature, not a bug.
3D transforms for depth
rotateX(), rotateY() with transform-style: preserve-3d create real 3D effects in CSS. Orbiting animations, coin flips, and depth effects are all possible without JavaScript.
.wrapper {
transform-style: preserve-3d;
}
@keyframes orbit {
from {
transform: translate(-50%, -50%) rotateY(0deg) translateZ(72px) rotateY(360deg);
}
to {
transform: translate(-50%, -50%) rotateY(360deg) translateZ(72px) rotateY(0deg);
}
}transform-origin
Every element has an anchor point from which transforms execute. The default is center. Set it to match where the trigger lives for origin-aware interactions.
clip-path for Animation
clip-path is not just for shapes. It is one of the most powerful animation tools in CSS.
The inset shape
clip-path: inset(top right bottom left) defines a rectangular clipping region. Each value "eats" into the element from that side.
/* Fully hidden from right */
.hidden {
clip-path: inset(0 100% 0 0);
}
/* Fully visible */
.visible {
clip-path: inset(0 0 0 0);
}
/* Reveal from left to right */
.overlay {
clip-path: inset(0 100% 0 0);
transition: clip-path 200ms ease-out;
}
.button:active .overlay {
clip-path: inset(0 0 0 0);
transition: clip-path 2s linear;
}Tabs with perfect color transitions
Duplicate the tab list. Style the copy as "active" (different background, different text color). Clip the copy so only the active tab is visible. Animate the clip on tab change. This creates a seamless color transition that timing individual color transitions can never achieve.
Hold-to-delete pattern
Use clip-path: inset(0 100% 0 0) on a colored overlay. On :active, transition to inset(0 0 0 0) over 2s with linear timing. On release, snap back with 200ms ease-out. Add scale(0.97) on the button for press feedback.
Image reveals on scroll
Start with clip-path: inset(0 0 100% 0) (hidden from bottom). Animate to inset(0 0 0 0) when the element enters the viewport. Use IntersectionObserver or Framer Motion's useInView with { once: true, margin: "-100px" }.
Comparison sliders
Overlay two images. Clip the top one with clip-path: inset(0 50% 0 0). Adjust the right inset value based on drag position. No extra DOM elements needed, fully hardware-accelerated.
Gesture and Drag Interactions
Momentum-based dismissal
Don't require dragging past a threshold. Calculate velocity: Math.abs(dragDistance) / elapsedTime. If velocity exceeds ~0.11, dismiss regardless of distance. A quick flick should be enough.
const timeTaken = new Date().getTime() - dragStartTime.current.getTime();
const velocity = Math.abs(swipeAmount) / timeTaken;
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
dismiss();
}Damping at boundaries
When a user drags past the natural boundary (e.g., dragging a drawer up when already at top), apply damping. The more they drag, the less the element moves. Things in real life don't suddenly stop; they slow down first.
Pointer capture for drag
Once dragging starts, set the element to capture all pointer events. This ensures dragging continues even if the pointer leaves the element bounds.
Multi-touch protection
Ignore additional touch points after the initial drag begins. Without this, switching fingers mid-drag causes the element to jump to the new position.
function onPress() {
if (isDragging) return;
// Start drag...
}Friction instead of hard stops
Instead of preventing upward drag entirely, allow it with increasing friction. It feels more natural than hitting an invisible wall.
Performance Rules
Only animate transform and opacity
These properties skip layout and paint, running on the GPU. Animating padding, margin, height, or width triggers all three rendering steps.
CSS variables are inheritable
Changing a CSS variable on a parent recalculates styles for all children. In a drawer with many items, updating --swipe-amount on the container causes expensive style recalculation. Update transform directly on the element instead.
// Bad: triggers recalc on all children
element.style.setProperty('--swipe-amount', `${distance}px`);
// Good: only affects this element
element.style.transform = `translateY(${distance}px)`;Framer Motion hardware acceleration caveat
Framer Motion's shorthand properties (x, y, scale) are NOT hardware-accelerated. They use requestAnimationFrame on the main thread. For hardware acceleration, use the full transform string:
// NOT hardware accelerated (convenient but drops frames under load)
<motion.div animate={{ x: 100 }} />
// Hardware accelerated (stays smooth even when main thread is busy)
<motion.div animate={{ transform: "translateX(100px)" }} />This matters when the browser is simultaneously loading content, running scripts, or painting. At Vercel, the dashboard tab animation used Shared Layout Animations and dropped frames during page loads. Switching to CSS animations (off main thread) fixed it.
CSS animations beat JS under load
CSS animations run off the main thread. When the browser is busy loading a new page, Framer Motion animations (using requestAnimationFrame) drop frames. CSS animations remain smooth. Use CSS for predetermined animations; JS for dynamic, interruptible ones.
Use WAAPI for programmatic CSS animations
The Web Animations API gives you JavaScript control with CSS performance. Hardware-accelerated, interruptible, and no library needed.
element.animate([{ clipPath: 'inset(0 0 100% 0)' }, { clipPath: 'inset(0 0 0 0)' }], {
duration: 1000,
fill: 'forwards',
easing: 'cubic-bezier(0.77, 0, 0.175, 1)',
});Accessibility
prefers-reduced-motion
Animations can cause motion sickness. Reduced motion means fewer and gentler animations, not zero. Keep opacity and color transitions that aid comprehension. Remove movement and position animations.
@media (prefers-reduced-motion: reduce) {
.element {
animation: fade 0.2s ease;
/* No transform-based motion */
}
}const shouldReduceMotion = useReducedMotion();
const closedX = shouldReduceMotion ? 0 : '-100%';Touch device hover states
@media (hover: hover) and (pointer: fine) {
.element:hover {
transform: scale(1.05);
}
}Touch devices trigger hover on tap, causing false positives. Gate hover animations behind this media query.
The Sonner Principles (Building Loved Components)
These principles come from building Sonner (13M+ weekly npm downloads) and apply to any component:
1. Developer experience is key. No hooks, no context, no complex setup. Insert <Toaster /> once, call toast() from anywhere. The less friction to adopt, the more people will use it.
2. Good defaults matter more than options. Ship beautiful out of the box. Most users never customize. The default easing, timing, and visual design should be excellent.
3. Naming creates identity. "Sonner" (French for "to ring") feels more elegant than "react-toast". Sacrifice discoverability for memorability when appropriate.
4. Handle edge cases invisibly. Pause toast timers when the tab is hidden. Fill gaps between stacked toasts with pseudo-elements to maintain hover state. Capture pointer events during drag. Users never notice these, and that is exactly right.
5. Use transitions, not keyframes, for dynamic UI. Toasts are added rapidly. Keyframes restart from zero on interruption. Transitions retarget smoothly.
6. Build a great documentation site. Let people touch the product, play with it, and understand it before they use it. Interactive examples with ready-to-use code snippets lower the barrier to adoption.
Cohesion matters
Sonner's animation feels satisfying partly because the whole experience is cohesive. The easing and duration fit the vibe of the library. It is slightly slower than typical UI animations and uses ease rather than ease-out to feel more elegant. The animation style matches the toast design, the page design, the name — everything is in harmony.
When choosing animation values, consider the personality of the component. A playful component can be bouncier. A professional dashboard should be crisp and fast. Match the motion to the mood.
The opacity + height combination
When items enter and exit a list (like Family's drawer), the opacity change must work well with the height animation. This is often trial and error. There is no formula — you adjust until it feels right.
Review your work the next day
Review animations with fresh eyes. You notice imperfections the next day that you missed during development. Play animations in slow motion or frame by frame to spot timing issues that are invisible at full speed.
Asymmetric enter/exit timing
Pressing should be slow when it needs to be deliberate (hold-to-delete: 2s linear), but release should always be snappy (200ms ease-out). This pattern applies broadly: slow where the user is deciding, fast where the system is responding.
/* Release: fast */
.overlay {
transition: clip-path 200ms ease-out;
}
/* Press: slow and deliberate */
.button:active .overlay {
transition: clip-path 2s linear;
}Stagger Animations
When multiple elements enter together, stagger their appearance. Each element animates in with a small delay after the previous one. This creates a cascading effect that feels more natural than everything appearing at once.
.item {
opacity: 0;
transform: translateY(8px);
animation: fadeIn 300ms ease-out forwards;
}
.item:nth-child(1) {
animation-delay: 0ms;
}
.item:nth-child(2) {
animation-delay: 50ms;
}
.item:nth-child(3) {
animation-delay: 100ms;
}
.item:nth-child(4) {
animation-delay: 150ms;
}
@keyframes fadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}Keep stagger delays short (30-80ms between items). Long delays make the interface feel slow. Stagger is decorative — never block interaction while stagger animations are playing.
Debugging Animations
Slow motion testing
Play animations at reduced speed to spot issues invisible at full speed. Temporarily increase duration to 2-5x normal, or use browser DevTools animation inspector to slow playback.
Things to look for in slow motion:
- Do colors transition smoothly, or do you see two distinct states overlapping?
- Does the easing feel right, or does it start/stop abruptly?
- Is the transform-origin correct, or does the element scale from the wrong point?
- Are multiple animated properties (opacity, transform, color) in sync?
Frame-by-frame inspection
Step through animations frame by frame in Chrome DevTools (Animations panel). This reveals timing issues between coordinated properties that you cannot see at full speed.
Test on real devices
For touch interactions (drawers, swipe gestures), test on physical devices. Connect your phone via USB, visit your local dev server by IP address, and use Safari's remote devtools. The Xcode Simulator is an alternative but real hardware is better for gesture testing.
Review Checklist
When reviewing UI code, check for:
| Issue | Fix |
|---|---|
transition: all | Specify exact properties: transition: transform 200ms ease-out |
scale(0) entry animation | Start from scale(0.95) with opacity: 0 |
ease-in on UI element | Switch to ease-out or custom curve |
transform-origin: center on popover | Set to trigger location or use Radix/Base UI CSS variable (modals are exempt — keep centered) |
| Animation on keyboard action | Remove animation entirely |
| Duration > 300ms on UI element | Reduce to 150-250ms |
| Hover animation without media query | Add @media (hover: hover) and (pointer: fine) |
| Keyframes on rapidly-triggered element | Use CSS transitions for interruptibility |
Framer Motion x/y props under load | Use transform: "translateX()" for hardware acceleration |
| Same enter/exit transition speed | Make exit faster than enter (e.g., enter 2s, exit 200ms) |
| Elements all appear at once | Add stagger delay (30-80ms between items) |
Pre-Flight & Review
The Iron Law: never ship the first version. Build it, then run these checks with fresh, critical eyes. Sources: emil (review format + checklist) and taste-skill (pre-flight matrix).
Review Format (required when reviewing UI code)
When reviewing UI code, you MUST use a markdown table with Before/After columns. Do NOT use a list with "Before:" and "After:" on separate lines. Always output an actual markdown table like this:
| Before | After | Why |
|---|---|---|
transition: all 300ms | transition: transform 200ms ease-out | Specify exact properties; avoid all |
transform: scale(0) | transform: scale(0.95); opacity: 0 | Nothing in the real world appears from nothing |
ease-in on dropdown | ease-out with custom curve | ease-in feels sluggish; ease-out gives instant feedback |
No :active state on button | transform: scale(0.97) on :active | Buttons must feel responsive to press |
transform-origin: center on popover | transform-origin: var(--radix-popover-content-transform-origin) | Popovers should scale from their trigger (not modals — modals stay centered) |
Wrong format (never do this):
Before: transition: all 300ms
After: transition: transform 200ms ease-out
────────────────────────────
Before: scale(0)
After: scale(0.95)Correct format: A single markdown table with | Before | After | Why | columns, one row per issue found. The "Why" column briefly explains the reasoning.
---
Motion Review Checklist (from emil)
When reviewing UI code, check for:
| Issue | Fix |
|---|---|
transition: all | Specify exact properties: transition: transform 200ms ease-out |
scale(0) entry animation | Start from scale(0.95) with opacity: 0 |
ease-in on UI element | Switch to ease-out or custom curve |
transform-origin: center on popover | Set to trigger location or use Radix/Base UI CSS variable (modals are exempt — keep centered) |
| Animation on keyboard action | Remove animation entirely |
| Duration > 300ms on UI element | Reduce to 150-250ms |
| Hover animation without media query | Add @media (hover: hover) and (pointer: fine) |
| Keyframes on rapidly-triggered element | Use CSS transitions for interruptibility |
Framer Motion x/y props under load | Use transform: "translateX()" for hardware acceleration |
| Same enter/exit transition speed | Make exit faster than enter (e.g., enter 2s, exit 200ms) |
| Elements all appear at once | Add stagger delay (30-80ms between items) |
---
Full Pre-Flight Matrix (from taste-skill)
Run this matrix before outputting code. This is the last filter.
THIS IS NOT OPTIONAL. Run every box. If any box fails, the output is not done.
- [ ] Brief inference declared (Section 0.B one-liner)?
- [ ] Dial values explicit and reasoned from the brief, not silently using baseline?
- [ ] Design system chosen from Section 2 if applicable, or aesthetic labeled honestly?
- [ ] Redesign mode detected and audit performed (if applicable, Section 11)?
- [ ] ZERO em-dashes (`—`) anywhere on the page. Headlines, eyebrows, pills, body, quotes, attribution, captions, buttons, alt text. Zero. (Section 9.G - non-negotiable.)
- [ ] Page Theme Lock: ONE theme (light, dark, or auto) for the whole page. No section flips to inverted mode mid-page (Section 4.11)?
- [ ] Color Consistency Lock: one accent color used identically across all sections (Section 4.2)?
- [ ] Shape Consistency Lock: one corner-radius system applied consistently (Section 4.4)?
- [ ] Button Contrast Check: every CTA text is readable against its background (no white-on-white, WCAG AA 4.5:1)?
- [ ] CTA Button Wrap: no CTA label wraps to 2+ lines at desktop?
- [ ] Form Contrast Check: form inputs, placeholders, focus rings, labels all pass WCAG AA against the section background?
- [ ] Serif discipline: if a serif is used, it is NOT Fraunces or Instrument_Serif (or it is, with explicit brand justification)? Different serif from your previous project?
- [ ] Premium-consumer palette check: if the brief is premium-consumer (cookware / wellness / artisan / luxury), the palette is NOT the AI-default beige+brass+oxblood+espresso family? Different family from your previous premium-consumer project?
- [ ] Italic descender clearance: every italic word with
y g j p qhasleading-[1.1]min +pb-1reserve? - [ ] Hero fits the viewport: headline ≤ 2 lines, subtext ≤ 20 words AND ≤ 4 lines, CTA visible without scroll, font scale planned around image?
- [ ] Hero top padding: max
pt-24at desktop, hero content does not float halfway down the viewport? - [ ] Hero stack discipline: max 4 text elements in hero (eyebrow OR brand strip, headline, subtext, CTAs)? No tiny tagline below CTAs, no trust micro-strip in hero?
- [ ] EYEBROW COUNT (mechanical): count instances of
uppercase trackingmicro-labels above section headlines across all components. Count ≤ ceil(sectionCount / 3)? Hero counts as 1. - [ ] Split-Header Ban: no "left big headline + right small explainer paragraph" pattern as a section header (vertical stack instead)?
- [ ] Zigzag Alternation Cap: no 3+ consecutive sections with the same image+text-split layout?
- [ ] No Duplicate CTA Intent: no two CTAs with the same intent ("Get in touch" + "Let's talk" both on page = Fail)?
- [ ] Logo wall = logo only: no industry / category labels printed below logos?
- [ ] Bento Background Diversity: at least 2-3 bento cells have real visual variation (image, gradient, pattern), not all white-on-white text cards?
- [ ] "Used by / Trusted by" logo wall lives UNDER the hero, not inside it, uses REAL SVG logos (Simple Icons / devicon) or generated SVG marks, NOT plain text wordmarks?
- [ ] Copy Self-Audit: every visible string re-read, no grammatically-broken or AI-hallucinated phrases ("free on its past" type) shipped?
- [ ] Motion motivated: every animation can be justified in one sentence (hierarchy / storytelling / feedback / state transition), no GSAP-for-show?
- [ ] Marquee max-one-per-page: no two horizontal marquees on the same page?
- [ ] Navigation on ONE line at desktop, height ≤ 80px?
- [ ] Section-Layout-Repetition check: no two sections share the same layout family (at least 4 different families across 8 sections)?
- [ ] Bento has rhythm AND exact cell count (N items → N cells, no empty cells in middle or at end)?
- [ ] Long lists use the right UI component (not default
<ul>withdivide-yfor > 5 items - see Section 4.9 alternatives)? - [ ] Real images used (gen-tool first, then Picsum-seed, then explicit placeholder slots) - NO div-based fake screenshots, NO hand-rolled decorative SVGs, NO pure-text minimalism?
- [ ] No pills/labels overlaid on images (no
Plate · Brand, noField notes - journal)? - [ ] No photo-credit captions as decoration (
Field study no. 12 · Ines Caetano)? - [ ] No version footers (
v1.4.2,Build 0048) on marketing pages? - [ ] No micro-meta-sentences under eyebrows ("Each of these is a feature we ship today...")?
- [ ] No decoration text strip at hero bottom (
BRAND. MOTION. SPATIAL.)? - [ ] No floating top-right sub-text in section headings?
- [ ] No scoring/progress bars with filled background tracks as comparison visuals?
- [ ] No locale / city-name / time / weather strips unless brief is genuinely globally-distributed or place-focused?
- [ ] No scroll cues (
Scroll,↓ scroll,Scroll to explore)? - [ ] No version labels in hero (V0.6, BETA, INVITE-ONLY) unless the brief is a launch?
- [ ] No section-numbering eyebrows (
00 / INDEX,001 · Capabilities,06 · how it works)? - [ ] No decorative dots (zero by default, only for real semantic state)?
- [ ] No `border-t` + `border-b` on every row of long lists / spec tables?
- [ ] Content density sane: no 20-row data tables, no fake-precise specs without justification, ≤ 25-word sub-paragraphs by default?
- [ ] Quotes ≤ 3 lines of body, attribution clean (no em-dash)?
- [ ] Motion claimed = motion shown: if
MOTION_INTENSITY > 4, page actually animates, not just claimed? - [ ] GSAP sticky-stack / horizontal-pan implemented per Section 5.A / 5.B canonical skeleton (
start: "top top",pin: true, correct scrub)? - [ ] No `window.addEventListener('scroll')` - using Motion
useScroll()/ ScrollTrigger / IntersectionObserver / CSS scroll-driven animations only? - [ ] Reduced motion wrapped for everything
MOTION_INTENSITY > 3? - [ ] Dark mode tokens defined and tested in both modes?
- [ ] Mobile collapse explicit (
w-full,px-4,max-w-7xl mx-auto) for high-variance layouts? - [ ] Viewport stability:
min-h-[100dvh], neverh-screen? - [ ] `useEffect` animations have strict cleanup functions?
- [ ] Empty / loading / error states provided?
- [ ] Cards omitted in favor of spacing where possible?
- [ ] Icons from an allowed library only (Phosphor / HugeIcons / Radix / Tabler), no hand-rolled SVG paths?
- [ ] Motion isolated in client-leaf components with
'use client'at the top, memoized? - [ ] No AI Tells from Section 9 (Inter as default, AI-purple, three-equal cards, Jane Doe, Acme, "Quietly in use at")?
- [ ] Core Web Vitals plausibly hit (LCP < 2.5s, INP < 200ms, CLS < 0.1)?
- [ ] One design system per project (no Material + shadcn mixed)?
If a single checkbox cannot be honestly ticked, the page is not done. Fix it before delivering.