@webority/theme
v0.9.14
Published
Webority shared design system — Bootstrap 5.3 SCSS overrides, --wui-* tokens, and shared component CSS. The single source of brand chrome for React portals and Razor sites.
Readme
@webority/theme
Webority's internal design system — Bootstrap 5.3 SCSS overrides, --wui-* tokens, and
shared component CSS. Consumed by @webority/ui-react and Webority.Ui.Razor.
Published publicly so npm install needs no private registry. Built for Webority
products; no support commitment outside them.
Bias: thin layer on Bootstrap. Shared identity + control language + hard gaps — not a second framework and not every product's marketing site.
Use
Compile the partials with your brand values, in this order — the order is load-bearing:
@import "your-brand/variables"; // your $primary, fonts, etc.
@import "@webority/theme/scss/variables"; // library defaults (yours win)
@import "bootstrap/scss/bootstrap";
@import "@webority/theme/scss/tokens"; // --wui-* tokens
@import "@webority/theme/scss/wui-aliases"; // --color-* / --radius-* aliases
@import "@webority/theme/scss/components"; // portal / control chrome
@import "@webority/theme/scss/wui-components";
// Optional — marketing sites only (heroes, section-y, pill CTAs, reveal motion):
// @import "@webority/theme/scss/marketing";Importing part of the core chain is a bug: skipping wui-aliases leaves every
--color-* reference unresolved. Country flags are pulled by wui-components
for the phone control — do not import flags a second time (legacy @import
does not dedupe).
$secondary is quiet grey (de-emphasised text/buttons), not a second brand
colour — intentional shared semantics (see _variables.scss).
dist/webority-theme.css is a precompiled default-brand portal build (no
marketing partial). Prefer the SCSS chain above for brand-exact output.
Dark mode
The theme ships with two built-in dark-mode support patterns:
Manual toggle (production)
Set data-theme="dark" and data-bs-theme="dark" on <html> to flip both WUI and Bootstrap
native components together. The dark token remap maps all --color-* aliases onto dark hexes,
and Bootstrap's utility classes (.text-body, .bg-secondary) and components respond via
the [data-bs-theme="dark"] selector that Bootstrap provides. Pair the attributes—setting
one without the other leaves part of the system on the wrong palette.
Persist the choice in localStorage and restore it on next visit:
// On mount:
const theme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.setAttribute('data-bs-theme', theme);
// On toggle:
const newTheme = theme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
document.documentElement.setAttribute('data-bs-theme', newTheme);
localStorage.setItem('theme', newTheme);Automatic detection (opt-in)
Set data-theme="auto" on <html> to detect the system preference via @media (prefers-color-scheme: dark)
and apply the dark remap automatically. Works with user-level system settings (OS dark mode);
does not persist a user's manual toggle (every reload reads the OS setting). Use for opt-in
portals where the portal doesn't control color preference; omit for apps that give users a
theme picker.
Implementation
Both patterns use the same 60-line dark-token SCSS mixin (defined in _wui-aliases.scss),
so the compiled dark CSS is byte-identical whether toggled manually or via system preference.
Token remap includes:
- Surfaces & text —
--color-bg,--color-border,--color-text* - Bootstrap RGB triplets —
--bs-body-bg-rgb,--bs-body-color-rgb, etc., so.bg-body/.text-bodyutilities flip - Brand & status colors — primary, success, danger, warning, info (mapped to readable values on dark surfaces)
- Scrim — overlay dimming adjusted for dark backgrounds
- Component shadows — slider thumbs, badge accents, etc. re-mapped for visibility
Marketing sites ship light-only (the scss/_marketing.scss partial is opt-in on portals
and omitted on marketing); dark mode is a portal feature, not a marketing requirement.
Library vs product ownership
Three layers. Do not collapse product art direction into the shared package.
| Layer | Owner | What |
|---|---|---|
| A — Bootstrap | Bootstrap | Grid, utilities, reboot, structure of .btn / .form-control / .modal / offcanvas |
| B — Shared UI system | this package + App* | Brandable tokens, one field/selection skin, portal shell/table/modal, hard interactive, React↔Razor parity |
| C — Product / marketing | Each product | Brand hexes, page layouts, heroes/motion, domain components, router/auth/content |
Products own (do not re-implement in the library)
- Brand values —
$primary/--wui-primary*(and related) via pre-bootstrap variables or runtime CSS; usewui-brand-primarywhen setting primary once - Marketing art direction — heroes, section rhythm, marquee, scroll reveals, page entry fades, marketing-only button shapes (pill / ghost-dark), wide marketing canvas
- Screens and domain UI — feature layouts, status→label maps, RoleBadge-style domain components
- App infrastructure — routing, auth, i18n copy, charts, rich text, API
Products must not own (deviation)
- Forked
AppButton/AppSelect/ theme hex tables - A second select/date/phone kit (Radix, react-day-picker, etc.)
- A second styling system (Tailwind, MUI, hand-rolled
.btnkit) - Per-product copies of shell/table/modal chrome
Ship rule for new PRs into this package
| Proposed change | Put it here if… | Put it in the product if… |
|---|---|---|
| Bootstrap $ or class restyle | Fixes shared a11y, alignment, or rebrand | Looks cool on one landing page |
| New CSS in theme | Both React and Razor portals need the same look | Marketing-only motion or section art |
| New App* / <app-*> | ≥2 products need it, or Bootstrap fails, or parity requires it | One screen / one domain |
| New token | Shared semantic meaning (status, surface, control height) | One brand's one-off hex |
What stays in the library (do not move to products)
Anti-over-correction checklist. When thinning the package (e.g. marketing extract), keep:
Tokens & system (scss/_variables, _tokens, _wui-aliases)
- Theme colors +
wui-brand-primary; body/text/surface/border ramps (incl. AA tertiary) - Radius ladder that differs from Bootstrap; spacer map extensions (6+)
- Status ramps: fills and
-text/ danger-strong for AA - Control heights (
--wui-control-height*); flat in-page elevation; overlay shadows only - Z-index scale (
--wui-z-*); dark-mode--bs-*/ RGB bridges - Focus: no input glow (
$input-focus-box-shadow: none); brand border as focus cue in post-CSS
One skin on Bootstrap classes (not a second kit)
- Fields:
.form-control/.form-selectchrome;.input/.select/.textareaare aliases for size/shell hooks - Selection:
.form-check-inputglyph;.wui-check/.wui-radioare aliases (table select compact under.wui-select-cellonly) - Buttons: tokenized primary / danger / outline / ghost / disabled; shared control height on default size
- Dropdown, card surface/radius, table cell padding via SCSS vars where specificity requires it
Hard interactive & portal patterns (packages outside this folder, same monorepo)
@webority/ui-elements+ App* wrappers: select, multiselect, autocomplete, phone, date, daterange, OTP- Portal:
AppShell,AppDataTable/ ledger,AppModal/AppConfirmDialog, toast contract,AppSidebarMenu - React↔Razor parity gates and unit tests for catalogue components
Marketing (opt-in — not default)
Editorial helpers live in scss/_marketing.scss, imported as
@webority/theme/scss/marketing after the core chain. Includes section-y,
eyebrow, h-display, hero/CTA gradients, btn-pill / btn-ghost-dark, link-wipe,
container-cloves, hero-wash, page/stagger/reveal motion, marquee, section
content-visibility. Portals omit this import. Marketing sites that used
these classes must add the import.
Catalogue tiers
Keep shipping all of these; classify for when to use, not for removal:
| Tier | Components | Library? | Notes |
|---|---|---|---|
| Portal core | Shell, DataTable, fields/selects, Modal, Confirm, Tabs, Card, Alert, Badge, StatusBadge, Stepper, File*, Date*, Phone, OTP, Toast, Empty/Skeleton/Spinner | Yes — never product-fork | Daily product UI |
| Portal display | AppKpiTile, AppInfoList, AppTimeline, AppCallout, AppSplash, AppBanner, AppBreadcrumbs, AppProgress, AppMeter | Yes | Shared across portals |
| Commercial / plan UI | AppPricingCard, AppCompareTable, AppResourceCard | Yes — keep | Pricing pages and in-app plan/billing. Editorial shape, not a second design system. Moving them to products would break React↔Razor parity. |
| CSS marketing only | section-y, btn-pill, reveal, marquee, … | Opt-in partial | Not App*; see marketing import above |
Do not remove PricingCard / CompareTable / ResourceCard without a multi-product usage audit outside this monorepo. Inventory conclusion: keep in library, document tier.
Squircle policy
corner-shape: squircle is applied on many controls (buttons, fields, chips, menus, shell) as progressive enhancement:
- Supporting browsers get slightly smoother corners; others keep plain
border-radius. - Not a cross-browser design contract; not a WCAG requirement.
- Missing squircle is not a bug.
Policy: keep on shared control surfaces for consistent geometry; do not strip globally. Optional .rounded-squircle remains an opt-in utility. Products that want pure arcs only override locally — do not fork App*.
