diff --git a/client/src/components/boards/BoardSettingsModal/PreferencesPane/Others.jsx b/client/src/components/boards/BoardSettingsModal/PreferencesPane/Others.jsx
index fa7506e7..40465833 100644
--- a/client/src/components/boards/BoardSettingsModal/PreferencesPane/Others.jsx
+++ b/client/src/components/boards/BoardSettingsModal/PreferencesPane/Others.jsx
@@ -43,6 +43,15 @@ const Others = React.memo(() => {
className={styles.radio}
onChange={handleChange}
/>
+
+
{t('common.showCardCounterHint')}
{
[],
);
+ const selectCardCountByListId = useMemo(() => selectors.makeSelectCardCountByListId(), []);
+
const clipboard = useSelector(selectors.selectClipboard);
const isFavoritesActive = useSelector(selectors.selectIsFavoritesActiveForCurrentUser);
const list = useSelector((state) => selectListById(state, id));
const cardIds = useSelector((state) => selectFilteredCardIdsByListId(state, id));
+ // Shown on the add-card button when the board asks for it. Counted over every
+ // card the list holds, not the ones the filter leaves standing: a filter
+ // hides cards, it does not remove them, and a number that fell as the filter
+ // narrowed would say the list had emptied.
+ const showCardCounter = useSelector(
+ (state) => selectors.selectCurrentBoard(state).showCardCounter,
+ );
+
+ const cardCount = useSelector((state) => selectCardCountByListId(state, id));
+
const { canEdit, canArchiveCards, canAddCard, canPasteCard, canDropCard } = useSelector(
(state) => {
const isEditModeEnabled = selectors.selectIsEditModeEnabled(state); // TODO: move out?
@@ -148,6 +160,10 @@ const List = React.memo(({ id, index }) => {
cardsWrapperRef.current.scrollTop = cardsWrapperRef.current.scrollHeight;
}, [scrollBottomState]);
+ const cardCountNode = showCardCounter && cardCount !== null && (
+ {cardCount}
+ );
+
const ActionsPopup = usePopup(ActionsStep);
const ArchiveCardsPopup = usePopup(ArchiveCardsStep);
@@ -274,6 +290,7 @@ const List = React.memo(({ id, index }) => {
)}
onClick={handleAddCardClick}
>
+ {cardCountNode}
{cardIds.length > 0 ? t('action.addAnotherCard') : t('action.addCard')}
diff --git a/client/src/components/lists/List/List.module.scss b/client/src/components/lists/List/List.module.scss
index d4a4b767..5fb488a5 100644
--- a/client/src/components/lists/List/List.module.scss
+++ b/client/src/components/lists/List/List.module.scss
@@ -33,6 +33,24 @@
}
}
+ .addCardButtonCount {
+ background: #17394d;
+ border-radius: 10px;
+ color: #dfe3e6;
+ display: inline-block;
+ font-size: 11px;
+ font-weight: 700;
+ // Same 20px line box as the label beside it, so the two are centred on
+ // each other by construction rather than by a nudge that only holds at one
+ // font size.
+ line-height: 20px;
+ margin-right: 6px;
+ min-width: 20px;
+ padding: 0 6px;
+ text-align: center;
+ vertical-align: top;
+ }
+
.addCardButtonIcon {
height: 20px;
padding: 0.64px;
diff --git a/client/src/locales/de-DE/core.js b/client/src/locales/de-DE/core.js
index 9b5498c6..a92b8e08 100644
--- a/client/src/locales/de-DE/core.js
+++ b/client/src/locales/de-DE/core.js
@@ -398,6 +398,9 @@ export default {
settings: 'Einstellungen',
shared: 'Geteilt',
sharedWithMe_title: 'Mit mir geteilt',
+ showCardCounter: 'Kartenzähler in Listen anzeigen',
+ showCardCounterHint:
+ 'Zeigt die Anzahl der Karten einer Liste auf ihrer Schaltfläche zum Hinzufügen einer Karte an. Gezählt werden alle Karten der Liste, nicht nur die von einem Filter übrig gelassenen.',
showOnFrontOfCard: 'Auf der Vorderseite der Karte anzeigen',
smtp: 'SMTP',
sortList_title: 'Liste sortieren',
diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js
index 09433d58..709ba1ce 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -375,6 +375,9 @@ export default {
settings: 'Settings',
shared: 'Shared',
sharedWithMe_title: 'Shared With Me',
+ showCardCounter: 'Show card counter in lists',
+ showCardCounterHint:
+ 'Adds the number of cards a list holds to its add-card button. The count covers every card in the list, not only the ones a filter leaves visible.',
showOnFrontOfCard: 'Show on front of card',
smtp: 'SMTP',
sortList_title: 'Sort List',
diff --git a/client/src/models/Board.js b/client/src/models/Board.js
index 0acea83b..0bb55b5e 100755
--- a/client/src/models/Board.js
+++ b/client/src/models/Board.js
@@ -36,6 +36,7 @@ export default class extends BaseModel {
defaultCardType: attr(),
limitCardTypesToDefaultOne: attr(),
alwaysDisplayCardCreator: attr(),
+ showCardCounter: attr(),
displayCardAges: attr(),
expandTaskListsByDefault: attr(),
context: attr(),
diff --git a/client/src/selectors/lists.js b/client/src/selectors/lists.js
index 098259bf..acd605cd 100644
--- a/client/src/selectors/lists.js
+++ b/client/src/selectors/lists.js
@@ -65,6 +65,27 @@ export const makeSelectFilteredCardIdsByListId = () =>
export const selectFilteredCardIdsByListId = makeSelectFilteredCardIdsByListId();
+// How many cards the list holds.
+//
+// Unfiltered on purpose: a filter hides cards, it does not remove them, and a
+// count that fell as the filter narrowed would say the list had emptied.
+export const makeSelectCardCountByListId = () =>
+ createSelector(
+ orm,
+ (_, id) => id,
+ ({ List }, id) => {
+ const listModel = List.withId(id);
+
+ if (!listModel) {
+ return null;
+ }
+
+ return listModel.getCardsModelArray().length;
+ },
+ );
+
+export const selectCardCountByListId = makeSelectCardCountByListId();
+
export const selectIsListWithIdAvailableForCurrentUser = createSelector(
orm,
(_, id) => id,
@@ -171,6 +192,8 @@ export default {
selectCardIdsByListId,
makeSelectFilteredCardIdsByListId,
selectFilteredCardIdsByListId,
+ makeSelectCardCountByListId,
+ selectCardCountByListId,
selectIsListWithIdAvailableForCurrentUser,
selectCurrentListId,
selectCurrentList,
diff --git a/server/api/controllers/boards/update.js b/server/api/controllers/boards/update.js
index ab0a5176..8744f304 100755
--- a/server/api/controllers/boards/update.js
+++ b/server/api/controllers/boards/update.js
@@ -55,6 +55,10 @@
* type: boolean
* description: Whether to always display card creators
* example: false
+ * showCardCounter:
+ * type: boolean
+ * description: Whether a list's add-card button shows how many cards it holds
+ * example: false
* displayCardAges:
* type: boolean
* description: Whether to display card ages
@@ -124,6 +128,9 @@ module.exports = {
alwaysDisplayCardCreator: {
type: 'boolean',
},
+ showCardCounter: {
+ type: 'boolean',
+ },
displayCardAges: {
type: 'boolean',
},
@@ -167,6 +174,7 @@ module.exports = {
'defaultCardType',
'limitCardTypesToDefaultOne',
'alwaysDisplayCardCreator',
+ 'showCardCounter',
'displayCardAges',
'expandTaskListsByDefault',
);
@@ -186,6 +194,7 @@ module.exports = {
'defaultCardType',
'limitCardTypesToDefaultOne',
'alwaysDisplayCardCreator',
+ 'showCardCounter',
'displayCardAges',
'expandTaskListsByDefault',
'isSubscribed',
diff --git a/server/api/models/Board.js b/server/api/models/Board.js
index 1d118162..9245ef16 100755
--- a/server/api/models/Board.js
+++ b/server/api/models/Board.js
@@ -25,6 +25,7 @@
* - defaultCardType
* - limitCardTypesToDefaultOne
* - alwaysDisplayCardCreator
+ * - showCardCounter
* - displayCardAges
* - expandTaskListsByDefault
* - createdAt
@@ -68,6 +69,11 @@
* default: false
* description: Whether to always display the card creator
* example: false
+ * showCardCounter:
+ * type: boolean
+ * default: false
+ * description: Whether a list's add-card button shows how many cards it holds
+ * example: false
* displayCardAges:
* type: boolean
* default: false
@@ -143,6 +149,12 @@ module.exports = {
defaultsTo: false,
columnName: 'always_display_card_creator',
},
+ // Whether a list's add-card button carries the number of cards it holds.
+ showCardCounter: {
+ type: 'boolean',
+ defaultsTo: false,
+ columnName: 'show_card_counter',
+ },
displayCardAges: {
type: 'boolean',
defaultsTo: false,
diff --git a/server/db/migrations/20260918000000_add_show_card_counter_to_boards.js b/server/db/migrations/20260918000000_add_show_card_counter_to_boards.js
new file mode 100644
index 00000000..5abc8570
--- /dev/null
+++ b/server/db/migrations/20260918000000_add_show_card_counter_to_boards.js
@@ -0,0 +1,19 @@
+/*!
+ * Copyright (c) 2026 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+// Whether a list says how many cards it holds, on its add-card button. Off by
+// default: a count is useful to a board that is being kept to a size and noise
+// to one that is not.
+module.exports.up = async (knex) => {
+ await knex.schema.alterTable('board', (table) => {
+ table.boolean('show_card_counter').notNullable().defaultTo(false);
+ });
+};
+
+module.exports.down = async (knex) => {
+ await knex.schema.alterTable('board', (table) => {
+ table.dropColumn('show_card_counter');
+ });
+};