diff --git a/bun.lock b/bun.lock
index 3ea6f31f5d..643a062813 100644
--- a/bun.lock
+++ b/bun.lock
@@ -413,6 +413,10 @@
"drizzle-kit": "catalog:",
},
},
+ "packages/css": {
+ "name": "@opencode-ai/css",
+ "version": "1.18.4",
+ },
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.18.4",
@@ -2085,6 +2089,8 @@
"@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"],
+ "@opencode-ai/css": ["@opencode-ai/css@workspace:packages/css"],
+
"@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"],
"@opencode-ai/docs": ["@opencode-ai/docs@workspace:packages/docs"],
diff --git a/packages/css/LICENSE b/packages/css/LICENSE
new file mode 100644
index 0000000000..6439474bee
--- /dev/null
+++ b/packages/css/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 opencode
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/css/README.md b/packages/css/README.md
new file mode 100644
index 0000000000..c87eeeec8f
--- /dev/null
+++ b/packages/css/README.md
@@ -0,0 +1,156 @@
+# @opencode-ai/css
+
+OpenCode's design system as plain CSS. It has no JavaScript, framework, build tool, or runtime dependency.
+
+Use semantic HTML and `data-*` attributes. Native browser states such as `:hover`, `:focus-visible`, and `:disabled` work automatically; your JavaScript can represent application state with attributes such as `data-expanded`, `data-checked`, and `data-invalid`.
+
+## Install
+
+```sh
+npm install @opencode-ai/css
+```
+
+Load the complete design system from JavaScript:
+
+```js
+import "@opencode-ai/css"
+```
+
+Or from CSS:
+
+```css
+@import "@opencode-ai/css";
+```
+
+For a static page with no package tooling:
+
+```html
+
+```
+
+Add `data-oc-root` to the element that should receive the default page background, text color, and font. Set `data-color-scheme` to `light` or `dark` on that element or an ancestor.
+
+```html
+
+ Deploy
+
+```
+
+## Selective imports
+
+The default export contains every component. Small pages can load only what they use:
+
+```css
+@import "@opencode-ai/css/tokens.css";
+@import "@opencode-ai/css/base.css";
+@import "@opencode-ai/css/components/button.css";
+@import "@opencode-ai/css/components/badge.css";
+@import "@opencode-ai/css/components/table.css";
+```
+
+`tokens.css` defines the color ramps and semantic theme variables. `base.css` is intentionally small and scoped: it only applies the full-page defaults to `[data-oc-root]` and normalization to design-system elements.
+
+## Common components
+
+### Button
+
+```html
+Create release
+Delete
+Disabled
+```
+
+Variants: `neutral`, `contrast`, `outline`, `ghost`, `ghost-muted`, `danger`, `warning`, and `loading`. Sizes: `small`, `normal`, and `large`.
+
+### Badge
+
+```html
+Inactive Active
+```
+
+### Internal-tool page
+
+```html
+
+
+
+
Release control
+
Artifacts
+
+
+
+
+
+ Published builds
+ Newest first.
+
+
+
+
+
+
+ Name
+ Status
+
+
+
+
+
+ cli@1.0.0
+ Active
+
+ Activate
+
+
+
+
+
+
+
+
+```
+
+### JavaScript-owned state
+
+The package styles state; it does not implement behavior. Keep semantic HTML and ARIA as the source of truth, then mirror state to a `data-*` attribute when a component needs it.
+
+```js
+const trigger = document.querySelector('[data-component="button"]')
+trigger.addEventListener("click", () => {
+ const expanded = trigger.getAttribute("aria-expanded") !== "true"
+ trigger.setAttribute("aria-expanded", String(expanded))
+ trigger.toggleAttribute("data-expanded", expanded)
+})
+```
+
+## Theme customization
+
+Override semantic tokens at your app root instead of editing component CSS:
+
+```css
+[data-oc-root] {
+ --oc-font-family-sans: Inter, system-ui, sans-serif;
+ --oc-page-max-width: 1440px;
+ --oc-bg-accent: #7152f4;
+ --oc-border-focus: #8271f8;
+}
+```
+
+The low-level color ramps live in `colors.css`; light and dark semantic mappings live in `theme.css`.
+
+## Component inventory
+
+The first release is intentionally focused on simple internal tools:
+
+- Layout: page, surface, and table
+- Actions and status: button and badge
+- Forms: field, input, select, textarea, and native checkbox/radio styling
+- Structure: divider
+
+Each stylesheet is available as `@opencode-ai/css/components/.css`. This small interface avoids publishing framework-specific DOM contracts for menus, dialogs, comboboxes, and other interactive widgets.
+
+## Scope
+
+This package is styling only. It does not create DOM, manage focus, provide keyboard navigation, position overlays, or add ARIA attributes. Use native HTML for simple controls and a small behavior layer of your choice for menus, dialogs, comboboxes, and other interactive widgets.
+
+`@opencode-ai/ui` does not depend on this package yet. The CSS is intentionally isolated while its public interface settles.
diff --git a/packages/css/base.css b/packages/css/base.css
new file mode 100644
index 0000000000..b90a82af7b
--- /dev/null
+++ b/packages/css/base.css
@@ -0,0 +1,32 @@
+@layer base {
+ :root {
+ --font-weight-regular: 440;
+ --line-height-normal: 1.5;
+ --letter-spacing-normal: normal;
+ --oc-page-max-width: 1200px;
+ --oc-page-padding-block: 56px;
+ }
+
+ :where([data-component], [data-component] *, [data-slot]) {
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: geometricPrecision;
+ }
+
+ :where([data-component]) {
+ font-family: var(--oc-font-family-sans);
+ }
+}
+
+@layer base {
+ [data-oc-root] {
+ min-height: 100%;
+ margin: 0;
+ background: var(--oc-bg-deep);
+ color: var(--oc-text-base);
+ font-family: var(--oc-font-family-sans);
+ font-size: 13px;
+ line-height: 1.5;
+ }
+}
diff --git a/packages/css/colors.css b/packages/css/colors.css
new file mode 100644
index 0000000000..fe616b4945
--- /dev/null
+++ b/packages/css/colors.css
@@ -0,0 +1,172 @@
+@layer theme {
+ :root {
+ /* ── Grey ── */
+ --oc-grey-50: #ffffffff;
+ --oc-grey-100: #fafafaff;
+ --oc-grey-200: #f2f2f2ff;
+ --oc-grey-300: #eeeeeeff;
+ --oc-grey-400: #dbdbdbff;
+ --oc-grey-500: #aeaeaeff;
+ --oc-grey-600: #808080ff;
+ --oc-grey-700: #5c5c5cff;
+ --oc-grey-800: #3a3a3aff;
+ --oc-grey-900: #2e2e2eff;
+ --oc-grey-1000: #242424ff;
+ --oc-grey-1100: #161616ff;
+ --oc-grey-1200: #080808ff;
+
+ /* ── Alpha Dark ── */
+ --oc-alpha-dark-100: #000000ff;
+ --oc-alpha-dark-90: #000000e5;
+ --oc-alpha-dark-80: #000000cc;
+ --oc-alpha-dark-70: #000000b2;
+ --oc-alpha-dark-60: #00000099;
+ --oc-alpha-dark-50: #00000080;
+ --oc-alpha-dark-40: #00000066;
+ --oc-alpha-dark-30: #0000004d;
+ --oc-alpha-dark-24: #0000003d;
+ --oc-alpha-dark-20: #00000033;
+ --oc-alpha-dark-16: #00000029;
+ --oc-alpha-dark-14: #00000024;
+ --oc-alpha-dark-12: #0000001f;
+ --oc-alpha-dark-10: #0000001a;
+ --oc-alpha-dark-8: #00000014;
+ --oc-alpha-dark-6: #0000000f;
+ --oc-alpha-dark-4: #0000000a;
+ --oc-alpha-dark-2: #00000005;
+ --oc-alpha-dark-0: #00000000;
+
+ /* ── Alpha Light ── */
+ --oc-alpha-light-100: #ffffffff;
+ --oc-alpha-light-90: #ffffffe5;
+ --oc-alpha-light-80: #ffffffcc;
+ --oc-alpha-light-70: #ffffffb2;
+ --oc-alpha-light-60: #ffffff99;
+ --oc-alpha-light-50: #ffffff80;
+ --oc-alpha-light-40: #ffffff66;
+ --oc-alpha-light-30: #ffffff4d;
+ --oc-alpha-light-24: #ffffff3d;
+ --oc-alpha-light-20: #ffffff33;
+ --oc-alpha-light-16: #ffffff29;
+ --oc-alpha-light-14: #ffffff24;
+ --oc-alpha-light-12: #ffffff1f;
+ --oc-alpha-light-10: #ffffff1a;
+ --oc-alpha-light-8: #ffffff14;
+ --oc-alpha-light-6: #ffffff0f;
+ --oc-alpha-light-4: #ffffff0a;
+ --oc-alpha-light-2: #ffffff05;
+ --oc-alpha-light-0: #ffffff00;
+
+ /* ── Red ── */
+ --oc-red-100: #fcecebff;
+ --oc-red-200: #f6d5d3ff;
+ --oc-red-300: #f2bbb7ff;
+ --oc-red-400: #f29b96ff;
+ --oc-red-500: #f17471ff;
+ --oc-red-600: #f1484fff;
+ --oc-red-700: #d92e3cff;
+ --oc-red-800: #b82d35ff;
+ --oc-red-900: #97252bff;
+ --oc-red-1000: #7a1f23ff;
+ --oc-red-1100: #5f1a1cff;
+ --oc-red-1200: #461516ff;
+
+ /* ── Orange ── */
+ --oc-orange-100: #fdf2edff;
+ --oc-orange-200: #ffe7dcff;
+ --oc-orange-300: #ffd8c6ff;
+ --oc-orange-400: #ffc1a4ff;
+ --oc-orange-500: #ffa478ff;
+ --oc-orange-600: #ff8648ff;
+ --oc-orange-700: #ee7330ff;
+ --oc-orange-800: #d16427ff;
+ --oc-orange-900: #b35624ff;
+ --oc-orange-1000: #954c27ff;
+ --oc-orange-1100: #723d22ff;
+ --oc-orange-1200: #5a2c14ff;
+
+ /* ── Yellow ── */
+ --oc-yellow-100: #fefaecff;
+ --oc-yellow-200: #fcefd0ff;
+ --oc-yellow-300: #f7e5b5ff;
+ --oc-yellow-400: #f3da9bff;
+ --oc-yellow-500: #f2cf76ff;
+ --oc-yellow-600: #f6c251ff;
+ --oc-yellow-700: #e7af36ff;
+ --oc-yellow-800: #cb9f34ff;
+ --oc-yellow-900: #ac8833ff;
+ --oc-yellow-1000: #8e7231ff;
+ --oc-yellow-1100: #68552bff;
+ --oc-yellow-1200: #4b4025ff;
+
+ /* ── Green ── */
+ --oc-green-100: #e7f9eaff;
+ --oc-green-200: #d0f0d5ff;
+ --oc-green-300: #b8e9c1ff;
+ --oc-green-400: #96e3a6ff;
+ --oc-green-500: #6bd586ff;
+ --oc-green-600: #49c970ff;
+ --oc-green-700: #2eaf5aff;
+ --oc-green-800: #198b43ff;
+ --oc-green-900: #1d783cff;
+ --oc-green-1000: #196130ff;
+ --oc-green-1100: #164c26ff;
+ --oc-green-1200: #14361dff;
+
+ /* ── Cyan ── */
+ --oc-cyan-100: #e2f7fbff;
+ --oc-cyan-200: #c4edf4ff;
+ --oc-cyan-300: #a3e4efff;
+ --oc-cyan-400: #65d9ebff;
+ --oc-cyan-500: #00c5dfff;
+ --oc-cyan-600: #00abcfff;
+ --oc-cyan-700: #0096b8ff;
+ --oc-cyan-800: #007d9bff;
+ --oc-cyan-900: #006c85ff;
+ --oc-cyan-1000: #005a6eff;
+ --oc-cyan-1100: #004756ff;
+ --oc-cyan-1200: #00353fff;
+
+ /* ── Blue ── */
+ --oc-blue-100: #ecf1feff;
+ --oc-blue-200: #d7e2fcff;
+ --oc-blue-300: #c3d4fdff;
+ --oc-blue-400: #a2bcffff;
+ --oc-blue-500: #7698fdff;
+ --oc-blue-600: #3b5cf6ff;
+ --oc-blue-700: #3250dfff;
+ --oc-blue-800: #2c47c8ff;
+ --oc-blue-900: #263fa9ff;
+ --oc-blue-1000: #22388fff;
+ --oc-blue-1100: #1c2e70ff;
+ --oc-blue-1200: #1b2852ff;
+
+ /* ── Purple ── */
+ --oc-purple-100: #ebecfeff;
+ --oc-purple-200: #d5d5fcff;
+ --oc-purple-300: #b9b8f5ff;
+ --oc-purple-400: #9e99f7ff;
+ --oc-purple-500: #8271f8ff;
+ --oc-purple-600: #7152f4ff;
+ --oc-purple-700: #623be2ff;
+ --oc-purple-800: #5230c2ff;
+ --oc-purple-900: #442aa1ff;
+ --oc-purple-1000: #361f83ff;
+ --oc-purple-1100: #2b1b6aff;
+ --oc-purple-1200: #221358ff;
+
+ /* ── Pink ── */
+ --oc-pink-100: #fdecf3ff;
+ --oc-pink-200: #f7d5e4ff;
+ --oc-pink-300: #fabcd8ff;
+ --oc-pink-400: #f799c6ff;
+ --oc-pink-500: #f26cb2ff;
+ --oc-pink-600: #f64aabff;
+ --oc-pink-700: #e4429eff;
+ --oc-pink-800: #c83d8bff;
+ --oc-pink-900: #aa3576ff;
+ --oc-pink-1000: #8c2d61ff;
+ --oc-pink-1100: #6f284fff;
+ --oc-pink-1200: #5c1d3fff;
+ }
+}
diff --git a/packages/css/components.css b/packages/css/components.css
new file mode 100644
index 0000000000..7a643c2002
--- /dev/null
+++ b/packages/css/components.css
@@ -0,0 +1,9 @@
+@import "./components/badge.css" layer(components);
+@import "./components/button.css" layer(components);
+@import "./components/check.css" layer(components);
+@import "./components/divider.css" layer(components);
+@import "./components/field.css" layer(components);
+@import "./components/input.css" layer(components);
+@import "./components/page.css" layer(components);
+@import "./components/surface.css" layer(components);
+@import "./components/table.css" layer(components);
diff --git a/packages/css/components/badge.css b/packages/css/components/badge.css
new file mode 100644
index 0000000000..600f4729d3
--- /dev/null
+++ b/packages/css/components/badge.css
@@ -0,0 +1,33 @@
+[data-component="badge"] {
+ box-sizing: border-box;
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ height: 16px;
+ padding: 0 4px;
+ user-select: none;
+
+ border-radius: 2px;
+ border: 0.5px solid var(--oc-border-base);
+ background: var(--oc-bg-layer-02);
+
+ font-style: normal;
+ font-weight: 530;
+ font-size: 11px;
+ line-height: 1;
+ letter-spacing: 0.05px;
+ color: var(--oc-text-muted);
+ font-variant-numeric: tabular-nums;
+}
+
+[data-component="badge"][data-high-contrast] {
+ border-color: var(--oc-border-strong);
+}
+
+[data-component="badge"][data-variant="accent"] {
+ border: 0;
+ background: var(--oc-bg-accent);
+ color: var(--oc-text-contrast);
+}
diff --git a/packages/css/components/button.css b/packages/css/components/button.css
new file mode 100644
index 0000000000..4a629369cd
--- /dev/null
+++ b/packages/css/components/button.css
@@ -0,0 +1,246 @@
+[data-component="button"] {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ border-radius: 6px;
+ font-style: normal;
+ font-weight: 530;
+ font-size: 13px;
+ line-height: 20px;
+ color: var(--oc-text-base);
+ text-shadow: none;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: -0.04px;
+ user-select: none;
+ cursor: pointer;
+ outline: none;
+}
+
+[data-component="button"]:focus {
+ outline: none;
+}
+
+[data-component="button"]:is(:focus-visible, [data-state="focus"]):not(:disabled):not([data-variant="loading"]) {
+ outline: 2px solid var(--oc-border-focus);
+ outline-offset: 2.5px;
+}
+
+[data-component="button"][data-size="normal"] {
+ height: 28px;
+ padding: 0 11px;
+}
+
+[data-component="button"][data-size="small"] {
+ height: 24px;
+ padding: 0 9px;
+ border-radius: 4px;
+}
+
+[data-component="button"][data-size="large"] {
+ height: 32px;
+ padding: 0 15px;
+}
+
+[data-component="button"][data-icon][data-size="small"] {
+ padding-left: 9px;
+}
+
+[data-component="button"][data-icon][data-size="normal"] {
+ padding-left: 11px;
+}
+
+[data-component="button"][data-icon][data-size="large"] {
+ padding-left: 15px;
+}
+
+[data-component="button"] [data-slot="icon-svg"] {
+ color: currentColor;
+}
+
+/* Neutral */
+[data-component="button"][data-variant="neutral"] {
+ background-color: var(--oc-bg-button-neutral);
+ color: var(--oc-text-base);
+ box-shadow: var(--oc-elevation-button-neutral);
+}
+
+[data-component="button"][data-variant="neutral"]:is(:hover, [data-state="hover"]):not(:disabled) {
+ background-image:
+ linear-gradient(90deg, var(--oc-overlay-hover) 0%, var(--oc-overlay-hover) 100%),
+ linear-gradient(90deg, var(--oc-bg-button-neutral) 0%, var(--oc-bg-button-neutral) 100%);
+}
+
+[data-component="button"][data-variant="neutral"]:is(:active, [data-state="pressed"]):not(:disabled) {
+ background-image:
+ linear-gradient(90deg, var(--oc-overlay-pressed) 0%, var(--oc-overlay-pressed) 100%),
+ linear-gradient(90deg, var(--oc-bg-button-neutral) 0%, var(--oc-bg-button-neutral) 100%);
+}
+
+[data-component="button"][data-variant="neutral"]:is(:disabled, [data-state="disabled"]) {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Danger */
+[data-component="button"][data-variant="danger"] {
+ background-color: var(--oc-bg-button-neutral);
+ color: var(--oc-state-fg-danger);
+ box-shadow: var(--oc-elevation-button-neutral);
+}
+
+[data-component="button"][data-variant="danger"]:is(:hover, [data-state="hover"]):not(:disabled) {
+ background-image:
+ linear-gradient(90deg, var(--oc-overlay-hover) 0%, var(--oc-overlay-hover) 100%),
+ linear-gradient(90deg, var(--oc-bg-button-neutral) 0%, var(--oc-bg-button-neutral) 100%);
+}
+
+[data-component="button"][data-variant="danger"]:is(:active, [data-state="pressed"]):not(:disabled) {
+ background-image:
+ linear-gradient(90deg, var(--oc-overlay-pressed) 0%, var(--oc-overlay-pressed) 100%),
+ linear-gradient(90deg, var(--oc-bg-button-neutral) 0%, var(--oc-bg-button-neutral) 100%);
+}
+
+[data-component="button"][data-variant="danger"]:is(:disabled, [data-state="disabled"]) {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+[data-component="button"][data-variant="warning"] {
+ background-color: var(--oc-bg-button-neutral);
+ color: var(--oc-state-fg-warning);
+ box-shadow: var(--oc-elevation-button-neutral);
+}
+
+[data-component="button"][data-variant="warning"]:is(:hover, [data-state="hover"]):not(:disabled) {
+ background-image:
+ linear-gradient(90deg, var(--oc-overlay-hover) 0%, var(--oc-overlay-hover) 100%),
+ linear-gradient(90deg, var(--oc-bg-button-neutral) 0%, var(--oc-bg-button-neutral) 100%);
+}
+
+[data-component="button"][data-variant="warning"]:is(:active, [data-state="pressed"]):not(:disabled) {
+ background-image:
+ linear-gradient(90deg, var(--oc-overlay-pressed) 0%, var(--oc-overlay-pressed) 100%),
+ linear-gradient(90deg, var(--oc-bg-button-neutral) 0%, var(--oc-bg-button-neutral) 100%);
+}
+
+[data-component="button"][data-variant="warning"]:is(:disabled, [data-state="disabled"]) {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Outline */
+[data-component="button"][data-variant="outline"] {
+ background-color: transparent;
+ color: var(--oc-text-base);
+ box-shadow: inset 0 0 0 1px var(--oc-border-muted);
+}
+
+[data-component="button"][data-variant="outline"]:is(:hover, [data-state="hover"]):not(:disabled) {
+ background-color: var(--oc-overlay-hover);
+}
+
+[data-component="button"][data-variant="outline"]:is(:active, [data-state="pressed"]):not(:disabled) {
+ background-color: var(--oc-overlay-pressed);
+}
+
+[data-component="button"][data-variant="outline"]:is(:disabled, [data-state="disabled"]) {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Contrast */
+[data-component="button"][data-variant="contrast"] {
+ background-image:
+ linear-gradient(180deg, var(--oc-alpha-light-20) 0%, var(--oc-alpha-light-0) 100%),
+ linear-gradient(90deg, var(--oc-bg-contrast) 0%, var(--oc-bg-contrast) 100%);
+ color: var(--oc-text-contrast);
+ text-shadow: 0 0.5px 0.5px rgba(0, 0, 0, 0.3);
+ box-shadow: var(--oc-elevation-button-contrast);
+}
+
+[data-component="button"][data-variant="contrast"]:is(:hover, [data-state="hover"]):not(:disabled) {
+ background-image:
+ linear-gradient(90deg, var(--oc-overlay-contrast-hover) 0%, var(--oc-overlay-contrast-hover) 100%),
+ linear-gradient(180deg, var(--oc-alpha-light-20) 0%, var(--oc-alpha-light-0) 100%),
+ linear-gradient(90deg, var(--oc-bg-contrast) 0%, var(--oc-bg-contrast) 100%);
+}
+
+[data-component="button"][data-variant="contrast"]:is(:active, [data-state="pressed"]):not(:disabled) {
+ background-image:
+ linear-gradient(90deg, var(--oc-overlay-contrast-pressed) 0%, var(--oc-overlay-contrast-pressed) 100%),
+ linear-gradient(180deg, var(--oc-alpha-light-20) 0%, var(--oc-alpha-light-0) 100%),
+ linear-gradient(90deg, var(--oc-bg-contrast) 0%, var(--oc-bg-contrast) 100%);
+}
+
+[data-component="button"][data-variant="contrast"]:is(:disabled, [data-state="disabled"]) {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+/* Loading */
+[data-component="button"][data-variant="loading"] {
+ background: #f2f2f2;
+ color: var(--oc-text-base);
+ box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.08);
+ cursor: default;
+ pointer-events: none;
+}
+
+/* Ghost */
+[data-component="button"][data-variant="ghost"] {
+ background-color: transparent;
+ color: var(--oc-text-base);
+}
+
+[data-component="button"][data-variant="ghost"]:is(:hover, [data-state="hover"]):not(:disabled):not([data-expanded]) {
+ background-color: var(--oc-overlay-hover);
+}
+
+[data-component="button"][data-variant="ghost"]:is(:active, [data-state="pressed"]):not(:disabled) {
+ background-color: var(--oc-overlay-pressed);
+}
+
+[data-component="button"][data-variant="ghost"]:where([data-expanded]):not(:disabled) {
+ background-color: var(--oc-overlay-pressed);
+}
+
+[data-component="button"][data-variant="ghost"]:is(:disabled, [data-state="disabled"]) {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Ghost muted */
+[data-component="button"][data-variant="ghost-muted"] {
+ background-color: transparent;
+ color: var(--oc-text-muted);
+}
+
+[data-component="button"][data-variant="ghost-muted"] [data-slot="icon-svg"] {
+ color: var(--oc-icon-muted);
+}
+
+[data-component="button"][data-variant="ghost-muted"]:is(:hover, [data-state="hover"]):not(:disabled):not(
+ [data-expanded]
+ ) {
+ background-color: var(--oc-overlay-hover);
+}
+
+[data-component="button"][data-variant="ghost-muted"]:is(:active, [data-state="pressed"]):not(:disabled) {
+ background-color: var(--oc-overlay-pressed);
+}
+
+[data-component="button"][data-variant="ghost-muted"]:where([data-expanded]):not(:disabled) {
+ background-color: var(--oc-overlay-pressed);
+ color: var(--oc-text-base);
+}
+
+[data-component="button"][data-variant="ghost-muted"]:where([data-expanded]):not(:disabled) [data-slot="icon-svg"] {
+ color: var(--oc-icon-base);
+}
+
+[data-component="button"][data-variant="ghost-muted"]:is(:disabled, [data-state="disabled"]) {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
diff --git a/packages/css/components/check.css b/packages/css/components/check.css
new file mode 100644
index 0000000000..836f99f90f
--- /dev/null
+++ b/packages/css/components/check.css
@@ -0,0 +1,20 @@
+[data-component="check"] {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--oc-text-base);
+ font-size: 13px;
+ user-select: none;
+}
+
+[data-component="check"] :is(input[type="checkbox"], input[type="radio"]) {
+ width: 16px;
+ height: 16px;
+ margin: 0;
+ accent-color: var(--oc-bg-accent);
+}
+
+[data-component="check"]:has(input:disabled) {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
diff --git a/packages/css/components/divider.css b/packages/css/components/divider.css
new file mode 100644
index 0000000000..d64746cce5
--- /dev/null
+++ b/packages/css/components/divider.css
@@ -0,0 +1,11 @@
+[data-component="divider"] {
+ box-sizing: border-box;
+ width: 100%;
+ height: 1px;
+ transform: scaleY(0.5);
+ border: none;
+ background: var(--oc-border-strong);
+ flex: none;
+ align-self: stretch;
+ flex-grow: 0;
+}
diff --git a/packages/css/components/field.css b/packages/css/components/field.css
new file mode 100644
index 0000000000..366f3ebe55
--- /dev/null
+++ b/packages/css/components/field.css
@@ -0,0 +1,24 @@
+[data-component="field"] {
+ display: grid;
+ gap: 6px;
+ min-width: 0;
+}
+
+[data-component="field"] [data-slot="field-label"] {
+ color: var(--oc-text-base);
+ font-size: 13px;
+ font-weight: 530;
+ line-height: 1.25;
+}
+
+[data-component="field"] [data-slot="field-description"] {
+ margin: 0;
+ color: var(--oc-text-muted);
+ font-size: 11px;
+}
+
+[data-component="field"] [data-slot="field-error"] {
+ margin: 0;
+ color: var(--oc-state-fg-danger);
+ font-size: 11px;
+}
diff --git a/packages/css/components/input.css b/packages/css/components/input.css
new file mode 100644
index 0000000000..e453289ffe
--- /dev/null
+++ b/packages/css/components/input.css
@@ -0,0 +1,55 @@
+[data-component="input"],
+[data-component="select"],
+[data-component="textarea"] {
+ width: 100%;
+ min-width: 0;
+ border: 0;
+ border-radius: 6px;
+ outline: 1px solid transparent;
+ background: linear-gradient(180deg, var(--oc-alpha-light-2) 0%, var(--oc-alpha-light-0) 100%), var(--oc-bg-base);
+ box-shadow: var(--oc-elevation-button-neutral);
+ color: var(--oc-text-base);
+ font: inherit;
+ font-size: 13px;
+ transition:
+ outline-color 85ms ease-out,
+ background 85ms ease-out;
+}
+
+[data-component="input"],
+[data-component="select"] {
+ height: 32px;
+ padding: 0 10px;
+}
+
+[data-component="textarea"] {
+ min-height: 88px;
+ padding: 8px 10px;
+ line-height: 1.5;
+ resize: vertical;
+}
+
+:is([data-component="input"], [data-component="select"], [data-component="textarea"])::placeholder {
+ color: var(--oc-text-faint);
+}
+
+:is([data-component="input"], [data-component="select"], [data-component="textarea"]):hover:not(:disabled) {
+ background: linear-gradient(0deg, var(--oc-overlay-hover), var(--oc-overlay-hover)), var(--oc-bg-base);
+}
+
+:is([data-component="input"], [data-component="select"], [data-component="textarea"]):focus-visible {
+ outline: 2px solid var(--oc-border-focus);
+ outline-offset: 1px;
+}
+
+:is([data-component="input"], [data-component="select"], [data-component="textarea"]):is(:disabled, [data-disabled]) {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+:is([data-component="input"], [data-component="select"], [data-component="textarea"]):is(
+ [aria-invalid="true"],
+ [data-invalid]
+) {
+ outline-color: var(--oc-state-fg-danger);
+}
diff --git a/packages/css/components/page.css b/packages/css/components/page.css
new file mode 100644
index 0000000000..4dc2984373
--- /dev/null
+++ b/packages/css/components/page.css
@@ -0,0 +1,64 @@
+[data-component="page"] {
+ width: min(100% - 32px, var(--oc-page-max-width, 1200px));
+ margin-inline: auto;
+ padding-block: var(--oc-page-padding-block, 56px);
+}
+
+[data-component="page"] [data-slot="page-header"] {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 24px;
+ margin-bottom: 32px;
+}
+
+[data-component="page"] [data-slot="page-heading"] {
+ min-width: 0;
+}
+
+[data-component="page"] [data-slot="page-eyebrow"] {
+ margin: 0 0 4px;
+ color: var(--oc-text-muted);
+ font-size: 13px;
+}
+
+[data-component="page"] [data-slot="page-title"] {
+ margin: 0;
+ color: var(--oc-text-base);
+ font-size: clamp(36px, 6vw, 64px);
+ font-weight: 440;
+ line-height: 0.95;
+ letter-spacing: -0.045em;
+}
+
+[data-component="page"] [data-slot="page-description"] {
+ max-width: 64ch;
+ margin: 12px 0 0;
+ color: var(--oc-text-muted);
+}
+
+[data-component="page"] [data-slot="page-actions"] {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+ flex: none;
+}
+
+[data-component="page"] [data-slot="page-content"] {
+ display: grid;
+ gap: 16px;
+}
+
+@media (max-width: 640px) {
+ [data-component="page"] {
+ width: min(100% - 24px, var(--oc-page-max-width, 1200px));
+ padding-block: 32px;
+ }
+
+ [data-component="page"] [data-slot="page-header"] {
+ align-items: flex-start;
+ flex-direction: column;
+ margin-bottom: 24px;
+ }
+}
diff --git a/packages/css/components/surface.css b/packages/css/components/surface.css
new file mode 100644
index 0000000000..1f443ad526
--- /dev/null
+++ b/packages/css/components/surface.css
@@ -0,0 +1,38 @@
+[data-component="surface"] {
+ overflow: hidden;
+ border: 1px solid var(--oc-border-muted);
+ border-radius: 12px;
+ background: var(--oc-bg-layer-01);
+ box-shadow: var(--oc-elevation-raised);
+}
+
+[data-component="surface"][data-variant="flat"] {
+ box-shadow: none;
+}
+
+[data-component="surface"] [data-slot="surface-header"] {
+ padding: 24px 24px 16px;
+}
+
+[data-component="surface"] [data-slot="surface-title"] {
+ margin: 0;
+ color: var(--oc-text-base);
+ font-size: 16px;
+ font-weight: 530;
+ line-height: 1.25;
+ letter-spacing: -0.01em;
+}
+
+[data-component="surface"] [data-slot="surface-description"] {
+ margin: 6px 0 0;
+ color: var(--oc-text-muted);
+ font-size: 13px;
+}
+
+[data-component="surface"] [data-slot="surface-content"] {
+ padding: 0 24px 24px;
+}
+
+[data-component="surface"][data-padding="none"] [data-slot="surface-content"] {
+ padding: 0;
+}
diff --git a/packages/css/components/table.css b/packages/css/components/table.css
new file mode 100644
index 0000000000..b8031611c3
--- /dev/null
+++ b/packages/css/components/table.css
@@ -0,0 +1,52 @@
+[data-component="table-container"] {
+ width: 100%;
+ overflow-x: auto;
+}
+
+[data-component="table"] {
+ width: 100%;
+ border-collapse: collapse;
+ color: var(--oc-text-base);
+ font-size: 13px;
+ font-variant-numeric: tabular-nums;
+}
+
+[data-component="table"] :is(th, td) {
+ height: 48px;
+ padding: 0 16px;
+ border-bottom: 1px solid var(--oc-border-muted);
+ text-align: left;
+ vertical-align: middle;
+ white-space: nowrap;
+}
+
+[data-component="table"] th {
+ height: 40px;
+ color: var(--oc-text-muted);
+ font-size: 11px;
+ font-weight: 530;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+[data-component="table"] tbody tr:last-child td {
+ border-bottom: 0;
+}
+
+[data-component="table"] tbody tr:is(:hover, [data-state="hover"]) td {
+ background: var(--oc-overlay-hover);
+}
+
+[data-component="table"] [data-align="end"] {
+ text-align: right;
+}
+
+[data-component="table"] [data-numeric] {
+ font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
+}
+
+[data-component="table"] [data-slot="table-actions"] {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
diff --git a/packages/css/examples/internal-tool.html b/packages/css/examples/internal-tool.html
new file mode 100644
index 0000000000..974b3e034d
--- /dev/null
+++ b/packages/css/examples/internal-tool.html
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+ OpenCode CSS internal tool
+
+
+
+
+
+
+
+
Release control
+
Artifacts
+
+ dax@anoma.ly
+
+
+
+
+
+
+
+
+
+ Channel
+ Name
+ Distribution
+ Version
+ Created
+ Status
+
+
+
+
+
+ next
+ cli
+ npm
+ 0.0.0-next-16020
+ 2026-07-22T13:23:42.642Z
+ Active
+
+
+
+ next
+ cli
+ npm
+ 0.0.0-next-16019
+ 2026-07-22T10:33:24.519Z
+ Inactive
+
+ Activate
+
+
+
+ next
+ cli
+ npm
+ 0.0.0-next-16018
+ 2026-07-22T10:03:19.411Z
+ Inactive
+
+ Activate
+
+
+
+ next
+ cli
+ npm
+ 0.0.0-next-16017
+ 2026-07-22T09:28:50.782Z
+ Inactive
+
+ Activate
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/css/index.css b/packages/css/index.css
new file mode 100644
index 0000000000..3a759bd79d
--- /dev/null
+++ b/packages/css/index.css
@@ -0,0 +1,3 @@
+@import "./tokens.css";
+@import "./base.css";
+@import "./components.css";
diff --git a/packages/css/package.json b/packages/css/package.json
new file mode 100644
index 0000000000..b93b1ff27f
--- /dev/null
+++ b/packages/css/package.json
@@ -0,0 +1,45 @@
+{
+ "$schema": "https://json.schemastore.org/package.json",
+ "name": "@opencode-ai/css",
+ "version": "1.18.4",
+ "description": "OpenCode's framework-agnostic CSS design system",
+ "type": "module",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/anomalyco/opencode.git",
+ "directory": "packages/css"
+ },
+ "homepage": "https://github.com/anomalyco/opencode/tree/v2/packages/css",
+ "bugs": "https://github.com/anomalyco/opencode/issues",
+ "keywords": [
+ "css",
+ "design-system",
+ "opencode"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "style": "./index.css",
+ "sideEffects": [
+ "**/*.css"
+ ],
+ "files": [
+ "*.css",
+ "components",
+ "README.md",
+ "LICENSE"
+ ],
+ "exports": {
+ ".": "./index.css",
+ "./index.css": "./index.css",
+ "./base.css": "./base.css",
+ "./tokens.css": "./tokens.css",
+ "./components.css": "./components.css",
+ "./components/*.css": "./components/*.css",
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "test": "bun test"
+ }
+}
diff --git a/packages/css/script/publish.ts b/packages/css/script/publish.ts
new file mode 100644
index 0000000000..e405780363
--- /dev/null
+++ b/packages/css/script/publish.ts
@@ -0,0 +1,24 @@
+#!/usr/bin/env bun
+
+import { Script } from "@opencode-ai/script"
+import { $ } from "bun"
+import { rm } from "node:fs/promises"
+import { fileURLToPath } from "node:url"
+
+process.chdir(fileURLToPath(new URL("..", import.meta.url)))
+
+const pkg = (await Bun.file("package.json").json()) as { name: string; version: string }
+const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
+
+if ((await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
+ console.log(`already published ${pkg.name}@${pkg.version}`)
+ process.exit(0)
+}
+
+try {
+ await $`bun run test`
+ await $`bun pm pack`
+ await $`npm publish ${tarball} --access public --tag ${Script.channel}`
+} finally {
+ await rm(tarball, { force: true })
+}
diff --git a/packages/css/test/package.test.ts b/packages/css/test/package.test.ts
new file mode 100644
index 0000000000..826c9c58a5
--- /dev/null
+++ b/packages/css/test/package.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, test } from "bun:test"
+import path from "node:path"
+
+const root = path.join(import.meta.dir, "..")
+const manifest = await Bun.file(path.join(root, "package.json")).json()
+
+function imports(file: string) {
+ return [...file.matchAll(/@import\s+["'](.+?)["']/g)].map((match) => match[1])
+}
+
+describe("package", () => {
+ test("all exports resolve to files", async () => {
+ const targets = Object.values(manifest.exports) as string[]
+ const concrete = targets.filter((target) => !target.includes("*"))
+ expect(await Promise.all(concrete.map((target) => Bun.file(path.join(root, target)).exists()))).toEqual(
+ concrete.map(() => true),
+ )
+ })
+
+ test("all local CSS imports resolve", async () => {
+ const files = [...new Bun.Glob("**/*.css").scanSync({ cwd: root, absolute: true })]
+ const missing = (
+ await Promise.all(
+ files.map(async (file) =>
+ Promise.all(
+ imports(await Bun.file(file).text())
+ .filter((target) => target.startsWith("."))
+ .map(async (target) =>
+ (await Bun.file(path.resolve(path.dirname(file), target)).exists()) ? undefined : target,
+ ),
+ ),
+ ),
+ )
+ )
+ .flat()
+ .filter(Boolean)
+ expect(missing).toEqual([])
+ })
+
+ test("component index includes every component once", async () => {
+ const expected = [...new Bun.Glob("components/*.css").scanSync({ cwd: root })].sort()
+ const actual = imports(await Bun.file(path.join(root, "components.css")).text())
+ .map((target) => target.replace(/^\.\//, ""))
+ .sort()
+ expect(actual).toEqual(expected)
+ })
+
+ test("public CSS does not expose v2 names", async () => {
+ const files = [...new Bun.Glob("**/*.css").scanSync({ cwd: root, absolute: true })]
+ const stale = (
+ await Promise.all(files.map(async (file) => ((await Bun.file(file).text()).includes("-v2") ? file : undefined)))
+ ).filter(Boolean)
+ expect(stale).toEqual([])
+ })
+
+ test("all referenced tokens are declared", async () => {
+ const files = [...new Bun.Glob("**/*.css").scanSync({ cwd: root, absolute: true })]
+ const css = (await Promise.all(files.map((file) => Bun.file(file).text()))).join("\n")
+ const declared = new Set([...css.matchAll(/(--[\w-]+)\s*:/g)].map((match) => match[1]))
+ const referenced = new Set([...css.matchAll(/var\((--[\w-]+)/g)].map((match) => match[1]))
+ expect([...referenced].filter((token) => !declared.has(token)).sort()).toEqual([])
+ })
+})
diff --git a/packages/css/theme.css b/packages/css/theme.css
new file mode 100644
index 0000000000..e08fdde171
--- /dev/null
+++ b/packages/css/theme.css
@@ -0,0 +1,107 @@
+@layer theme {
+ :root {
+ --oc-font-family-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
+ }
+
+ :root,
+ [data-color-scheme="light"] {
+ color-scheme: light;
+
+ --oc-bg-base: var(--oc-grey-50);
+ --oc-bg-deep: var(--oc-grey-100);
+ --oc-bg-layer-01: var(--oc-grey-100);
+ --oc-bg-layer-02: var(--oc-grey-200);
+ --oc-bg-layer-03: var(--oc-grey-300);
+ --oc-bg-contrast: var(--oc-grey-1000);
+ --oc-bg-button-neutral: var(--oc-grey-50);
+ --oc-bg-accent: var(--oc-blue-600);
+
+ --oc-text-base: var(--oc-grey-1100);
+ --oc-text-muted: var(--oc-grey-700);
+ --oc-text-faint: var(--oc-grey-600);
+ --oc-text-contrast: var(--oc-grey-50);
+ --oc-icon-base: var(--oc-grey-800);
+ --oc-icon-muted: var(--oc-grey-600);
+
+ --oc-border-muted: var(--oc-alpha-dark-8);
+ --oc-border-base: var(--oc-alpha-dark-10);
+ --oc-border-strong: var(--oc-alpha-dark-20);
+ --oc-border-focus: var(--oc-blue-500);
+
+ --oc-overlay-hover: var(--oc-alpha-dark-4);
+ --oc-overlay-pressed: var(--oc-alpha-dark-8);
+ --oc-overlay-contrast-hover: var(--oc-alpha-light-12);
+ --oc-overlay-contrast-pressed: var(--oc-alpha-light-24);
+
+ --oc-state-bg-success: var(--oc-green-100);
+ --oc-state-fg-success: var(--oc-green-800);
+ --oc-state-border-success: var(--oc-green-300);
+ --oc-state-bg-warning: var(--oc-yellow-100);
+ --oc-state-fg-warning: var(--oc-yellow-800);
+ --oc-state-border-warning: var(--oc-yellow-300);
+ --oc-state-bg-danger: var(--oc-red-100);
+ --oc-state-fg-danger: var(--oc-red-800);
+ --oc-state-border-danger: var(--oc-red-300);
+ --oc-state-bg-info: var(--oc-blue-100);
+ --oc-state-fg-info: var(--oc-blue-800);
+ --oc-state-border-info: var(--oc-blue-300);
+
+ --oc-elevation-raised:
+ 0 2px 4px var(--oc-alpha-dark-4), 0 1px 2px -1px var(--oc-alpha-dark-8), 0 0 0 0.5px var(--oc-alpha-dark-12);
+ --oc-elevation-button-neutral: 0 1px 1.5px var(--oc-alpha-dark-10), 0 0 0 0.5px var(--oc-alpha-dark-14);
+ --oc-elevation-button-contrast:
+ 0 1px 1.5px var(--oc-alpha-dark-20), 0 0 0 0.5px var(--oc-grey-800), inset 0 1px 2px var(--oc-alpha-light-14),
+ inset 0 -1px 2px var(--oc-alpha-dark-6);
+ }
+
+ [data-color-scheme="dark"] {
+ color-scheme: dark;
+
+ --oc-bg-base: var(--oc-grey-1100);
+ --oc-bg-deep: var(--oc-grey-1200);
+ --oc-bg-layer-01: var(--oc-grey-1000);
+ --oc-bg-layer-02: var(--oc-grey-900);
+ --oc-bg-layer-03: var(--oc-grey-800);
+ --oc-bg-contrast: var(--oc-grey-700);
+ --oc-bg-button-neutral: var(--oc-alpha-light-6);
+ --oc-bg-accent: var(--oc-blue-600);
+
+ --oc-text-base: var(--oc-grey-100);
+ --oc-text-muted: var(--oc-grey-500);
+ --oc-text-faint: var(--oc-grey-600);
+ --oc-text-contrast: var(--oc-grey-50);
+ --oc-icon-base: var(--oc-grey-400);
+ --oc-icon-muted: var(--oc-grey-600);
+
+ --oc-border-muted: var(--oc-alpha-light-8);
+ --oc-border-base: var(--oc-alpha-light-10);
+ --oc-border-strong: var(--oc-alpha-light-20);
+ --oc-border-focus: var(--oc-blue-500);
+
+ --oc-overlay-hover: var(--oc-alpha-light-6);
+ --oc-overlay-pressed: var(--oc-alpha-light-10);
+ --oc-overlay-contrast-hover: var(--oc-alpha-dark-24);
+ --oc-overlay-contrast-pressed: var(--oc-alpha-dark-40);
+
+ --oc-state-bg-success: var(--oc-green-1200);
+ --oc-state-fg-success: var(--oc-green-500);
+ --oc-state-border-success: var(--oc-green-900);
+ --oc-state-bg-warning: var(--oc-yellow-1200);
+ --oc-state-fg-warning: var(--oc-yellow-500);
+ --oc-state-border-warning: var(--oc-yellow-900);
+ --oc-state-bg-danger: var(--oc-red-1200);
+ --oc-state-fg-danger: var(--oc-red-500);
+ --oc-state-border-danger: var(--oc-red-900);
+ --oc-state-bg-info: var(--oc-blue-1200);
+ --oc-state-fg-info: var(--oc-blue-500);
+ --oc-state-border-info: var(--oc-blue-900);
+
+ --oc-elevation-raised:
+ 0 2px 4px var(--oc-alpha-dark-30), 0 1px 2px var(--oc-alpha-dark-30), 0 0 0 0.5px var(--oc-alpha-light-16),
+ 0 -0.5px 0 var(--oc-alpha-light-6);
+ --oc-elevation-button-neutral:
+ 0 1px 2px var(--oc-alpha-dark-40), 0 0 0 0.5px var(--oc-alpha-light-20), 0 -0.5px 0 var(--oc-alpha-light-10);
+ --oc-elevation-button-contrast:
+ 0 1px 2px var(--oc-alpha-dark-40), 0 0 0 0.5px var(--oc-alpha-light-40), 0 -0.5px 0 var(--oc-alpha-light-30);
+ }
+}
diff --git a/packages/css/tokens.css b/packages/css/tokens.css
new file mode 100644
index 0000000000..7c6ef245b4
--- /dev/null
+++ b/packages/css/tokens.css
@@ -0,0 +1,2 @@
+@import "./colors.css";
+@import "./theme.css";
diff --git a/script/publish.ts b/script/publish.ts
index 3d74fd6812..09a2d6a41d 100755
--- a/script/publish.ts
+++ b/script/publish.ts
@@ -63,6 +63,9 @@ await $`bun ./packages/plugin/script/publish.ts`
console.log("\n=== ui ===\n")
await $`bun ./packages/ui/script/publish.ts`
+console.log("\n=== css ===\n")
+await $`bun ./packages/css/script/publish.ts`
+
if (Script.release) {
await $`bun ./packages/desktop/scripts/finalize-latest-json.ts`
await $`bun ./packages/desktop/scripts/finalize-latest-yml.ts`