Mischief

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.

Anything that moves decides what it does when motion is unwelcome.

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.

Motion changes how something arrives, never whether it arrives.

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.

Stopping is not always the reduced-motion answer.

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.

A drawing surface that cannot be seen is not drawing.

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.

Say what is being waited for, not that something is happening.

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.

Nothing to show is a state somebody designed, not a blank area.

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 placeholder should be the shape of what is coming.

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.

Anything long enough to regret is long enough to stop.

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.

A composite widget is one tab stop, not one per item.

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.

Arrow keys mean what the shape implies.

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.

Escape closes the thing that opened last.

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.

Nothing is reachable only by hovering.

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.

Style focus-visible, never focus.

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.

An outline is replaced, not removed.

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.

Focus comes back from wherever it went.

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.

Say it once.

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.

A change with no new text still needs saying.

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.

State belongs in the accessibility tree, not only in the styling.

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.

Text arriving a token at a time is not announced a token at a time.

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.

An icon beside a word is decoration.

Icons are hidden from assistive technology, either directly or through the element wrapping them, so a button is announced as what it does rather than as a glyph and a word.

A drawing never carries meaning the words do not.

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 backdrop takes its colours from the theme it was installed into.

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.

A component that animates a keyframe by name ships that keyframe.

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.

This site defines them too.

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.

Every file an item ships declares what it imports.

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 dependency on a sibling names where it lives.

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.

The registry is tested by installing it.

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.

Browse all 117 components