CnWidgetWrapper
Container shell around a dashboard widget. Provides a header with icon and title, a scrollable content area, and an optional footer with action links. Accepts a styleConfig object for runtime style overrides (background, border, padding). Used internally by CnDashboardPage for all non-tile widgets.
Try it
Usage
<!-- Basic wrapper -->
<CnWidgetWrapper title="My Cases" icon-url="/apps/myapp/img/icon.svg">
<MyCasesChart :data="chartData" />
</CnWidgetWrapper>
<!-- Without header (borderless, flush — for self-contained card widgets) -->
<CnWidgetWrapper :show-title="false" :borderless="true">
<CnStatsBlock :stats="kpis" />
</CnWidgetWrapper>
<!-- With NC widget object (used by CnDashboardPage internally) -->
<CnWidgetWrapper
:title="widget.title"
:icon-url="widget.iconUrl"
:icon-class="widget.iconClass"
:buttons="widget.buttons">
<CnWidgetRenderer :widget="widget" />
</CnWidgetWrapper>
<!-- With custom footer and header actions -->
<CnWidgetWrapper title="Tasks">
<template #actions>
<NcButton type="tertiary" @click="refresh">Refresh</NcButton>
</template>
<TaskList :items="tasks" />
<template #footer>
<a href="/apps/tasks">View all</a>
</template>
</CnWidgetWrapper>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | String | 'Widget' | Widget title shown in the header |
showTitle | Boolean | true | Whether to render the header bar |
chrome | String | 'default' | Card chrome variant. 'default' uses the library card chrome; 'nc-dashboard' reproduces the native Nextcloud Dashboard panel exactly (translucent blurred background via --color-main-background-blur + --filter-background-blur, --border-radius-container-large corners, no border/shadow, 16px header with a 20px/700 title and 32px leading icon, content inset 16px). styleConfig overrides still layer on top. |
showActions | Boolean | true | Whether the header's overflow action menu (Refresh / Documentation / Request-a-feature + #action-items) renders. Set false for compact surfaces (e.g. a KPI tile) to drop the menu and free header width. |
borderless | Boolean | false | Remove border and background — makes the wrapper transparent |
flush | Boolean | false | Remove content padding — lets content extend edge-to-edge |
iconUrl | String | null | Image URL for the header icon |
iconClass | String | null | CSS class for the header icon (e.g. Nextcloud icon class) |
titleIconPosition | String | 'right' | Position of the title-icon slot in the header: 'left' places it before the title group; 'right' places it after the actions |
titleIconColor | String | null | CSS color value applied to the title-icon slot container (e.g. '#e74c3c') |
buttons | Array | [] | Footer button links: [{ text, link }] |
styleConfig | Object | {} | Runtime style overrides: { backgroundColor?, borderStyle?, borderWidth?, borderColor?, borderRadius?, padding?: { top, right, bottom, left }, headerStyle?: { backgroundColor?, textColor? } }. The optional headerStyle colours the header bar per-widget. |
refreshing | Boolean | false | When bound (e.g. :refreshing="loading" around the host's refetch), the Refresh item is disabled and shows a loading spinner for exactly as long as this stays true — so the spinner reflects the real refresh time. |
Slots
| Slot | Description |
|---|---|
| default | Widget content rendered in the scrollable body area |
actions | Buttons or controls placed in the right side of the header |
title-icon | Extra icon element rendered in the header at the position controlled by titleIconPosition (left of title or right of actions) |
footer | Custom footer content (replaces the buttons prop rendering) |
Built-in Actions menu
CnWidgetWrapper ships with a small overflow … menu in the header — the shared CnActionsMenu — containing up to three actions. Documentation and Request-a-feature are functional without any host wiring when the wrapper is mounted under CnAppRoot; Refresh is shown only when something will handle it (see below):
- Refresh — shown only when it will do something.
showRefreshis tri-state:true/falseforce it on/off, and the default (null) is auto — the item renders only when a parent has attached an@refreshlistener. This prevents dead buttons on widgets that can't refresh (e.g. a prop-drivenCnObjectDataWidget) and on detail-page auto-body widgets where the page owns refresh. A widget that wants a manual Refresh while refreshing itself via the bus (no@refreshlistener) can set:show-refresh="true"explicitly. When shown and clicked it emits@refresh, then (unless the host listener callsevent.preventDefault()) emits on the@nextcloud/event-buschannelcn:widget:refreshwith payload{ widgetId, title }. - Documentation — rendered only when a
documentationUrlis supplied. Opens the host-provided link in a new tab (target="_blank",rel="noopener noreferrer"); no JS handler. Apps pass the URL from the widget configuration (:documentation-url="widget.documentationUrl"); customise the wording withdocumentationLabel. - Request a feature — on by default; emits
@request-feature, then (unless suppressed) auto-mountsCnSuggestFeatureModalwithapp + page + surface=widget:<id>context auto-filled fromCnAppRootinjects. The host can override the default by binding@request-featureand callingevent.preventDefault()to handle it themselves.
Force Refresh on/off per-instance with :show-refresh="true"/:show-refresh="false" (the legacy hide-refresh alias still opts out for back-compat); opt out of Request-a-feature with :show-request-feature="false" (the legacy hide-request-feature alias also still works). When everything is hidden — Refresh auto-hidden or opted out, Request-a-feature opted out, no documentationUrl, and no #action-items slot content — the overflow menu disappears entirely. To drop the entire actions area in one go — e.g. on a compact KPI tile whose only header affordance is a date chip — set :show-actions="false".
Set :widget-id so the event-bus payload + modal surface tag are stable across renames; otherwise the wrapper falls back to a slugified title.
Opting into Refresh
Because Refresh auto-hides unless a parent attaches an @refresh listener, opting in is the act of wiring one of the patterns below. A widget that refreshes itself purely via the bus (no @refresh listener — mode 3) must also pass :show-refresh="true" so the action renders.
A widget can opt in to Refresh in one of three ways. Pick whichever fits the widget's existing reactivity model — all three are first-class.
1. Ref-callable refresh() method (canonical)
The cleanest pattern: expose a method, let the host call it through the ref.
<template>
<CnWidgetWrapper title="Outgoing calls" widget-id="outgoing-calls-daily">
<CallsChart ref="chart" />
</CnWidgetWrapper>
</template>
<script>
export default {
// Inside CallsChart.vue:
methods: {
async refresh() {
this.data = await this.$store.dispatch('callLogs/refetch')
},
},
}
</script>
The host listens via the event-bus channel and routes by id (see mode 3 below for the wiring), or the wrapper invokes the method directly when integrated.
2. Reactive refreshTrigger prop
Useful when the widget cannot easily expose a ref (e.g. async-loaded). The host increments a timestamp; the widget watches it.
<template>
<CnWidgetWrapper title="Outgoing calls" widget-id="outgoing-calls-daily" :show-refresh="true">
<CallsChart :refresh-trigger="callsRefreshTrigger" />
</CnWidgetWrapper>
</template>
<script>
import { subscribe } from '@nextcloud/event-bus'
export default {
data() {
return { callsRefreshTrigger: 0 }
},
mounted() {
subscribe('cn:widget:refresh', ({ widgetId }) => {
if (widgetId === 'outgoing-calls-daily') {
this.callsRefreshTrigger = Date.now()
}
})
},
}
</script>
3. Event-bus subscription (direct)
The widget subscribes itself; no host plumbing needed.
<script>
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
export default {
props: { widgetId: { type: String, required: true } },
mounted() {
this._onRefresh = ({ widgetId }) => {
if (widgetId === this.widgetId) this.refetch()
}
subscribe('cn:widget:refresh', this._onRefresh)
},
beforeDestroy() {
unsubscribe('cn:widget:refresh', this._onRefresh)
},
methods: {
async refetch() { /* ... */ },
},
}
</script>
Reference (auto-generated)
The tables below are generated from the SFC source via vue-docgen-cli. They reflect what's actually in CnWidgetWrapper.vue and update automatically whenever the component changes.
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | () => t('nextcloud-vue', 'Widget') | Widget title | |
showTitle | boolean | true | Whether to show the header with title | |
chrome | string | 'default' | Chrome variant for the wrapper card. - 'default' — the library's own card chrome (opaque background, 1px border, compact header). - 'nc-dashboard' — matches the native Nextcloud Dashboard panel exactly using the same design tokens: translucent blurred background (--color-main-background-blur + --filter-background-blur), --border-radius-container-large corners, no border/shadow, a 16px header with a 20px/700 title and 32px leading icon, and content inset 16px on the sides + bottom. styleConfig overrides still layer on top so a user can customise any token. | |
borderless | boolean | false | Remove border and background — makes the wrapper transparent. Useful for widgets that are self-contained cards (e.g. CnStatsBlock). | |
flush | boolean | false | Remove content padding — allows content to go edge-to-edge. Useful for list-style widgets where items should span the full width. | |
iconUrl | string | null | Icon URL (image) | |
iconClass | string | null | Icon CSS class (e.g., Nextcloud icon class) | |
titleIconPosition | string | 'right' | Position of the title-icon slot in the header. 'left' places it before the title; 'right' places it after the actions. | |
titleIconColor | string | null | Explicit CSS colour for the header icon. Overrides titleIconVariant. Must be a CSS custom property or a theme token — never a literal hex (NL Design System: the nldesign app re-themes by overriding the Nextcloud variables, and a hardcoded colour ignores that). | |
titleIconVariant | string | 'primary' | Semantic colour for the header icon. Every widget's icon is coloured — primary (the theme colour) is the default, and a widget whose subject already carries a semantic meaning names it here so the icon says so at a glance (a Depublished list is error, a Concepts list warning, a Published list success). | |
buttons | array | [] | Footer action buttons: [{ text, link }] | |
showActions | boolean | true | Whether the header's overflow action menu (Refresh / Documentation / Request-a-feature + any #action-items) renders. Shown by default; set false for compact surfaces — e.g. a KPI tile whose only header affordance is a date chip — to drop the menu and free header width. | |
styleConfig | { backgroundColor: string, borderStyle: string, borderWidth: number, borderColor: string, borderRadius: number, padding: { top: number, right: number, bottom: number, left: number } } | \{\} | Style configuration for the wrapper. | |
hideRefresh | boolean | false | Hide the built-in Refresh item from the overflow action menu. The Refresh item is shown by default — set this when the widget has no refreshable data source (e.g. a static tile). Alias for the inverse :show-refresh="false"; either form opts out. | |
hideRequestFeature | boolean | false | Hide the built-in Request-a-feature item from the overflow action menu. Shown by default; set when the consuming app has no public issue tracker to link out to. Alias for the inverse :show-request-feature="false"; either form opts out. | |
showRefresh | union | null | Whether to show the built-in Refresh item. Tri-state: - true / false — force the action on or off. - null (the default) — auto: show the action only when a parent has attached an @refresh listener (i.e. something will actually handle the refresh). This keeps widgets that can't refresh — e.g. a prop-driven CnObjectDataWidget — from showing a dead button. Widgets that refresh themselves via the cn:widget:refresh event bus (with no @refresh listener) should set :show-refresh="true" explicitly. hideRefresh (or :show-refresh="false") always wins. | |
showRequestFeature | boolean | true | Inverse of hideRequestFeature. Defaults to true so the action renders. Set :show-request-feature="false" to hide it. Either flag hides the action. | |
showReportBug | boolean | true | Whether the built-in "Report a bug" item renders. On by default — the trio Request a feature / Report a bug / Documentation is the contract for every widget; this exists for the rare surface that must suppress one deliberately. | |
showDocumentation | boolean | true | Whether the built-in "Documentation" item renders. On by default, for the same reason as showReportBug. The item's target is resolved by the shared menu (see docsAnchor), so leaving it on costs the host nothing. | |
documentationUrl | string | '' | Explicit documentation link for this widget, opened in a new tab. Usually unnecessary: leave it empty and set docsAnchor instead, so the link is built from the app-wide documentation base and lands on this widget's own section. | |
documentationLabel | string | () => t('nextcloud-vue', 'Documentation') | Optional pre-translated label for the Documentation action. Defaults to the lib's translation of "Documentation". | |
widgetId | string | '' | Widget id for the built-in default Refresh / Request-a-feature handlers (B2). Forwarded as the surface: "widget:<id>" value on the auto-mounted CnSuggestFeatureModal AND as the widgetId field on the cn:widget:refresh event-bus payload. When unset, the wrapper falls back to a slugified displayTitle, which works but is less stable than an explicit id. | |
docsAnchor | string | '' | This widget's own section in the app's documentation, appended to the app-wide documentation base URL (provided by CnAppRoot) to build the Actions menu's Documentation deep-link. A bare slug becomes a #fragment. Supply it per widget type — without it the item still renders, but lands on the docs homepage rather than this widget. | |
reportBugUrl | string | '' | Explicit "Report a bug" target for the Actions menu. Empty (the default) builds a new-issue deep-link on the app's own forge. | |
specRef | string | '' | Optional specRef forwarded to the auto-mounted CnSuggestFeatureModal so the resulting GitHub issue links to the spec capability this widget belongs to. | |
refreshing | boolean | false | Whether a refresh is currently in flight. When bound by the host (e.g. :refreshing="loading" around its refetch), the Refresh item is disabled and shows a loading spinner for exactly as long as this stays true — so the spinner reflects the real refresh time. | |
refreshLabel | string | () => t('nextcloud-vue', 'Refresh') | Optional pre-translated label for the Refresh action. Defaults to the lib's translation of "Refresh" so callers usually don't need to set this. | |
requestFeatureLabel | string | () => t('nextcloud-vue', 'Request a feature') | Optional pre-translated label for the Request-a-feature action. Defaults to the lib's translation of "Request a feature". | |
actionsMenuLabel | string | () => t('nextcloud-vue', 'Actions') | Pre-translated aria-label / tooltip for the overflow menu trigger. Defaults to "Actions". | |
translate | ((key: string) => string)|null | null | Translate function. Falls back to the injected cnTranslate, which itself defaults to an identity function. |
Events
| Name | Payload | Description |
|---|---|---|
refresh | undefined | User clicked the Refresh item in the overflow action menu. Payload: { widgetId, title }. Handlers may call the second arg's preventDefault() to suppress the built-in default (event-bus emit on cn:widget:refresh). |
request-feature | undefined | User clicked the Request a feature item. Payload: { widgetId, title }. Handlers may call the second arg's preventDefault() to suppress the built-in default (auto-opening CnSuggestFeatureModal). |
Slots
| Name | Bindings | Description |
|---|---|---|
title-icon | — | |
title-meta | — | |
actions | — | actions Custom action buttons rendered before the |
action-items | — | |
default | — | |
footer | — |
Additional props & slots
| Prop | Type | Default | Description |
|---|---|---|---|
specRef | String | '' | Forwarded to the auto-mounted CnSuggestFeatureModal so the resulting issue links to the widget's spec capability. |
documentationUrl | String | '' | When set, renders a Documentation item in the overflow menu that opens this link in a new tab. Empty hides it. |
documentationLabel | String | t('Documentation') | Pre-translated label for the Documentation action. |
refreshLabel | String | t('Refresh') | Pre-translated label for the Refresh action. |
requestFeatureLabel | String | t('Request a feature') | Pre-translated label for the Request-a-feature action. |
actionsMenuLabel | String | t('Actions') | Pre-translated aria-label / tooltip for the overflow … menu trigger. |
| Slot | Description |
|---|---|
title-meta | Inline content rendered next to the title (e.g. the dashboard date-range chip). |
Coloured header icon
Every widget's header icon is coloured — titleIconVariant defaults to primary, so no configuration is needed. A widget whose subject already carries a meaning names it instead: a Concepts list is warning, a Published list success, a Depublished list error. titleIconColor overrides the variant with an explicit CSS variable (never a literal hex — that would ignore the nldesign app's re-theming).
The colour is published once as --cn-widget-icon-color on the header and cascades to every icon under it.
Actions menu props
| Prop | Type | Default | Description |
|---|---|---|---|
docsAnchor | String | '' | This widget's own section in the app's documentation, appended to the app-wide base so the Documentation entry deep-links to this widget rather than the docs homepage. Prefer it over documentationUrl. |
showDocumentation | Boolean | true | Whether the Documentation entry renders. |
showReportBug | Boolean | true | Whether the Report-a-bug entry renders. |
reportBugUrl | String | '' | Explicit bug-report target; empty builds a new-issue deep-link on the app's own forge. |
titleIconVariant | String | 'primary' | Semantic header-icon colour (primary/success/warning/error/info/neutral). |
Request a feature / Report a bug / Documentation render on every widget.