@whereq/react
v0.7.0
Published
WhereQ's open-source React component library. Lightweight, accessible, themeable UI primitives — starting with a beautiful thin Scrollbar.
Maintainers
Readme
@whereq/react
WhereQ's open-source React component library. Lightweight, accessible, themeable UI primitives — a thin Scrollbar, a generic Avatar, an inline-SVG CountryFlag, a multi-variant Tag, and a portal-style Toast.
The Scrollbar was extracted from whereq.cc, our privacy-first
photo gallery. It styles the native scrollbar — no scroll hijacking — so wheel,
keyboard, touch, and accessibility all keep working, with zero runtime scroll math.
- 🪶 Tiny & dependency-free — one
<div>, native scrollbars, a few bytes of CSS. - 🎨 Themeable — light/dark/auto presets, or full control via colour props / CSS variables.
- 🌗 Auto light-dark — the default adapts to the page's
color-scheme. - 🙈 Auto-hide — optionally reveal the thumb only on hover / while scrolling.
- ♿ Accessible & native — real scrollbars, real keyboard & screen-reader behaviour.
- 🧩 SSR-safe & RSC-ready — ships a
"use client"boundary; styles inject before paint. - 🔔 Toasts included — built-in ToastProvider + imperative
toast.error("…")API with 4 variants and 6 positions. - 🔠 First-class TypeScript — full types, ESM + CJS.
Install
npm install @whereq/react
# or
pnpm add @whereq/react
# or
yarn add @whereq/react
react >= 18is a peer dependency.
Usage
<Scrollbar> — a scoped scroll container
import { Scrollbar } from '@whereq/react'
export function Sidebar() {
return (
<Scrollbar style={{ maxHeight: 320 }}>
{/* any tall content */}
<ul>{/* … */}</ul>
</Scrollbar>
)
}Auto-hide + a thicker, rounded thumb, adapting to light/dark:
<Scrollbar autoHide size={8} theme="auto" style={{ maxHeight: '70vh' }}>
<Content />
</Scrollbar>Horizontal, with custom colours:
<Scrollbar axis="horizontal" thumbColor="#17a2ae" thumbHoverColor="#0f7f89">
<Row />
</Scrollbar><GlobalScrollbar> — style every scrollbar on the page
Reproduces the whereq.cc app-wide look. Mount it once near your root; it renders nothing.
import { GlobalScrollbar } from '@whereq/react'
export function App() {
return (
<>
<GlobalScrollbar theme="auto" size={6} />
<Routes />
</>
)
}Prefer an imperative call (e.g. outside React)? Use injectGlobalScrollbarStyles:
import { injectGlobalScrollbarStyles } from '@whereq/react'
const cleanup = injectGlobalScrollbarStyles({ theme: 'dark', size: 6 })
// cleanup() removes the styles<Avatar> — image, with a graceful initials fallback
import { Avatar, AvatarGroup } from '@whereq/react'
// Image; falls back to initials on a name-derived colour if it 404s.
;<Avatar src="/users/ada.jpg" name="Ada Lovelace" status="online" />
// No image → deterministic initials chip.
;<Avatar name="Grace Hopper" />
// Head-crop a portrait so the face fills the circle, with a ring.
;<Avatar src="/mascot.png" name="Q" position="top" ring="#e4e8ee" ringWidth={2} />
// Overlapping stack with a +N overflow chip.
;<AvatarGroup size={32} max={3}>
<Avatar src="/a.jpg" name="Ada" />
<Avatar src="/b.jpg" name="Ben" />
<Avatar name="Cai" />
<Avatar name="Dee" />
</AvatarGroup>API
<Scrollbar> props
Extends React.HTMLAttributes<HTMLDivElement> (so className, style, onScroll, ref, … all work).
| Prop | Type | Default | Description |
| ----------------- | --------------------------------------------- | ------------ | ----------------------------------------------------------------- |
| axis | 'vertical' \| 'horizontal' \| 'both' | 'vertical' | Which axis may scroll. |
| size | number | 6 | Thumb/track thickness in px. |
| radius | number | size / 2 | Thumb corner radius in px. |
| theme | 'auto' \| 'dark' \| 'light' \| 'neutral' | 'auto' | Colour preset (see Theming). |
| thumbColor | string | theme value | Override the thumb colour. |
| thumbHoverColor | string | theme value | Override the thumb hover colour. |
| trackColor | string | transparent| Override the track colour. |
| autoHide | boolean | false | Reveal the thumb only on hover / while scrolling (webkit). |
| autoHideDelay | number | 1000 | ms the thumb stays after scrolling stops. |
| firefoxWidth | 'auto' \| 'thin' \| 'none' | 'thin' | Firefox scrollbar-width keyword. |
<Avatar> props
Extends React.HTMLAttributes<HTMLSpanElement> (minus color). Full table in the
Avatar docs.
| Prop | Type | Default | Description |
| ----------- | ------------------------------------------- | ---------- | ------------------------------------------------------- |
| src | string | — | Image URL; falls back to initials if missing / errors. |
| name | string | — | Drives auto-initials, the colour and the a11y label. |
| size | number | 40 | Diameter in px. |
| shape | 'circle' \| 'rounded' \| 'square' | 'circle' | Outline shape. |
| position | CSSProperties['objectPosition'] | 'center' | Image object-position — use 'top' to head-crop. |
| ring | string | — | Inset ring colour (ringWidth px, default 1). |
| status | 'online' \| 'offline' \| 'busy' \| 'away' | — | Presence dot in the corner. |
<AvatarGroup> overlaps children (size, max, spacing, ring, shape).
Exports
Scrollbar,GlobalScrollbarinjectGlobalScrollbarStyles(options)→ cleanup fnbuildGlobalScrollbarCss(options)→ CSS string (for SSR / manual<style>)ensureScrollbarStyles(),resolveColors(theme, overrides),scrollbarCss,SCROLLBAR_CLASSAvatar,AvatarGroupinitialsFromName(name, max?),colorFromName(name, palette?),radiusFor(shape, size),AVATAR_PALETTE,STATUS_COLORSCountryFlag— props:country(ISO 3166-1 alpha-2),size,shape,fit,className,altlookupFlag(slug),isSupported(slug),FLAG_CATALOG,SUPPORTED_SLUGS,FLAG_VIEWBOX,star5(cx, cy, r)Tag,TagImpl<T>— props:variant(subtle/solid/dot/link),size(sm/md),as(span/a/button/div),solidColor,dotColor,className,childrentagClasses(variant?, size?, dotColor?, solidColor?)— pure helpertoast(imperative handle),ToastProvider,ToastViewport,useToast/useToastContext- Types:
ScrollbarProps,ScrollbarAxis,ScrollbarTheme,ScrollbarColors,ScrollbarStyleOptions,FirefoxScrollbarWidth,GlobalScrollbarOptions,AvatarProps,AvatarGroupProps,AvatarShape,AvatarStatus,CountryFlagProps,CountryFlagShape,CountryFlagFit,CountrySlug,TagProps,TagPropsDefault,TagVariant,TagSize,TagAs,ToastApi,ToastItem,ToastOptions,ToastPosition,ToastVariant,ToastViewportProps,ToastContextValue
Theming
Colours are driven by CSS custom properties, so you can theme with props or with plain CSS.
theme="auto"(default) — uses the CSSlight-dark()function and matches whereq.cc exactly on both schemes. For it to adapt, declare acolor-schemeon your page (e.g.:root { color-scheme: light dark }). If none is declared it falls back to the light value.theme="dark" | "light"— explicit whereq.cc palette values.theme="neutral"— a translucent grey that's visible on any background, even withoutcolor-scheme.
Override individual tokens per instance:
<Scrollbar thumbColor="rgba(23,162,174,.5)" thumbHoverColor="rgba(23,162,174,.8)" />…or globally with CSS (the component reads these variables):
.wq-scrollbar {
--wq-sb-thumb: rgba(255, 255, 255, 0.1);
--wq-sb-thumb-hover: rgba(255, 255, 255, 0.2);
--wq-sb-size: 6px;
}Browser support
Native scrollbar styling: Chromium/Safari via ::-webkit-scrollbar, Firefox via
scrollbar-width + scrollbar-color. autoHide's per-state thumb reveal is a
webkit enhancement; Firefox shows the thin scrollbar consistently.
Docs & playground
- 📖 Docs — https://whereq.github.io/whereq-react/
- 🧪 Storybook — https://whereq.github.io/whereq-react/storybook/ (
pnpm storybooklocally) - 🖼️ A zero-build demo lives at
examples/demo.html.
Contributing & releasing
Issues and PRs welcome — see CONTRIBUTING.md. We ship from a
dev → main → tag pipeline; see RELEASING.md. This is the first
component in WhereQ's frontend library; more primitives are on the way.
License
MIT © WhereQ
