Design system¶
Native extension consumers¶
Installed native extensions may use the curated catalog in
src/plugins/native/uiCatalog.client.ts. Dynamically installed code must use these
actual shared primitives rather than private application paths or a recreated theme.
The product feel¶
Apple platform interaction model¶
The shared shell follows Apple's layout, sidebar, toolbar, and button guidance. It remains a web application; native Pencil latency, VoiceOver, Safari keyboard behavior, and installed-app lifecycle require device acceptance, not a claim of blanket platform compliance.
- Navigation and controls occupy a quiet material layer above the reading surface. Dark remains the initial appearance; existing theme, font, accent, and reading preferences remain authoritative.
- A persistent sidebar needs room for its 256px width and the 848px reading composition. Below 1100px (68.75rem), navigation uses a dismissible sheet and the workspace projects one focused stack. All saved stacks remain available through the pane switcher when there is more than one. Resizing restores the saved multi-pane layout without changing it. This rule also applies to narrow Mac windows and tablet multitasking.
- Shared controls use 44px minimum boxes when any coarse pointer is available, including tablets
with an attached trackpad. Compact desktop controls retain their density.
Buttoncarries a stabledata-ui="button"marker because composed dialog/popover triggers replacedata-slot. - Narrow toolbars separate navigation from actions; all actions remain reachable. Course navigation precedes an expandable complete course list. Only the actual course overview selects Overview. A single stack does not need an additional pane-switcher row.
- Segmented controls render each label once with roving radio focus, arrow navigation, Home/End, and visible selection. No geometry measurement or duplicated clipped text is needed. A running timer shows its countdown directly, without requiring hover.
- Safe-area insets protect window controls. The visual viewport bounds workspace height when a software keyboard appears; browser pinch zoom never recomposes the page. Input fonts stay at least 16px in touch contexts. Dialogs and search results have bounded, scrollable content.
- Pane changes are immediate and opaque. Motion is reserved for controls and temporary surfaces. Reduced motion and reduced transparency have explicit fallbacks.
src/components/ui/workspace-appearance.css owns the shared window appearance and adaptive
control rules; globals.css owns theme tokens and workspace geometry. Keep course content and
ink coordinates out of the appearance layer. Drawing still uses ContentScale to preserve the
reference composition: reclaim space through navigation before shrinking a reader. The drawing
palette scrolls within its available area so larger controls never make Undo or Clear unreachable.
For UI delivery, inspect desktop, tablet portrait/landscape, narrow windows, phone, keyboard, touch targets, reduced motion, and both supported appearances. Exercise tabs, splits, drawing, notes, search, dialogs, and Settings; preserve state when changing arrangements.
TU/e Study Hub is a persistent study workspace. It borrows the best qualities of Arc and Obsidian:
- Arc: one stable shell, fast switching between contexts, compact controls, and keyboard-first navigation.
- Obsidian: content is the focus, navigation is quiet, information can be dense without feeling crowded, and the interface recedes during long reading sessions.
- Linear/Cursor: restrained hierarchy, predictable rows and panels, short motion, and precise interaction states.
These are behavioral references, not a license to copy another product's chrome. The Study Hub should feel calm, neutral, and academic. It is not a SaaS dashboard, a marketing page, or a set of color-coded course microsites.
Component reference library¶
Use these as searchable references when an existing shared primitive needs a stronger interaction
pattern. Bring back the smallest useful behavior, adapt it to the semantic tokens and compact
workspace geometry, and keep the result owned by src/components/ui; do not copy a whole theme or
marketing section into the product.
- Shadcnblocks — application shells, dense lists, and composed shadcn/ui patterns.
- Magic UI — short React and Motion microinteractions; avoid its looping decorative effects in reading views.
- Aceternity UI — interaction and transition references; omit glow and ornamental backgrounds that compete with course content.
- AI Elements — composable AI conversation, response, and tool-state patterns.
Design principles¶
- One workspace, many courses. Keep the shell and navigation stable while content changes.
- Content before decoration. Use hierarchy, spacing, and typography before color or shadow.
- Dense, not cramped. Prefer compact rows and readable measure over oversized cards.
- Semantic, not course-branded. Course identity comes from code, name, and icon—not a custom palette or bespoke page layout.
- Keyboard and mobile are first-class. Every action must remain reachable without hover.
- The theme is a contract. Components consume semantic tokens and work in light and dark mode.
Typography¶
Students can choose one application font in Settings. Geist is the default; Inter, IBM Plex Sans,
Source Sans 3, Atkinson Hyperlegible Next, and the native system font are also supported. Web
fonts are loaded through next/font in src/app/fonts.ts for production;
local development aliases that module to src/app/fonts.local.ts so
the preview never needs Google Fonts network access. src/app/globals.css
maps the active data-font value to --font-sans in both environments.
Geist Mono always loads alongside the selected family as the meta voice — keyboard hints
(the Kbd primitive), course codes in chrome, and tabular metadata chips via font-meta. It is
the one sanctioned monospace; prose, interface labels, and code blocks never use it.
Use the selected family across the entire workspace, including code. Do not introduce local
component fonts, serif display faces, or other monospace styling for visual flavor. The font
preference is stored locally under tue-study-hub-font, and the design-system test enforces the
approved family set.
Topic reading also exposes local text-size, reading-width, and line-spacing preferences. Text-size
changes apply to paragraph, list, quote, and table copy in learning blocks; reading width changes the
marked reading measure while leaving intentionally wide diagrams and tables wide. Line spacing owns
reading leading independently: compact is 1.45, comfortable is 1.6, and relaxed is 1.8.
| Role | Size | Typical Tailwind class | Use |
|---|---|---|---|
| Micro label | 10px | text-[10px] |
Keyboard hints and exceptional compact metadata |
| Metadata | 12px | text-xs |
Counts, badges, timestamps, captions |
| Interface text | 14px | text-sm |
Navigation, buttons, controls, secondary copy |
| Reading text | 16px | text-base |
Explanations and sustained prose |
| Section heading | 20px | text-xl |
Major content sections |
| Page heading | 24px | text-2xl |
Normal page titles |
| Large page heading | 30px | text-3xl |
Sparse overview pages only |
Rules:
- Use
font-mediumorfont-semiboldfor hierarchy; reservefont-boldfor course codes or rare emphasis. - Headings use
-0.02emtracking and balanced wrapping globally. - Body copy should normally use
leading-6andtext-muted-foregroundonly when it is genuinely secondary. - Numeric progress and counts should use
tabular-nums. - Do not communicate hierarchy by introducing a component-specific font family.
Color and themes¶
The palette is a CIE LCH system with semantic roles — the same space Linear generates its themes
in. Light and dark values live in src/app/globals.css; Tailwind
utilities are generated from the same variables.
Every token is written lch(L% C H). Two hue axes carry the system:
| Axis | Hue | Role |
|---|---|---|
| Neutral | 266 |
Every surface, border, and text token. A near-neutral with a whisper of cool: chroma stays under 1 on the surface ramp so the canvas reads as near-black and near-white. |
| Accent | configurable | Selected in Settings; primary action, focus ring, progress, sidebar identity, and chart-1. |
Surfaces separate by lightness alone (background 2.7 → card 7 → popover 9 → accent 14.5
in dark), which is what makes elevation readable without shadows. Because L is perceptual,
one step reads as the same step anywhere on the ramp — that is the reason for the space, not the
syntax.
When changing a value, move L and leave C and H alone. Foreground/background pairs are held
at WCAG AA; high-chroma values must stay inside the sRGB gamut, since the browser silently clips
anything outside it.
| Token pair | Purpose |
|---|---|
background / foreground |
App canvas and primary text |
card / card-foreground |
Bordered content surfaces |
popover / popover-foreground |
Dialogs, menus, and command surfaces |
primary / primary-foreground |
Primary actions, active emphasis, progress |
secondary / secondary-foreground |
Lower-emphasis actions |
muted / muted-foreground |
Quiet surfaces and secondary text |
accent / accent-foreground |
Hovered and selected rows |
destructive / destructive-foreground |
Destructive actions and errors |
success, warning |
Status feedback |
border, border-strong, input, ring |
Structure (translucent 8% hairlines; border-strong is the 16% hover/active voice), controls, and focus |
sidebar-* |
Workspace navigation equivalents |
code-bg, code-fg |
Code presentation |
code-comment, code-string, code-annotation, code-number, code-keyword, code-type |
Syntax highlighting |
The code surface keeps its own near-black ground in both themes, so the code-* syntax tokens are
defined once and do not vary by theme.
Use semantic utilities such as bg-card, text-muted-foreground, and border-border. Never place
raw hex, RGB, HSL, or OKLCH values inside a component, and never reach for a raw Tailwind palette
class such as bg-emerald-500 or text-amber-500 — the palette has no theme contract, so those
colors do not move with the accent or the light/dark ramp. Avoid course-specific colors. Semantic
status colors should support an icon or label; color alone must not carry meaning.
Hue is reserved for risk and for the accent. destructive marks what cannot be undone and
warning marks what needs review before it runs; both stay outline-and-tint rather than a solid
fill, so a page never has a shouting button in it. Everything else — toggles that are on, status
dots, selected rows — uses primary against muted, which keeps state legible in monochrome and
lets the learner's accent choice carry through.
Two places legitimately hold raw hex, and both are boundaries rather than styling:
- Excalidraw pen colors (
src/course-os/services/drawings/penSlots.ts,DrawingToolbar,DrawingOverlay). These are serialized into saved scenes and into the library's own color picker, which parse hex. Changing the notation would invalidate stored drawings. src/app/manifest.ts. The web app manifest is read by OS launchers and splash screens, which do not reliably parselch(). Keep it in sync with--backgroundby hand.
Gradients and color transitions must declare an interpolation space (in oklch). sRGB
interpolation passes through a desaturated midpoint between stops, which shows as grey seams on
anything thin.
Shape, spacing, and surfaces¶
- The base radius is
10px(--radius: 0.625rem), giving a6 / 8 / 10 / 14scale. Use the sharedrounded-sm,rounded-md,rounded-lg, androunded-xlsteps rather than arbitrary values. - Elevation is four
--elevation-*steps, and Tailwind'sshadow-xs,shadow-sm,shadow-md, andshadow-lgare rebound onto them. The dark theme uses much heavier alphas because a shadow cast on a near-black canvas barely registers otherwise. Reach for a lighter surface before reaching for a shadow; the ramp exists so elevation is legible without one. - Pointer controls are 32–40px high. Touch-capable devices use at least 44px targets, including tab close buttons and menu actions, even when a trackpad is connected.
- Default page content is constrained to
max-w-4xlby the generic course layout. - Use borders and small surface shifts for grouping. Shadows are limited to primitive-level affordance; avoid floating glass panels, glow, claymorphism, and decorative gradients.
- Prefer one outer container with dividers or compact rows over many individually elevated cards.
- Pills (
rounded-full) are for circular controls or true status chips, not every button and card.
Layout and navigation¶
src/components/AppShell.tsx owns the persistent workspace:
workspace identity
├── search / command palette
├── workspace navigation
├── active course's six universal sections
├── collapsible course library
├── active course tools
└── pinned local profile and Settings
The desktop sidebar collapses through its own labelled control to a 3.5rem icon rail that retains the workspace identity, Search, primary navigation, and compact profile/Settings targets. The expanded and mobile sidebars pin the learner's local display name beside Settings below the scrolling navigation. A missing name is an explicit first-run invitation and remains editable; the value is stored in the backed-up local preference record. The collapse preference persists locally; expanding restores the same mounted navigation tree and scroll position. On mobile, the same information architecture becomes a closed drawer; do not create a second competing navigation model. Course pages render inside the shared shell and scrolling lessons use a comfortable 48rem reading measure by default. Settings can switch the marked reading measure between narrow, comfortable, and wide without changing the shell or navigation geometry.
Every scrolling topic ends in the compact reader dock on desktop: previous and next topic links
around one page-level action, in place of the two-column in-flow pagination. Mark page completed
appends a reversible, source-scoped block.viewed event without a block ID. It completes every block
in that topic, so the topic reads 100% in the topic list, the overview resume, and course mastery;
pressing it again removes the marker and restores the derived progress. Collapsing the desktop
sidebar additionally enters reader focus mode, where the marked reading measure centers in the
remaining pane and plugin inspector panels stay mounted but recede from layout and accessibility
navigation. Drawing preserves the reading column so ink stays anchored to its text. Entering
Edit or Draw, opening a paged topic, printing, or using the compact drawer restores the in-flow
pagination instead of the reader dock.
The six course sections are stable:
- Overview
- Topics
- Resources
- Progress
- Reviews
- Exam Preparation
Scrolling topic pages expose every titled learning block through one on-demand Contents popover in the sticky app header. It uses each block's stable ID for hash navigation, highlights the section currently in view, closes after navigation, and preserves its own scroll position while closed. Once the lesson title leaves the viewport, the same active section appears as a quiet second line in the fixed-height header. Topics using the paged presentation keep their existing picker instead of rendering duplicate outline navigation.
The sticky header keeps course context on the left and Draw at the far-right edge. Contents, the compact Notes display control, and the lesson overflow sit immediately before Draw; Export belongs inside the overflow. Their transient layers render above the drawing canvas without moving the header or changing Draw's position. The lesson stays in one reading column at every sidebar state, so drawing and navigation do not cause text reflow.
Do not introduce a course-specific sidebar or route hierarchy.
The dockable workspace¶
Inside the shell, src/dockable-workspace arranges open pages as
stacks of tabs separated by draggable dividers. Chrome stays deliberately compact so the reading
surface remains the dominant layer: tabs are a single 32px row, and every state colour resolves
through a theme token.
- Focused stack carries a one-pixel inset ring in
--workspace-focused. Exactly one stack is focused, and it is the only one that contributes reader actions to the app header. - Direct docking shows one restrained accent overlay matching the exact rectangle the drop would produce — no permanent docking compass. Dragging starts after six pixels, and a drop the layout refuses removes the preview and leaves the tree untouched.
- Pane-local chrome means every open topic keeps its own sticky section line; only the focused pane speaks for the single global header.
- Mobile projects exactly one focused pane. Dividers and drag are absent, the whole tree is preserved, and a compact sheet switcher reaches every hidden stack. Returning to desktop restores every split and ratio.
- Panes never shrink below 240px; a resize or split that would breach that is refused rather than clamped.
Keyboard: tab strips are one tablist with roving focus and Arrow navigation; dividers are
separators that resize by 2% per Arrow and 10% with Shift. Every structural mutation — moved,
joined, split, closed, refused — is announced once through a single polite live region at the
workspace root.
Components¶
Shared primitives live in src/components/ui and follow the shadcn “new-york” conventions declared
in components.json.
- Use
Button,Input,Textarea,NativeSelect,Slider,Card,Badge,Table,Progress, and the sidebar primitives instead of styling native controls ad hoc. - Use
Tooltipfor quiet affordances and key-term definitions (provider is mounted once inProviders; the first hover waits ~350ms, consecutive hovers open instantly). UseToastviauseToast()for session-shaped feedback — not for every mutation. UseDialogfor centered modals (Sheetremains the drawer),SegmentedControlfor exclusive mode switches, andKbdfor keyboard hints set in the meta voice. - Use the
PageHeadercomposition for destination-level introductions. Its eyebrow, title, description, metadata, and action slots keep hierarchy consistent while allowing normal, split-action, and bordered reading variants. - Use the
EmptyStatecomposition when a collection, filter, review queue, or study tool has no content to show. Choose start or centered alignment deliberately; reserve icons and actions for states that benefit from reassurance or a clear recovery path. - Use the
cnhelper fromsrc/lib/utils.tsfor conditional class composition. - Variants belong in the primitive when they represent a reusable semantic state. One-off page layout belongs at the call site.
- Keep course data out of UI components. Generic components receive a
CourseBundle,CourseDescriptor, or learning block. - Use
Iconfromsrc/components/ui/Icon.tsx; names must be validlucide-reactexports. - Use
SettingsSectionfor every settings destination section. It owns the heading, the state summary beside it, the description, an optional action, and the collapsed disclosure row. ReusesettingsRowClassNamefor the label/control row inside it.
The automated design-system contract in
src/components/ui/design-system.test.ts prevents
legacy token vocabulary, native controls outside approved primitives, and competing font families.
Workspace settings¶
Settings is one focused column navigated by a sticky tab bar — there is no settings sidebar. The seven tabs group the sections: General (Account & sync, Appearance, Study Hub AI), Reading & input (Reading, Drawing & canvas), Study tools (Study sessions, Review scheduling), Data & privacy (Local data), Extensions (the overview plus one anchor per installed or managed extension), Platform (Platform usage, Help & docs), and Admin (Admin only). Sections render expanded inside their tab: one column, divided rows, and controls in a consistent right-hand column on wider screens that move below their labels on narrow screens.
Every section header carries its own state, so the page is scannable without reading rows: the heading is followed by a summary of what the section is currently set to. Search settings… in the sticky header opens a settings command dialog listing every section grouped by its tab, including each installed or managed extension; choosing one switches to the owning tab and scrolls to the section. The header covers the top of the pane scrollport, so anchored scrolls clear it through scroll padding rather than scroll margins on individual sections.
Deep links keep the section id as the URL vocabulary: /settings#<section id> and the ?section=
route param land on the owning tab and scroll to the section. Tab strips use roving focus with
Arrow-key movement and manual activation. Collapsing stays a page-level decision — a section
rendered outside the settings page, without SettingsDisclosureProvider, always renders expanded,
and the settings page itself no longer mounts the provider because the tabs replace page-level
collapsing.
Study Hub AI settings lead with the automatic routing chain — the server-configured order in which
models are tried, read from each model's automaticRank — and then list every configured model in one
compact table of model, provider, capability, and picker state. The picker controls only which models
appear as manual choices in the composer. Hidden models remain eligible for automatic server routing,
and an already pinned conversation retains its model identity until the learner chooses another model
or Automatic.
Local-data export uses the complete typed schema-v4 workspace snapshot: progress, course and study preferences, sticky notes, drawing pages, theme, font, accent, sidebar state, and drawing-tool configuration. The active timer, backup metadata, notification permission, offline app files, and unrelated origin keys are deliberately excluded. Older backups remain importable and preserve state their schema did not contain. The UI reports only that a backup download started; an anchor click cannot prove that the browser finished saving the file.
Local data also exposes five category resets—progress, review history, notes, drawings, and appearance and layout—plus reset everything. Each action uses category-specific confirmation and waits for its storage operation before reporting success. Review reset removes scheduling fields while preserving completed work; drawing reset preserves drawing-tool preferences; appearance reset preserves course, timer, sound, and review-session preferences. Backup metadata and unrelated browser data survive every reset.
Timer sound and system notifications are best-effort foreground affordances. Notification permission is requested only from the explicit Enable notifications action and remains owned by the browser. Neither sound nor a system notification is promised after the page or installed app is suspended or terminated.
Contextual annotations¶
Topic learning blocks support local sticky annotations without becoming a separate notebook or free-form canvas. Double-clicking a block creates a block note; selecting text exposes an explicit Add note action for a precise anchor. Open notes use the shared neutral popover surface, a short leader line, and an explicit collapse control; warning color is reserved for the anchor and connector. Students can give each note a title directly in its header. Collapsed notes become counted margin markers.
Each topic provides three visibility modes: Open notes, Markers only, and Hidden. They are presented as a direct three-state control rather than a select menu. Visibility changes presentation only and never deletes notes. The same actions remain keyboard reachable, while narrower layouts keep the editor in normal document flow. Open notes can be resized from their lower-right handle with a pointer or the arrow keys; their title and dimensions persist with the note. Notes use the platform-owned annotation store and are included in Settings backup, import, and reset.
Interactive learning-block headers reserve a right-side action gutter for annotation controls so the note button never covers a question title, disclosure chevron, or other block-owned action.
Motion¶
Motion explains state change; it does not decorate the page. The values follow one framework:
the strong ease-out cubic-bezier(0.23, 1, 0.32, 1) for anything entering — immediate movement
while the eye is watching, then a slow settle, never ease-in, which delays the first frame — and
the symmetric cubic-bezier(0.77, 0, 0.175, 1) for elements that morph on screen (the flashcard
reveal, crossfades).
Named rules:
- The Instant Keyboard Rule. Keyboard-initiated surfaces never animate. The command palette is the exemplar: used hundreds of times a day, it appears and closes in zero ms. Frequency decides: the more often an action repeats, the less it may move.
- The Press Answer Rule. Every pressable lands at
scale(0.96)for ~160ms so the interface answers the hand; hover effects are gated behindhover:hover and pointer:fine. - The Entrance Rule. Content arrives from
scale(0.95)with opacity, an 8px rise, and a short defocus — never from zero. Groups stagger at 40–60ms for one-shot entrances only; routine interactions never cascade. - The Asymmetry Rule. Deliberate fills (hold-to-skip: 900ms linear) release fast (200ms
ease-out). Toasts are the one deliberately calm exception: 400ms on plain
easein, 200ms ease-out out, timers paused under the pointer. - The Origin Rule. Popovers and tooltips scale out of their trigger via the radix-provided transform origin; modals, anchored to nothing, keep their center.
-
The Skip-Delay Rule. Tooltips wait ~350ms once, then open instantly while the pointer moves across neighbours — moving along a toolbar should feel faster than the first hover.
-
Keep transitions around 150–320ms.
- Shared buttons use a restrained
scale(0.96)response on press; reduced-motion users receive the same state change without the scale. MotionConfigapplies the user's reduced-motion preference across every Motion component. Local hooks remain appropriate when a component needs a different non-spatial fallback.- Shared fields acknowledge hover and focus with border and ring continuity; icon-bearing buttons, native selects, command results, and linked badges add one small directional or scale cue.
- Animate disclosure, drawer movement, and flashcard state.
- Pair the 200ms sidebar collapse with the reader focus dock's short delayed fade-and-rise, so the dock arrives after the reading pane begins to settle rather than competing with the shell.
- Prefer opacity and transform; avoid large layout shifts.
- Never loop decorative motion in a reading view.
- Preserve a clear final state when reduced motion is requested.
- Animate row affordances as one system: a short surface fade, a two-pixel directional icon shift,
and an immediate
scale(0.96)press response where the whole row is the control. - Sheets arrive in 200–300ms and exit faster; their content is visible in the default state, and reduced motion removes the transition without changing the final state. The command palette — keyboard-initiated — animates never.
- Conditional quiz feedback, calculator results, tab panels, and command/search empty states use the
shared
data-motion-risecue. Values that change in place may usedata-motion-pop; neither cue loops, and both resolve immediately under reduced motion.
Motion tokens¶
One curve family and four durations, declared in src/app/globals.css
and mirrored for Motion in src/components/ui/motion.ts. Pick from
this list rather than writing a curve inline, so unrelated surfaces settle at the same rate.
| Token | Tailwind | Value | Use |
|---|---|---|---|
--motion-ease-standard |
ease-standard |
cubic-bezier(0.2, 0, 0, 1) |
Interface state changes |
--motion-ease-emphasized |
ease-emphasized |
cubic-bezier(0.23, 1, 0.32, 1) |
Entrances and disclosure — the strong ease-out |
--motion-ease-morph |
ease-morph |
cubic-bezier(0.77, 0, 0.175, 1) |
On-screen morphs: flips, crossfades |
--motion-ease-exit |
ease-exit |
cubic-bezier(0.4, 0, 1, 1) |
Exits |
--motion-duration-instant |
duration-100 |
100ms | Press feedback |
--motion-duration-fast |
duration-150 |
150ms | Hover, focus, exits |
--motion-duration-base |
duration-200 |
200ms | Disclosure, drawers, tabs |
--motion-duration-slow |
duration-[320ms] |
320ms | Entrances, progress travel |
motion.ts additionally exports SPRING (duration: 0.3, bounce: 0) for anything that swaps in
place, SPRING_LAYOUT for elements that carry layout with them, and the riseVariants /
staggerContainer() pair. Every spring in the product is critically damped: overshoot reads as a
toy, and this is a study workspace.
Motion primitives¶
| Primitive | Use |
|---|---|
Stagger / StaggerItem |
One-shot entrances outside the persistent workspace, or content the user explicitly reveals. Do not apply it to route destinations: workspace navigation keeps content opaque and stable while the next page mounts. Never wrap a list that re-renders on filter or progress changes. |
.stagger-rise |
The same entrance for server components, staggered by CSS animation-delay. |
Skeleton / SkeletonText |
Loading placeholders in the shape of the content they replace. Never inside a reading view. |
AnimatedNumber |
Any number that changes in place. Carries tabular-nums, so the box does not twitch per digit. |
CompletionCheck |
The mark for something finished for good. animate={false} for a state that was already true on load — a check that redraws on every visit stops meaning "you just did this". |
AnimatedIcon |
Icons whose meaning changes. Scale 0.25 → 1, opacity 0 → 1, blur 4px → 0. |
Card interactive |
Cards that are themselves controls: one pixel of lift and one step on the elevation ramp. A press scale is wrong at card size. |
Entrances animate opacity, an 8px rise, and a 4px defocus together; exits move half that distance the other way in half the time, because the reader's attention is already on the next thing.
Prefer CSS transitions for interactive state — they retarget mid-flight, so a double-toggle reverses
instead of snapping. Reserve keyframes for one-shot sequences that run once and stop. Progress fills
scale on a transform rather than animating width, which would relay out the row around them.
Reference shelf¶
Use external component libraries as pattern references, not as drop-in visual systems. Adapt useful interaction ideas to Study Hub's semantic tokens, compact geometry, accessibility contract, and reduced-motion behavior. Do not copy looping effects, marketing-page spectacle, or a library's theme.
- Foundations and application patterns: shadcn/ui, shadcnblocks, ReUI, Beautiful UI, BeUI, and Rare UI.
- Motion and interaction references: Transitions, Magic UI, Aceternity UI, and You do not need animations.
- AI workspace patterns: AI Elements.
- Review tools: UI Skills, Coss UI, and the Design System Checklist.
Accessibility and content quality¶
- Every control needs an accessible name and visible focus state.
- Maintain logical heading order; do not pick a heading level for its size.
- Images require meaningful
alttext. Decorative images use empty alt text. - Tables use the shared semantic table components.
- Do not hide essential actions behind hover.
- Error, warning, success, and progress states require text or an icon in addition to color.
- Verify both themes, keyboard navigation, mobile layout, and print behavior for reading content.
UI change checklist¶
- Works with every supported font and uses the documented type scale.
- Uses semantic tokens in both light and dark mode.
- Reuses or extends a shared primitive.
- Preserves the single shell and universal course navigation.
- Works at narrow mobile and desktop widths.
- Has visible focus, accessible names, and non-color status cues.
- Uses motion only to explain state.
- Passes
npm run testandnpm run typecheck.
For historical reasoning, see the hub redesign specification. The code and this guide describe the current contract.