Interface

Widget

Lets modules add cards to a dashboard and tabs to a detail page that your app or another module owns.

Widget lets one module put a component on a page that another module owns. Subscriptions adds a membership card to the account overview, and Events adds cards to the admin dashboard, without either page knowing them. Use it for dashboards and tabbed detail pages that other modules should fill. When only the page itself decides its sections, use PageTabs instead.

The package is @frigear-nu/widget. A page names a widget area, such as admin.dashboard, and draws whatever is registered for it. Modules register Vue components to that area at build time. Like Menu, it is a registry.

What you get

  • Components (on by default): <WidgetArea> draws an area's widgets in a grid, and <WidgetTabs> draws its tabs.
  • Composables and helpers (on by default): useWidgets and useTabs read an area, and extendWidgets and extendTabs add to one from a module.
  • Hooks (on by default): widgets:filter and tabs:filter hide entries the current visitor should not see, by the entry's permission.

A widget's component loads only when its area is drawn.

ServerClientMobile
Installed for you:@frigear-nu/module-kit.You add: nothing else. <WidgetTabs> draws its icons with Nuxt UI's UIcon.Optional:@nuxtjs/i18n, to translate tab labels.

Quick setup

Install Widget

pnpm add @frigear-nu/widget

Add it to nuxt.config.ts

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@frigear-nu/widget'],
})

Draw an area

<WidgetArea name="admin.dashboard">
  <template #empty>Nothing to show yet.</template>
</WidgetArea>
What you should see: "Nothing to show yet." until a module calls extendWidgets(nuxt, 'admin.dashboard', …). Then its component appears in the grid.

Options

The module has no options. The widgets config key only holds internal state.

App

Components

<WidgetArea> and <WidgetTabs> draw an area on a page.

ComponentPropsSlotsPurpose
<WidgetArea>name: string, context?: Record<string, unknown>, gridClass?: stringempty, item with { widget, component, context }Draws every widget of the area in a responsive grid.
<WidgetTabs>name: string, context?: Record<string, unknown>emptyDraws the area's tabs, with the active tab's component below. Emits refresh.
  • context is what the page draws the area for, such as { user }. Each component receives its static props, then every entry of context as a prop. The filter hooks get it as ctx.context.
  • gridClass replaces the grid classes of the wrapper. The item slot lays the widgets out yourself.
  • <WidgetTabs> keeps the active tab in the ?tab= query parameter, without a navigation. It supports arrow keys, and passes a tab's refresh event on so the page can reload its data.

<WidgetTabs> looks exactly like PageTabs. When to use which is on Pages.

Composables

useWidgets and useTabs return an area's entries as plain data, ordered and filtered. They are auto-imported and return the shape of useAsyncData. context may be a ref or a getter.

useWidgets(area: string, context?: MaybeRefOrGetter<Record<string, unknown> | undefined>): AsyncData<WidgetMeta[]>
useTabs(area: string, context?: MaybeRefOrGetter<Record<string, unknown> | undefined>): AsyncData<TabMeta[]> // labels already translated

Add widgets and tabs from a module

Call extendWidgets or extendTabs in a module's setup(). The order between modules does not matter.

import { createResolver, defineNuxtModule } from '@nuxt/kit'
import { extendWidgets, extendTabs } from '@frigear-nu/widget/utils'

export default defineNuxtModule({
  setup(_options, nuxt) {
    const resolver = createResolver(import.meta.url)

    extendWidgets(nuxt, 'admin.dashboard', {
      component: resolver.resolve('./runtime/app/widgets/NewMembers.vue'),
      order: 10,
      span: 2,
      props: { days: 30 },
      permission: 'admin:access',
    })

    extendTabs(nuxt, 'admin.users', {
      id: 'payments',
      component: resolver.resolve('./runtime/app/tabs/Payments.vue'),
      order: 20,
      label: 'Payments',
      labelKey: 'payments.admin.tab',
      icon: 'i-lucide-credit-card',
      permission: 'payments:manage',
    })
  },
})
To replace a module's widget or tab, register your own under the same id in the same area. The id defaults to the component's file name.
type WidgetDefinition = {
  id?: string // unique within an area; default: component file name
  component: string // resolved path of the Vue component
  order?: number // lower first; unordered last, in registration order
  span?: 1 | 2 | 3 | 4 // grid columns in the default layout (default 1)
  props?: Record<string, unknown> // serializable
  requires?: string // free-form, for the widgets:filter hook; never interpreted by the module
  permission?: string // `resource:action`: shown only to viewers who hold it
  tags?: string[]
  meta?: Record<string, unknown>
}

type TabDefinition = {
  id?: string // default: component file name; also the `?tab=` value
  component: string
  order?: number
  props?: Record<string, unknown>
  label?: string // plain fallback label; default: the id
  labelKey?: string // i18n key, translated at render time with @nuxtjs/i18n
  icon?: string // for example 'i-lucide-credit-card'
  requires?: string // free-form, for the tabs:filter hook
  permission?: string // `resource:action`: shown only to viewers who hold it
  tags?: string[]
  meta?: Record<string, unknown>
}

type RegisteredWidget = WidgetDefinition & { id: string, area: string }
type RegisteredTab = TabDefinition & { id: string, area: string }
type WidgetMeta = Omit<RegisteredWidget, 'component'> // what useWidgets returns
type TabMeta = Omit<RegisteredTab, 'component' | 'labelKey' | 'label'> & { label: string, labelKey?: string } // what useTabs returns

Utils

@frigear-nu/widget/utils is framework-agnostic. The types above also come from @frigear-nu/widget/types.

ExportPurpose
extendWidgets(nuxt, area, widgets)Adds one or more widgets to an area, from a module's setup().
extendTabs(nuxt, area, tabs)Adds one or more tabs to an area.
deriveWidgetId(widget), deriveTabId(tab)The id an entry gets: its id, else the component file name.
sortWidgets(list)Orders by order, lowest first. Entries without one come last, in registration order.

Hide entries per visitor

The module itself never reads permission or requires. It calls widgets:filter and tabs:filter, and a listener drops what the current visitor should not see. With Authorization installed, a listener is already registered: it keeps an entry when it has no permission or when can(entry.permission) is true.

module.ts
extendTabs(nuxt, 'admin.users', {
  id: 'payments',
  component: resolver.resolve('./runtime/app/tabs/Payments.vue'),
  permission: 'payments:manage',
})

requires stays free-form, for your own rules such as a feature flag. Your app listens to the same hook:

app/plugins/widgets.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.hook('widgets:filter', (ctx) => {
    const flags = useRuntimeConfig().public.flags as string[]
    ctx.widgets = ctx.widgets.filter(widget => !widget.requires || flags.includes(widget.requires))
  })
})

Entries are cached under a key that includes an access version. Authorization raises the version on sign-in, sign-out and role changes, so widgets and tabs filter again without a reload.

Filtering only hides the interface. Always call authorize on the server for the data a widget loads.

Hooks

Module authors listen to the build hooks; your app listens to the filter hooks.

HookWhereWhenPayload
widgets:extendNuxt (build)extendWidgets adds widgets(area: string, widgets: RegisteredWidget[])
widgets:resolvedNuxt (build)Once, after every module has set up(areas: Record<string, RegisteredWidget[]>)
tabs:extendNuxt (build)extendTabs adds tabs(area: string, tabs: RegisteredTab[])
tabs:resolvedNuxt (build)Once, after every module has set up(areas: Record<string, RegisteredTab[]>)
widgets:filterNuxt appBefore an area's widgets are drawn{ area, widgets: WidgetMeta[], context? }; reassign or filter ctx.widgets
tabs:filterNuxt appBefore an area's tabs are drawn{ area, tabs: TabMeta[], context? }; reassign or filter ctx.tabs

Next steps

  • Admin – fill the three widget areas of the admin dashboard.
  • Pages – choose between WidgetTabs and PageTabs on a detail page.
  • Extension points – how widgets, menus and hooks let modules extend each other.
Copyright © 2026