Guidelines
Interface guidelines
A list of details that make an interface good, kept as we learn them. A few of them demonstrate themselves.
Motion4
Movement is a way of explaining a change, not a toll paid before seeing it.
Not one component animates without saying what happens under prefers-reduced-motion. It is a decision every time rather than a default, because the right answer differs: some things stop, some things cut to the end state, and some stop being that kind of component altogether.
An entrance that fades content in from nothing has made the content conditional on a script running, an observer firing, and a preference being off. The content is present and readable from the first paint, and the movement is the part that is optional.
A frozen marquee leaves whatever had not scrolled past permanently unreachable, and a frozen audio meter says the microphone is dead while it is open. Where stopping would lie or hide, the component becomes something else: a row that scrolls, or a line of text.
Every canvas here sleeps when it scrolls out of view, when its tab is hidden, and when it is told to pause, and picks up where it left off. Time spent asleep does not count, so an animation does not jump forward to catch up on arrival.
Waiting4
What the page says between asking and answering.
A spinner with no words is a claim that the page is not broken and nothing more. Where the wait has a subject or a length, the component says so: which tool is running, how long it has been, how much of the context is gone.
Empty, failed, and not-yet are three different things and want three different sentences. A table with no rows because a filter excluded them is not the same as a table with no rows at all.
A skeleton exists so the layout does not jump when the content lands. One that is not the size of the real thing has spent the reader's attention on a jump it was meant to prevent.
A generation that cannot be interrupted makes the reader wait for an answer they already know is wrong. The control that stops it says how long it has been running, and Escape works.
Keyboard4
Everything that can be done with a pointer can be done without one.
Tabbing through a hundred tree rows to reach what is after the tree is not navigation. A tree, a listbox or a toolbar takes one Tab, and the arrow keys move within it, with the widget remembering which item it was on.
In a tree, Right opens and then descends, Left closes and then climbs to the parent. In a list, Home and End go to the ends. These are not preferences; they are what somebody arriving with the habit already expects.
Anything laid over the page can be dismissed from the keyboard, and focus goes back where it came from rather than to the top of the document.
A control that appears on hover appears on focus too. Otherwise it exists for pointers alone, which is most of the reason row actions go missing for keyboard users.
Focus3
Where you are, and where you go back to.
Styling :focus puts a ring on a button somebody clicked, which reads as a bug and teaches people to remove it. Across every component here there is not one bare focus: utility.
outline: none on its own leaves keyboard users with no way to tell where they are. Where the native ring is dropped, a ring is drawn in its place in the same act.
When something laid over the page closes, focus returns to what opened it. Otherwise the next Tab starts from the beginning of the document and the reader has to find their place again.
Announcements4
What a screen reader is told, and how often.
A message shown on screen and repeated in a live region is heard twice. Where both exist, one of them is the announcement and the other is marked as decoration, so the sentence is said a single time.
An icon swapping from a clipboard to a tick is invisible to a screen reader. Anything that only changes appearance is announced politely in words.
Pressed, expanded, selected, sorted and current are attributes before they are colours. A control whose only record of being on is a background is off as far as assistive technology is concerned.
A live region on a streaming answer reads every fragment as it lands. The stream is not announced as it grows; what is announced is that it started and that it finished.
Decoration3
The parts that carry no meaning, and must not pretend to.
Every backdrop, trace and field here is aria-hidden. That is only honest if nothing is said by the drawing alone, which is why the components that draw a state also write it: the trace is confirmation, and the words are the claim.
A scene that brings its own palette is a scene that looks wrong in half of the projects that install it. These read the theme's own custom properties, including when it changes under them.
Installing5
Rules about being a registry rather than a repository, which are the ones nobody tests until an install is broken.
The failure is quiet: the component installs cleanly, renders correctly, and never animates, because the rule referred to a keyframe that only existed in the library it came from.
An install carries keyframes through the registry, and the npm package ships them in its stylesheet, but these pages render the same components from source. For a while one of them animated a keyframe this site never defined, which is exactly the bug the rule above prevents for everybody else.
A dependency imported by a fixture rather than the component is still a dependency of the install, and a package that is imported but not declared is an error in somebody else's project rather than this one.
A registry dependency written as a bare name sends the CLI to shadcn's registry looking for a component only this one has. Seventeen components were uninstallable this way, and nothing here caught it, because a dependency that resolves to the wrong place still parses.
Both of the failures above were invisible to anything that only read the JSON. Every build installs a handful of items into a throwaway project and checks what lands: the files, the stylesheet, and whether the imports resolve.