├── public ├── og.png ├── X-Medium.woff2 ├── X-Regular.woff2 └── favicon.svg ├── vercel.json ├── .gitignore ├── package.json ├── app ├── layout.js ├── page.js ├── icons.js └── globals.scss └── README.md /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raunofreiberg/interfaces/HEAD/public/og.png -------------------------------------------------------------------------------- /public/X-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raunofreiberg/interfaces/HEAD/public/X-Medium.woff2 -------------------------------------------------------------------------------- /public/X-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raunofreiberg/interfaces/HEAD/public/X-Regular.woff2 -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "/X-Regular.woff2", 5 | "headers": [ 6 | { 7 | "key": "Cache-Control", 8 | "value": "public, max-age=31536000, immutable" 9 | } 10 | ] 11 | }, 12 | { 13 | "source": "/X-Medium.woff2", 14 | "headers": [ 15 | { 16 | "key": "Cache-Control", 17 | "value": "public, max-age=31536000, immutable" 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interfaces", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "gray-matter": "^4.0.3", 13 | "next": "13.4.3", 14 | "next-mdx-remote": "^4.4.1", 15 | "react": "18.2.0", 16 | "react-dom": "18.2.0", 17 | "rehype-autolink-headings": "^6.1.1", 18 | "rehype-slug": "^5.1.0", 19 | "remark-gfm": "^3.0.1", 20 | "sass": "^1.62.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/layout.js: -------------------------------------------------------------------------------- 1 | import "./globals.scss"; 2 | 3 | const url = "https://interfaces.rauno.me"; 4 | const title = "Web Interface Guidelines"; 5 | const description = 6 | "A non-exhaustive list of details that make a good web interface."; 7 | const ogUrl = `${url}/og.png`; 8 | 9 | export const metadata = { 10 | title, 11 | description, 12 | openGraph: { 13 | title, 14 | description, 15 | url, 16 | images: [{ url: ogUrl }], 17 | }, 18 | twitter: { 19 | card: "summary_large_image", 20 | title, 21 | description, 22 | images: [ogUrl], 23 | }, 24 | }; 25 | 26 | export default function RootLayout({ children }) { 27 | return ( 28 | 29 | 30 | 31 | 38 | 45 | 46 | {children} 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/page.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs/promises"; 3 | import { MDXRemote } from "next-mdx-remote/rsc"; 4 | import remarkGfm from "remark-gfm"; 5 | import rehypeSlug from "rehype-slug"; 6 | import { cache } from "react"; 7 | import matter from "gray-matter"; 8 | import rehypeAutolinkHeadings from "rehype-autolink-headings"; 9 | import { Icons, GitHub } from "./icons"; 10 | 11 | export default async function Index() { 12 | const markdown = await getMarkdown(); 13 | return ( 14 |
15 | 21 | 28 | 29 | 30 | 31 | 32 | 41 | 50 | 51 |
52 |

53 | 61 | 68 | 75 | 82 | 89 | 96 | Web
97 | Interface
98 | Guidelines 99 |

100 |
101 | 102 |
103 |

Introduction

104 | 114 |
115 |
116 | ); 117 | } 118 | 119 | /////////////////////////////////////////////////////////////////// 120 | 121 | function VerticalFade({ side, ...props }) { 122 | return ( 123 |
124 | ); 125 | } 126 | 127 | function HorizontalFade({ side, ...props }) { 128 | return ( 129 |
130 | ); 131 | } 132 | 133 | function Line({ variant, direction, ...props }) { 134 | return ( 135 |
142 | ); 143 | } 144 | 145 | /////////////////////////////////////////////////////////////////// 146 | 147 | const getMarkdown = cache(async () => { 148 | const filePath = path.join(process.cwd(), "README.md"); 149 | const file = await fs.readFile(filePath, "utf8"); 150 | return matter(file); 151 | }); 152 | 153 | const mdxComponents = { 154 | h2: (props) => { 155 | return ( 156 |

157 | {Icons[props.id]} 158 | {props.children} 159 |

160 | ); 161 | }, 162 | a: (props) => { 163 | const p = { 164 | ...(props.href.startsWith("http") && { 165 | target: "_blank", 166 | rel: "noopener noreferrer", 167 | }), 168 | }; 169 | return ; 170 | }, 171 | }; 172 | -------------------------------------------------------------------------------- /app/icons.js: -------------------------------------------------------------------------------- 1 | export function Accessibility() { 2 | return ( 3 | 12 | 19 | 27 | 28 | ); 29 | } 30 | 31 | export function Interactivity() { 32 | return ( 33 | 42 | 48 | 49 | ); 50 | } 51 | 52 | export function Typography() { 53 | return ( 54 | 63 | 70 | 71 | ); 72 | } 73 | 74 | export function Motion() { 75 | return ( 76 | 85 | 92 | 99 | 106 | 107 | ); 108 | } 109 | 110 | export function Touch() { 111 | return ( 112 | 121 | 128 | 135 | 136 | ); 137 | } 138 | 139 | export function Optimizations() { 140 | return ( 141 | 150 | 157 | 158 | ); 159 | } 160 | 161 | export function Design() { 162 | return ( 163 | 172 | 179 | 186 | 187 | ); 188 | } 189 | 190 | export function GitHub() { 191 | return ( 192 | 199 | 203 | 204 | ); 205 | } 206 | 207 | export const Icons = { 208 | interactivity: , 209 | typography: , 210 | motion: , 211 | touch: , 212 | optimizations: , 213 | design: , 214 | accessibility: , 215 | }; 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Interface Guidelines 2 | 3 | This document outlines a non-exhaustive list of details that make a good (web) interface. It is a living document, periodically updated based on learnings. Some of these may be subjective, but most apply to all websites. 4 | 5 | The [WAI-ARIA](https://www.w3.org/TR/wai-aria-1.1/) spec is deliberately not duplicated in this document. However, some accessibility guidelines may be pointed out. Contributions are welcome. Edit [this file](https://github.com/raunofreiberg/interfaces/blob/main/README.md) and submit a pull request. 6 | 7 | ## Interactivity 8 | 9 | - Clicking the input label should focus the input field 10 | - Inputs should be wrapped with a `
` to submit by pressing Enter 11 | - Inputs should have an appropriate `type` like `password`, `email`, etc 12 | - Inputs should disable `spellcheck` and `autocomplete` attributes most of the time 13 | - Inputs should leverage HTML form validation by using the `required` attribute when appropriate 14 | - Input prefix and suffix decorations, such as icons, should be absolutely positioned on top of the text input with padding, not next to it, and trigger focus on the input 15 | - Toggles should immediately take effect, not require confirmation 16 | - Buttons should be disabled after submission to avoid duplicate network requests 17 | - Interactive elements should disable `user-select` for inner content 18 | - Decorative elements (glows, gradients) should disable `pointer-events` to not hijack events 19 | - Interactive elements in a vertical or horizontal list should have no dead areas between each element, instead, increase their `padding` 20 | 21 | ## Typography 22 | 23 | - Fonts should have `-webkit-font-smoothing: antialiased` applied for better legibility 24 | - Fonts should have `text-rendering: optimizeLegibility` applied for better legibility 25 | - Fonts should be subset based on the content, alphabet or relevant language(s) 26 | - Font weight should not change on hover or selected state to prevent layout shift 27 | - Font weights below 400 should not be used 28 | - Medium sized headings generally look best with a font weight between 500-600 29 | - Adjust values fluidly by using CSS [`clamp()`](https://developer.mozilla.org/en-US/docs/Web/CSS/clamp), e.g. `clamp(48px, 5vw, 72px)` for the `font-size` of a heading 30 | - Where available, tabular figures should be applied with `font-variant-numeric: tabular-nums`, particularly in tables or when layout shifts are undesirable, like in timers 31 | - Prevent text resizing unexpectedly in landscape mode on iOS with `-webkit-text-size-adjust: 100%` 32 | 33 | 34 | ## Motion 35 | 36 | - Switching themes should not trigger transitions and animations on elements [^1] 37 | - Animation duration should not be more than 200ms for interactions to feel immediate 38 | - Animation values should be proportional to the trigger size: 39 | - Don't animate dialog scale in from 0 → 1, fade opacity and scale from ~0.8 40 | - Don't scale buttons on press from 1 → 0.8, but ~0.96, ~0.9, or so 41 | - Actions that are frequent and low in novelty should avoid extraneous animations: [^2] 42 | - Opening a right click menu 43 | - Deleting or adding items from a list 44 | - Hovering trivial buttons 45 | - Looping animations should pause when not visible on the screen to offload CPU and GPU usage 46 | - Use `scroll-behavior: smooth` for navigating to in-page anchors, with an appropriate offset 47 | 48 | ## Touch 49 | 50 | - Hover states should not be visible on touch press, use `@media (hover: hover)` [^3] 51 | - Font size for inputs should not be smaller than 16px to prevent iOS zooming on focus 52 | - Inputs should not auto focus on touch devices as it will open the keyboard and cover the screen 53 | - Apply `muted` and `playsinline` to `