diff --git a/src/theme/theme.css b/src/theme/theme.css
new file mode 100644
index 00000000..9aa06505
--- /dev/null
+++ b/src/theme/theme.css
@@ -0,0 +1,263 @@
+/**
+ * Force UI design tokens for Tailwind CSS v4. Import-agnostic: declares only
+ * tokens, utilities and variants — no preflight, so consumers keep control of
+ * the base layer. Consumers: @import "tailwindcss"; @import "@bsf/force-ui/theme.css";
+ *
+ * @since x.x.x
+ */
+
+@theme {
+ /* brand */
+ --color-brand-background-50: #eff6ff;
+ --color-brand-background-hover-100: #dbeafe;
+ --color-brand-200: #bfdbfe;
+ --color-brand-border-300: #93c5fd;
+ --color-brand-400: #60a5fa;
+ --color-brand-500: #3b82f6;
+ --color-brand-primary-600: #2563eb;
+ --color-brand-hover-700: #1d4ed8;
+ --color-brand-800: #1e40af;
+ --color-brand-900: #1e3a8a;
+ --color-brand-text-950: #172554;
+
+ /* background */
+ --color-background-primary: #fff;
+ --color-background-secondary: #f3f4f6;
+ --color-background-inverse: #111827;
+ --color-background-brand: #2563eb;
+ --color-background-important: #dc2626;
+
+ /* field */
+ --color-field-primary-background: #f9fafb;
+ --color-field-secondary-background: #fff;
+ --color-field-primary-hover: #f3f4f6;
+ --color-field-secondary-hover: #f3f4f6;
+ --color-field-dropzone-background: #fff;
+ --color-field-border: #e5e7eb;
+ --color-field-dropzone-background-hover: #f9fafb;
+ --color-field-dropzone-color: #2563eb;
+ --color-field-label: #111827;
+ --color-field-input: #111827;
+ --color-field-helper: #6b7280;
+ --color-field-background-disabled: #f9fafb;
+ --color-field-color-disabled: #bdc1c7;
+ --color-field-placeholder: #6b7280;
+ --color-field-border-disabled: #f3f4f6;
+ --color-field-color-error: #dc2626;
+ --color-field-border-error: #fecaca;
+ --color-field-background-error: #fef2f2;
+ --color-field-required: #dc2626;
+
+ /* border */
+ --color-border-interactive: #2563eb;
+ --color-border-subtle: #e5e7eb;
+ --color-border-strong: #6b7280;
+ --color-border-inverse: #374151;
+ --color-border-disabled: #e5e7eb;
+ --color-border-muted: #e5e7eb;
+ --color-border-error: #dc2626;
+ --color-border-transparent-subtle: #37415114;
+ --color-border-white: #fff;
+
+ /* text */
+ --color-text-primary: #111827;
+ --color-text-secondary: #4b5563;
+ --color-text-tertiary: #6b7280;
+ --color-text-on-color: #fff;
+ --color-text-error: #dc2626;
+ --color-text-error-inverse: #f87171;
+ --color-text-inverse: #fff;
+ --color-text-disabled: #bdc1c7;
+ --color-text-on-button-disabled: #9ca3af;
+
+ /* link */
+ --color-link-primary: #2563eb;
+ --color-link-primary-hover: #1d4ed8;
+ --color-link-inverse: #38bdf8;
+ --color-link-visited: #7c3aed;
+ --color-link-visited-inverse: #a78bfa;
+ --color-link-inverse-hover: #7dd3fc;
+
+ /* icon */
+ --color-icon-primary: #111827;
+ --color-icon-secondary: #4b5563;
+ --color-icon-on-color: #fff;
+ --color-icon-inverse: #fff;
+ --color-icon-interactive: #2563eb;
+ --color-icon-on-color-disabled: #9ca3af;
+ --color-icon-disabled: #bdc1c7;
+
+ /* support */
+ --color-support-error: #dc2626;
+ --color-support-success: #16a34a;
+ --color-support-warning: #eab308;
+ --color-support-info: #0284c7;
+ --color-support-error-inverse: #f87171;
+ --color-support-success-inverse: #4ade80;
+ --color-support-warning-inverse: #fde047;
+ --color-support-info-inverse: #38bdf8;
+
+ /* button */
+ --color-button-primary: #2563eb;
+ --color-button-primary-hover: #1d4ed8;
+ --color-button-secondary: #1f2937;
+ --color-button-secondary-hover: #374151;
+ --color-button-tertiary: #fff;
+ --color-button-tertiary-hover: #f9fafb;
+ --color-button-danger: #dc2626;
+ --color-button-danger-secondary: #dc2626;
+ --color-button-danger-hover: #b91c1c;
+ --color-button-disabled: #f3f4f6;
+ --color-button-tertiary-border: #e5e7eb;
+ --color-button-tertiary-color: #111827;
+
+ /* focus */
+ --color-focus: #2563eb;
+ --color-focus-inset: #fff;
+ --color-focus-inverse: #38bdf8;
+ --color-focus-inverse-inset: #111827;
+ --color-focus-error: #dc2626;
+ --color-focus-border: #bfdbfe;
+ --color-focus-error-border: #fecaca;
+
+ /* misc */
+ --color-misc-highlight: #bfdbfe;
+ --color-misc-overlay: #11182780;
+ --color-misc-skeleton-background: #f3f4f6;
+ --color-misc-skeleton-element: #bdc1c7;
+ --color-misc-popup-button-hover: #1118270d;
+ --color-misc-tab-item-hover: #e5e7eb;
+ --color-misc-dropdown-hover: #f3f4f6;
+ --color-misc-loader-base: #1118270d;
+ --color-misc-loader-color: #2563eb;
+ --color-misc-progress-background: #e5e7eb;
+
+ /* badge */
+ --color-badge-background-gray: #f9fafb;
+ --color-badge-color-gray: #1f2937;
+ --color-badge-hover-gray: #f3f4f6;
+ --color-badge-border-gray: #e5e7eb;
+ --color-badge-background-red: #fef2f2;
+ --color-badge-color-red: #b91c1c;
+ --color-badge-hover-red: #fee2e2;
+ --color-badge-border-red: #fecaca;
+ --color-badge-background-yellow: #fefce8;
+ --color-badge-color-yellow: #a16207;
+ --color-badge-hover-yellow: #fef9c3;
+ --color-badge-border-yellow: #fef08a;
+ --color-badge-hover-green: #dcfce7;
+ --color-badge-border-green: #bbf7d0;
+ --color-badge-background-green: #f0fdf4;
+ --color-badge-color-green: #15803d;
+ --color-badge-background-sky: #f0f9ff;
+ --color-badge-color-sky: #0369a1;
+ --color-badge-hover-sky: #e0f2fe;
+ --color-badge-border-sky: #bae6fd;
+ --color-badge-background-disabled: #f3f4f6;
+ --color-badge-color-disabled: #bdc1c7;
+ --color-badge-hover-disabled: #f3f4f6;
+ --color-badge-border-disabled: #e5e7eb;
+ --color-badge-background-important: #dc2626;
+
+ /* alert */
+ --color-alert-background-neutral: #fff;
+ --color-alert-border-neutral: #e5e7eb;
+ --color-alert-background-danger: #fef2f2;
+ --color-alert-border-danger: #fecaca;
+ --color-alert-background-warning: #fefce8;
+ --color-alert-border-warning: #fef08a;
+ --color-alert-background-green: #f0fdf4;
+ --color-alert-border-green: #bbf7d0;
+ --color-alert-background-info: #f0f9ff;
+ --color-alert-border-info: #bae6fd;
+
+ /* tab */
+ --color-tab-background: #f3f4f6;
+ --color-tab-border: #e5e7eb;
+
+ /* tooltip */
+ --color-tooltip-background-light: #fff;
+ --color-tooltip-background-dark: #111827;
+
+ /* toggle */
+ --color-toggle-off: #e5e7eb;
+ --color-toggle-on: #2563eb;
+ --color-toggle-dial-background: #fff;
+ --color-toggle-off-hover: #bdc1c7;
+ --color-toggle-off-border: #bdc1c7;
+ --color-toggle-on-hover: #3b82f6;
+ --color-toggle-on-border: #60a5fa;
+ --color-toggle-on-disabled: #eff6ff;
+ --color-toggle-off-disabled: #f3f4f6;
+
+ /* box-shadow — overrides v4 default --shadow-xs and adds custom scale */
+ --shadow-xs: 0 1px 2px 0 rgba(16, 24, 40, 0.05);
+ --shadow-soft-shadow-sm: 0 6px 32px -12px rgba(149, 160, 178, 0.12);
+ --shadow-soft-shadow: 0 8px 32px -12px rgba(149, 160, 178, 0.16);
+ --shadow-soft-shadow-md: 0 10px 32px -12px rgba(149, 160, 178, 0.2);
+ --shadow-soft-shadow-lg: 0 12px 32px -12px rgba(149, 160, 178, 0.24);
+ --shadow-soft-shadow-xl: 0 16px 32px -12px rgba(149, 160, 178, 0.32);
+ --shadow-soft-shadow-2xl: 0 24px 64px -12px rgba(149, 160, 178, 0.32);
+ --shadow-soft-shadow-inner: 0 1px 1px 0 rgba(0, 0, 0, 0.05);
+ --shadow-toggle-disabled: 1px 1px 2px 0 rgba(0, 0, 0, 0.1) inset;
+
+ /* font-size */
+ --text-tiny: 0.625rem;
+
+ /* letter-spacing */
+ --tracking-2: 0.125em;
+}
+
+/* RTL-aware variant (was a tailwindcss/plugin addVariant in v3). */
+@custom-variant not-rtl (&:not([dir="rtl"], [dir="rtl"] *));
+
+/* v3 borderWidth {0.5} parity, incl. per-side variants. */
+@utility border-0\0.5 {
+ border-width: 0.5px;
+}
+
+@utility border-t-0\0.5 {
+ border-top-width: 0.5px;
+}
+
+@utility border-b-0\0.5 {
+ border-bottom-width: 0.5px;
+}
+
+@utility border-l-0\0.5 {
+ border-left-width: 0.5px;
+}
+
+@utility border-r-0\0.5 {
+ border-right-width: 0.5px;
+}
+
+@utility border-x-0\0.5 {
+ border-inline-width: 0.5px;
+}
+
+@utility border-y-0\0.5 {
+ border-block-width: 0.5px;
+}
+
+/* v3 divide 0.5 parity; shapes mirror v4 divide-* emission. */
+@utility divide-y-0\0.5 {
+ :where(& > :not(:last-child)) {
+ --tw-divide-y-reverse: 0;
+
+ border-bottom-style: var(--tw-border-style);
+ border-top-style: var(--tw-border-style);
+ border-top-width: calc(0.5px * var(--tw-divide-y-reverse));
+ border-bottom-width: calc(0.5px * calc(1 - var(--tw-divide-y-reverse)));
+ }
+}
+
+@utility divide-x-0\0.5 {
+ :where(& > :not(:last-child)) {
+ --tw-divide-x-reverse: 0;
+
+ border-inline-style: var(--tw-border-style);
+ border-inline-start-width: calc(0.5px * var(--tw-divide-x-reverse));
+ border-inline-end-width: calc(0.5px * calc(1 - var(--tw-divide-x-reverse)));
+ }
+}
diff --git a/src/utilities/withTW.js b/src/utilities/withTW.js
index fec63674..5a7e5a8e 100644
--- a/src/utilities/withTW.js
+++ b/src/utilities/withTW.js
@@ -1,6 +1,19 @@
import deepmerge from 'deepmerge';
import libraryConfig from '../theme/default-config';
+/**
+ * Deep-merges the Force UI theme into a consumer Tailwind config.
+ *
+ * @deprecated since 2.0.0. Force UI is now CSS-first for Tailwind v4. Instead of
+ * merging a JS config, import the shipped tokens in your CSS entry with
+ * `"tailwindcss"` followed by `"@bsf/force-ui/theme.css"`. This helper is
+ * retained only for consumers still on Tailwind v4's legacy JS-config path
+ * (loaded via the CSS `@config` directive) and will be removed in 3.0.0.
+ *
+ * @since x.x.x
+ * @param {Object} tailwindConfig Consumer Tailwind config object.
+ * @return {Object} Config merged with the Force UI theme.
+ */
const withTW = ( tailwindConfig ) => {
return deepmerge( libraryConfig, { ...tailwindConfig } );
};
diff --git a/tailwind.config.js b/tailwind.config.js
deleted file mode 100644
index c3ccfbb8..00000000
--- a/tailwind.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const withTW = require('./src/utilities/withTW');
-
-module.exports = withTW({
- content: ['./src/**/*.{ts,tsx,js,jsx}'],
- plugins: [require('@tailwindcss/container-queries')],
-});
diff --git a/version.json b/version.json
index b4050a71..fdc981e4 100644
--- a/version.json
+++ b/version.json
@@ -1,3 +1,3 @@
{
- "force-ui": "1.8.1"
+ "force-ui": "2.0.0"
}
diff --git a/vite.config.ts b/vite.config.ts
index d86d867f..45f32e7c 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,9 +2,22 @@ import preserveDirectives from 'rollup-preserve-directives';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
+import { copyFileSync } from 'fs';
import dts from 'vite-plugin-dts';
import pkg from './package.json';
+// Ship the CSS-first design tokens as a raw asset (@bsf/force-ui/theme.css).
+// Runs on closeBundle so it lands after Vite clears dist (emptyOutDir).
+const copyThemeCss = () => ( {
+ name: 'force-ui-copy-theme-css',
+ closeBundle() {
+ copyFileSync(
+ resolve(process.cwd(), 'src/theme/theme.css'),
+ resolve(process.cwd(), 'dist/theme.css')
+ );
+ },
+} );
+
// https://vitejs.dev/config/
export default defineConfig({
build: {
@@ -78,6 +91,7 @@ export default defineConfig({
exclude: ['**/node_modules/**', '**/_virtual/**'],
}),
preserveDirectives(),
+ copyThemeCss(),
],
esbuild: {
treeShaking: true,