Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions design-system/packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ matching component slot, just as for SVG icons. These slots constrain catalog
icons to the component's size; a standalone `Icon` retains its explicit size
(24px by default). Do not shrink the catalog globally to correct a slot mismatch.

`IconButton` defaults to `quiet`: its resting surface is transparent, hover and
pressed states use shared action feedback, and keyboard focus keeps a visible
focus ring. Use it for toolbar, dialog, and row utilities. `fill` and `primary`
keep an opaque backing surface for persistent emphasis. Disabled quiet actions
remain transparent and do not show hover or pressed feedback.

The catalog uses exported vectors, including their view boxes and per-path
opacity. Theme colors remain caller-owned through `currentColor`. Asset
fingerprints are reviewed with intentional resource updates so replacing a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ComponentMeta } from "../../registry.types";

export const iconButtonMeta = {
category: "action",
description: "A labeled icon-only action with opaque quiet, filled, and primary presentation variants.",
description: "A labeled icon-only action with a transparent quiet variant and opaque filled and primary variants.",
maturity: "stable",
name: "IconButton",
props: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@layer openbitfun.components {
.button {
--_icon-button-background: var(--openbitfun-color-surface-tertiary);
--_icon-button-background: transparent;
--_icon-button-background-hover: var(--openbitfun-color-action-neutral-surface);
--_icon-button-background-active: var(--openbitfun-color-action-neutral-surface-pressed);
--_icon-button-content: var(--openbitfun-color-action-neutral-content);
Expand All @@ -19,7 +19,7 @@
border: 0;
border-radius: var(--openbitfun-radius-sm);
color: var(--_icon-button-content);
background: var(--openbitfun-color-surface-tertiary);
background: transparent;
cursor: pointer;
isolation: isolate;
transition: color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard);
Expand Down Expand Up @@ -55,6 +55,11 @@
cursor: not-allowed;
}

.button[data-openbitfun-variant="fill"],
.button[data-openbitfun-variant="primary"] {
background: var(--openbitfun-color-surface-tertiary);
}

.button[data-openbitfun-variant="fill"] {
--_icon-button-background: var(--openbitfun-color-action-neutral-surface);
--_icon-button-background-hover: var(--openbitfun-color-action-neutral-surface-hover);
Expand Down
14 changes: 11 additions & 3 deletions design-system/packages/ui/tests/icon-button.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,27 @@ test("IconButton styles consume shared action and geometry tokens", async () =>
assert.match(styles, /--openbitfun-radius-pill/);
});

test("every IconButton variant is composited over an opaque surface", async () => {
test("quiet IconButtons are transparent at rest while emphasized variants retain an opaque surface", async () => {
const styles = await readFile(
new URL("../src/components/IconButton/IconButton.module.css", import.meta.url),
"utf8",
);

assert.match(
styles,
/\.button\s*\{[^}]*background:\s*var\(--openbitfun-color-surface-tertiary\)/s,
/\.button\s*\{[^}]*--_icon-button-background:\s*transparent;[^}]*\n\s*background:\s*transparent;/s,
);
assert.match(
styles,
/\.button::before\s*\{[^}]*background:\s*var\(--_icon-button-background\)/s,
);
assert.doesNotMatch(styles, /--_icon-button-background(?:-hover|-active)?:\s*transparent/);
assert.match(
styles,
/\.button\[data-openbitfun-variant="fill"\],\s*\.button\[data-openbitfun-variant="primary"\]\s*\{[^}]*background:\s*var\(--openbitfun-color-surface-tertiary\)/s,
);
assert.doesNotMatch(styles, /--_icon-button-background-(?:hover|active):\s*transparent/);
for (const state of ["hover", "active"]) {
assert.ok(styles.includes(`:is(:${state}, [data-openbitfun-preview-state="${state}"]):not(:disabled)::before`));
}
assert.match(styles, /\.button:focus-visible\s*\{[^}]*outline:.*var\(--openbitfun-color-focus-ring\)/s);
});
Loading
Loading