diff --git a/apps/desktop/src/vault/app/vault-v3/vault-list.component.spec.ts b/apps/desktop/src/vault/app/vault-v3/vault-list.component.spec.ts
new file mode 100644
index 000000000000..d4404f4ac7c6
--- /dev/null
+++ b/apps/desktop/src/vault/app/vault-v3/vault-list.component.spec.ts
@@ -0,0 +1,22 @@
+import { optionsColumnWidthClass, OWNER_COLUMN_WIDTH_CLASS } from "./vault-list.component";
+
+describe("vault list column widths", () => {
+ describe("optionsColumnWidthClass", () => {
+ it("reserves room for an icon per copyable field when quick copy actions are shown", () => {
+ // launch + 3 copy icons + overflow trigger + cell padding ≈ 209px
+ expect(optionsColumnWidthClass(true)).toBe("tw-w-56");
+ });
+
+ it("reserves room for a single combined copy button otherwise", () => {
+ // launch + 1 copy button + overflow trigger + cell padding ≈ 145px
+ expect(optionsColumnWidthClass(false)).toBe("tw-w-40");
+ });
+ });
+
+ describe("OWNER_COLUMN_WIDTH_CLASS", () => {
+ it("reserves room for the owner badge", () => {
+ // The badge is truncated to 13 characters, running to ~120px plus cell padding.
+ expect(OWNER_COLUMN_WIDTH_CLASS).toBe("tw-w-40");
+ });
+ });
+});
diff --git a/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts
index e7fab1079c2e..8ab985521282 100644
--- a/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts
+++ b/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts
@@ -36,7 +36,12 @@ import {
CheckboxModule,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
-import { NewCipherMenuComponent, VaultBatchBarService, VaultItem } from "@bitwarden/vault";
+import {
+ NewCipherMenuComponent,
+ VaultBatchBarService,
+ VaultCopyButtonsService,
+ VaultItem,
+} from "@bitwarden/vault";
import { VaultCipherRowComponent } from "./vault-items/vault-cipher-row.component";
import { VaultCollectionRowComponent } from "./vault-items/vault-collection-row.component";
@@ -45,6 +50,35 @@ import { VaultItemEvent } from "./vault-items/vault-item-event";
// Fixed manual row height required due to how cdk-virtual-scroll works
export const RowHeight = 76.5;
export const RowHeightClass = `tw-h-[76.5px]`;
+
+/**
+ * Width of the Options column, sized to the widest action strip a row can draw.
+ *
+ * The strip is icon buttons, so its width doesn't scale with the window — but a fractional column
+ * width does. Under `table-layout: fixed` a px width holds even once the rest of the table has to
+ * give, whereas a fraction keeps shrinking past what the buttons need. The cell is
+ * `whitespace-nowrap`, so once it's too narrow the strip overflows to the right rather than
+ * wrapping, and the overflow menu trigger lands past the table's edge, off screen.
+ *
+ * Budget is 40px each for the launch and overflow triggers (`bitIconButton` at its default size),
+ * 32px per quick-copy icon (`size="small"`), ~4px per collapsed whitespace gap between them, and
+ * 12px of cell padding each side. Both cases round up to the next step on the spacing scale:
+ * - quick copy — launch + 3 copy icons + trigger: `40 + 3*32 + 40 + 2*4 + 24` ≈ 209 → `w-56`.
+ * - collapsed — launch + one combined copy button + trigger: `40 + 32 + 40 + 2*4 + 24` ≈ 145 → `w-40`.
+ */
+export const optionsColumnWidthClass = (showQuickCopyActions: boolean): string =>
+ showQuickCopyActions ? "tw-w-56" : "tw-w-40";
+
+/**
+ * Width of the Owner column, sized to hold its badge.
+ *
+ * The badge is a chip truncated to 13 characters, which runs to ~120px — wider than a fraction of
+ * a narrow table leaves it. Sized as a px column for the same reason as
+ * {@link optionsColumnWidthClass}: otherwise the chip overflows into the Options column and the
+ * action buttons render on top of it.
+ */
+export const OWNER_COLUMN_WIDTH_CLASS = "tw-w-40";
+
type EmptyStateItem = {
title: string;
description: string;
@@ -102,6 +136,25 @@ export class VaultListComponent {
private batchBarService = inject>(VaultBatchBarService, {
optional: true,
});
+ private vaultCopyButtonsService = inject(VaultCopyButtonsService);
+
+ /**
+ * Whether copy actions render as an icon per copyable field rather than a single combined menu.
+ * Mirrors {@link VaultCipherRowComponent}'s own check, so the column reserves what the rows draw.
+ */
+ private readonly showQuickCopyActions = toSignal(
+ combineLatest([
+ this.configService.getFeatureFlag$(FeatureFlag.PM40435_QuickCopyIconSetting),
+ this.vaultCopyButtonsService.showQuickCopyActions$,
+ ]).pipe(map(([flagEnabled, settingEnabled]) => flagEnabled && settingEnabled)),
+ { initialValue: false },
+ );
+
+ protected readonly optionsColumnWidthClass = computed(() =>
+ optionsColumnWidthClass(this.showQuickCopyActions()),
+ );
+
+ protected readonly ownerColumnWidthClass = OWNER_COLUMN_WIDTH_CLASS;
protected readonly showBatchBar = toSignal(
combineLatest([
@@ -155,8 +208,6 @@ export class VaultListComponent {
});
}
- protected readonly showExtraColumn = computed(() => this.showOwner());
-
protected event(event: VaultItemEvent) {
this.onEvent.emit(event);
}