@quanticjs/tailwind-preset
v9.0.0
Published
Shared Tailwind CSS v4 theme for QuanticJS — design tokens, color palette, typography, light/dark mode
Readme
@quanticjs/tailwind-preset
Shared Tailwind CSS v4 theme for QuanticJS apps. Ships design tokens for colors, typography, border radius, and dark mode as a single CSS import.
Install
pnpm add @quanticjs/tailwind-presetPeer dependency: tailwindcss >=4.0.0.
Usage
Replace your hand-written @theme block with one import:
@import "tailwindcss";
@import "@quanticjs/tailwind-preset/theme.css";That's it. All Tailwind utility classes (bg-primary, text-muted-foreground, border-border, etc.) work immediately with consistent values across every project.
What's Included
Colors (Light + Dark)
Every semantic color slot that shadcn/ui components expect:
| Token | Light | Dark |
|---|---|---|
| background / foreground | White / near-black | Near-black / near-white |
| primary / primary-foreground | Blue 220° | Blue 217° |
| secondary | Light gray | Dark gray |
| muted | Light gray | Dark gray |
| accent | Light gray | Dark gray |
| destructive | Red | Dark red |
| success | Green | Dark green |
| warning | Amber | Dark amber |
| border / input / ring | Light border | Dark border |
| sidebar-* | Full sidebar palette | Full sidebar palette |
| chart-1 through chart-5 | 5-color chart palette | 5-color chart palette |
Light mode uses a faint gray page background (240 5% 98%) under pure-white cards; dark mode steps surface lightness (background 5% → card 9% → popover 12%) so depth reads without shadows. Every documented fill/foreground pair is gated at ≥ 4.5:1 (WCAG 2.2 AA) by the test suite.
Border Radius
--radius: 0.625rem (10px)
--radius-xl: calc(var(--radius) + 4px) /* surfaces: Card, DataTable */
--radius-lg: var(--radius) /* overlays: Dialog, popovers, Toast */
--radius-md: calc(var(--radius) - 2px) /* controls */
--radius-sm: calc(var(--radius) - 4px)Tier convention: controls rounded-md, surfaces rounded-xl, overlays rounded-lg. All four track --radius, so a single override re-rounds the entire UI.
Motion
--duration-fast: 120ms /* hovers, fades, overlay exits */
--duration-base: 180ms /* overlay enters */
--duration-slow: 240ms /* drawers */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1) /* enter */
--ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53) /* exit */Named animations (each with bundled keyframes): animate-overlay-in/out (fade), animate-pop-in/out (scale 0.96→1 + fade — dialogs, dropdowns, panels), animate-toast-in/out (slide from inline end + fade), animate-drawer-in/out (slide from inline start). The slide keyframes read --slide-from-end/--slide-from-start, which flip under [dir='rtl'] — RTL-correct with no extra classes. The global reduced-motion rule collapses all of them to 0.01ms.
Elevation
--shadow-surface: cards, tables
--shadow-raised: popovers, dropdowns, toasts
--shadow-overlay: dialogs, drawersUse shadow-surface | shadow-raised | shadow-overlay instead of literal shadow-md/lg/xl. Dark mode deliberately has no shadow overrides — the surface lightness steps carry depth.
Z-Index Scale
:root variables consumed as z-(--z-*) utilities:
--z-sticky: 10 sticky table columns/headers
--z-drawer: 40 mobile drawer
--z-modal: 50 dialogs
--z-popover: 60 dropdowns, notification panel (above modal: dropdowns inside dialogs)
--z-toast: 70 toasts
--z-skip-link: 100 skip-to-content linkInvariant (test-enforced): sticky < drawer < modal < popover < toast < skip-link.
Typography
--font-sans: 'Inter', 'IBM Plex Sans Arabic', ui-sans-serif, system-ui, sans-serif
--font-mono: 'JetBrains Mono', ui-monospace, monospaceLatin text renders in Inter; Arabic glyphs fall through to IBM Plex Sans Arabic (the closest visual match — similar x-height, modern grotesque, open license). The preset only declares the family names — see Loading the fonts for how to ship them.
Base Styles
* { border-color: theme(colors.border); }
body { background: theme(colors.background); color: theme(colors.foreground); }Dark Mode
Uses class strategy via @custom-variant dark (&:is(.dark *)). Add class="dark" to <html> to activate.
Don't manage that class by hand — @quanticjs/react-ui ships useTheme and ThemeToggle, which persist a light | dark | system preference, follow prefers-color-scheme, sync across tabs, and toggle this class for you. Its README also documents the inline bootstrap snippet that prevents a flash of the wrong theme before hydration.
Loading the fonts
The preset declares font names only; your app loads the files. With Fontsource:
pnpm add @fontsource-variable/inter @fontsource/ibm-plex-sans-arabic// app entry
import '@fontsource-variable/inter';
import '@fontsource/ibm-plex-sans-arabic/400.css';
import '@fontsource/ibm-plex-sans-arabic/500.css';
import '@fontsource/ibm-plex-sans-arabic/600.css';Or via Google Fonts:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@400;500;600&family=Inter:[email protected]&display=swap" rel="stylesheet" />Apps that load only Inter keep working — Arabic falls back to system fonts exactly as before.
RTL & reduced motion
- RTL needs no preset configuration: all
@quanticjs/*components use Tailwind logical properties (start-*,ps-*,text-start, …), so settingdir="rtl"on<html>(or any subtree) mirrors the entire UI. Mixed-direction pages work per-element. - Reduced motion:
theme.cssships a global@media (prefers-reduced-motion: reduce)block that near-zeroes all animation/transition durations and forcesscroll-behavior: auto(WCAG 2.2 SC 2.3.3). Components additionally carrymotion-reduce:utilities, so they behave even wheretheme.cssisn't loaded (tests, Storybook).
Overriding Tokens
Override specific tokens in your project CSS after the import:
@import "tailwindcss";
@import "@quanticjs/tailwind-preset/theme.css";
/* Project-specific brand color (OKLCH — full value, no hsl() wrapper) */
:root {
--primary: oklch(0.55 0.2 290);
--ring: oklch(0.55 0.2 290);
}
.dark {
--primary: oklch(0.72 0.16 290);
--ring: oklch(0.72 0.16 290);
}Override as few or as many tokens as needed. Unspecified tokens keep the preset defaults.
JS Token Access
For programmatic use (charts, canvas, dynamic styles), import the raw OKLCH values:
import { light, dark, radius, motion, shadows, zIndex } from '@quanticjs/tailwind-preset';
light.primary; // 'oklch(0.52 0.19 264)'
dark.primary; // 'oklch(0.72 0.16 264)'
radius; // '0.625rem'
motion.durations.base; // '180ms'
motion.easings.out; // 'cubic-bezier(0.16, 1, 0.3, 1)'
shadows.raised; // '0 4px 12px -2px oklch(0 0 0 / 0.08), …'
zIndex.toast; // 70The SemanticToken type provides autocompletion for all color token names.
Exports
// CSS (main deliverable)
import "@quanticjs/tailwind-preset/theme.css"
// JS (programmatic access)
light: Record<SemanticToken, string>
dark: Record<SemanticToken, string>
radius: string
motion: { durations: { fast; base; slow }; easings: { out; in } }
shadows: { surface; raised; overlay }
zIndex: { sticky; drawer; modal; popover; toast; skipLink }
type SemanticToken