doc/development/callouts.md
Callouts are a mechanism for presenting notifications to users. Users can dismiss the notifications, and the notifications can stay dismissed for a predefined duration. Notification dismissal is persistent across page loads and different user devices.
Global context: Callouts can be displayed to a user regardless of where they are in the application. For example, we can show a notification that reminds the user to have two-factor authentication recovery codes stored in a safe place. Dismissing this type of callout is effective for the particular user across the whole GitLab instance, no matter where they encountered the callout.
Group and project contexts: Callouts can also be displayed to a specific user and have a particular context binding, like a group or a project context. For example, group owners can be notified that their group is running out of available seats. Dismissing that callout would be effective for the particular user only in this particular group, while they would still see the same callout in other groups, if applicable.
Regardless of the context, dismissing a callout is only effective for the given user. Other users still see their relevant callouts.
Callouts use unique names to identify them, and a unique value to store dismissals data. For example:
amazing_alert: 42,
Here amazing_alert is the callout ID, and 42 is a unique number to be used to register dismissals in the database.
Here's how a group callout would be saved:
id | user_id | group_id | feature_name | dismissed_at
----+---------+----------+--------------+-------------------------------
0 | 1 | 4 | 42 | 2025-05-21 00:00:00.000000+00
To create a new callout ID, add a new key to the feature_name enum in the relevant context type registry file, using a
unique name and a sequential value:
app/models/users/callout.rb. Callouts are dismissed by a user globally. Related notifications would
not be displayed anywhere in the GitLab instance for that user.app/models/users/group_callout.rb. Callouts are dismissed by a user in a given group. Related
notifications are still shown to the user in other groups.app/models/users/project_callout.rb. Callouts dismissed by a user in a given project. Related
notifications are still shown to the user in other projects.After adding a new callout ID to any of the enum files above, you must update the GraphQL documentation by running:
bundle exec rake gitlab:graphql:compile_docs
[!note] Do not reuse old enum values, as it may lead to false-positive dismissals. Instead, create a new sequential number.
When we no longer need a callout, we can remove it from the callout ID enums. But since dismissal records in the DB use the numerical value of the enum, we need to explicitly preserve the deprecated ID from being reused, so that old dismissals don't affect the new callouts. Thus to remove a callout ID:
For example:
- amazing_alert: 42,
+ # 42 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920
When implementing dismissible alerts in HAML views, use the dismissible alert components. These components
extend Pajamas::AlertComponent and provide strong validation, simplified setup, and automatic handling of dismissal
logic.
Users::DismissibleAlertComponent - For user(global) context calloutsUsers::GroupDismissibleAlertComponent - For group context calloutsUsers::ProjectDismissibleAlertComponent - For project context calloutsAll components inherit from Pajamas::AlertComponent and support the same interface, with the addition of
dismiss_options and optional wrapper_options parameters.
= render Users::DismissibleAlertComponent.new(
title: _('Alert title'),
variant: :warning,
dismiss_options: { user: current_user, feature_id: 'my_user_callout' }
) do |c|
- c.with_body do
= _('Alert message content goes here.')
= render Users::GroupDismissibleAlertComponent.new(
title: _('Group-specific alert'),
dismiss_options: { user: current_user, group: @group, feature_id: 'my_group_callout' },
variant: :info
) do |c|
- c.with_body do
= _('This alert is specific to the current group.')
= render Users::ProjectDismissibleAlertComponent.new(
title: _('Project notification'),
dismiss_options: { user: current_user, project: @project, feature_id: 'my_project_callout' },
variant: :success
) do |c|
- c.with_body do
= _('This alert is specific to the current project.')
dismiss_options (required)All dismissible alert components require a dismiss_options hash:
{ user: current_user, feature_id: 'callout_name' }{ user: current_user, group: @group, feature_id: 'callout_name' }{ user: current_user, project: @project, feature_id: 'callout_name' }ignore_dismissal_earlier_than (optional)Add ignore_dismissal_earlier_than to make callouts reappear after a certain time period:
= render Users::DismissibleAlertComponent.new(
title: _('Recurring alert'),
dismiss_options: {
user: current_user,
feature_id: 'recurring_callout',
ignore_dismissal_earlier_than: 30.days.ago
}
) do |c|
- c.with_body do
= _('This alert will reappear every 30 days.')
You can use Time, Date, DateTime objects, or valid date/time strings:
# Using Time objects (recommended)
ignore_dismissal_earlier_than: 30.days.ago
ignore_dismissal_earlier_than: 1.week.ago
# Using date strings
ignore_dismissal_earlier_than: '2023-01-01'
ignore_dismissal_earlier_than: '2023-01-01 12:00:00'
Without this parameter, dismissals are permanent. With it, the alert reappears if it was dismissed before the specified time.
wrapper_options (optional)Use wrapper_options to wrap the alert in a custom container:
= render Users::GroupDismissibleAlertComponent.new(
title: _('Alert with wrapper'),
dismiss_options: { user: current_user, group: @group, feature_id: 'wrapped_callout' },
wrapper_options: { tag: :section, class: 'custom-wrapper' }
) do |c|
- c.with_body do
= _('This alert is wrapped in a custom container.')
AlertComponent parameters and functionalityWhen migrating from manual Pajamas::AlertComponent usage:
Before:
= render Pajamas::AlertComponent.new(
title: _('Alert title'),
variant: :warning,
alert_options: {
class: 'js-persistent-callout',
data: {
feature_id: 'my_callout',
dismiss_endpoint: callouts_path
}
},
dismissible: true
) do |c|
- c.with_body do
= _('Alert content')
After:
= render Users::DismissibleAlertComponent.new(
title: _('Alert title'),
variant: :warning,
dismiss_options: { user: current_user, feature_id: 'my_callout' }
) do |c|
- c.with_body do
= _('Alert content')
When implementing dismissible banners in HAML views, use the dismissible banner components. These components
extend Pajamas::BannerComponent and provide strong validation, simplified setup, and automatic handling of dismissal
logic.
Users::DismissibleBannerComponent - For user(global) context calloutsUsers::GroupDismissibleBannerComponent - For group context calloutsUsers::ProjectDismissibleBannerComponent - For project context calloutsAll components inherit from Pajamas::BannerComponent and support the same interface, with the addition of
dismiss_options and optional wrapper_options parameters.
= render Users::DismissibleBannerComponent.new(
button_text: _('Learn more'),
button_link: 'https://about.gitlab.com/',
svg_path: 'illustrations/devops-sm.svg',
variant: :promotion,
dismiss_options: { user: current_user, feature_id: 'my_user_callout' }
) do |c|
- c.with_title do
= _('Banner title')
%p
= _('Banner message content goes here.')
= render Users::GroupDismissibleBannerComponent.new(
button_text: _('Learn more'),
button_link: 'https://about.gitlab.com/',
svg_path: 'illustrations/devops-sm.svg',
variant: :promotion,
dismiss_options: { user: current_user, group: @group, feature_id: 'my_group_callout' }
) do |c|
- c.with_title do
= _('Group-specific banner')
%p
= _('This banner is specific to the current group.')
= render Users::ProjectDismissibleBannerComponent.new(
button_text: _('Learn more'),
button_link: 'https://about.gitlab.com/',
svg_path: 'illustrations/devops-sm.svg',
variant: :promotion,
dismiss_options: { user: current_user, project: @project, feature_id: 'my_project_callout' }
) do |c|
- c.with_title do
= _('Project notification')
%p
= _('This banner is specific to the current project.')
dismiss_options (required)All dismissible banner components require a dismiss_options hash:
{ user: current_user, feature_id: 'callout_name' }{ user: current_user, group: @group, feature_id: 'callout_name' }{ user: current_user, project: @project, feature_id: 'callout_name' }ignore_dismissal_earlier_than (optional)Add ignore_dismissal_earlier_than to make callouts reappear after a certain time period:
= render Users::DismissibleBannerComponent.new(
button_text: _('Learn more'),
svg_path: 'illustrations/devops-sm.svg',
variant: :promotion,
dismiss_options: {
user: current_user,
feature_id: 'recurring_callout',
ignore_dismissal_earlier_than: 30.days.ago
}
) do |c|
- c.with_title do
= _('Recurring banner')
%p
= _('This banner will reappear every 30 days.')
You can use Time, Date, DateTime objects, or valid date/time strings:
# Using Time objects (recommended)
ignore_dismissal_earlier_than: 30.days.ago
ignore_dismissal_earlier_than: 1.week.ago
# Using date strings
ignore_dismissal_earlier_than: '2023-01-01'
ignore_dismissal_earlier_than: '2023-01-01 12:00:00'
Without this parameter, dismissals are permanent. With it, the banner reappears if it was dismissed before the specified time.
wrapper_options (optional)Use wrapper_options to wrap the banner in a custom container:
= render Users::GroupDismissibleBannerComponent.new(
button_text: _('Learn more'),
svg_path: 'illustrations/devops-sm.svg',
variant: :promotion,
dismiss_options: { user: current_user, group: @group, feature_id: 'wrapped_callout' },
wrapper_options: { tag: :section, class: 'custom-wrapper' }
) do |c|
- c.with_title do
= _('Banner with wrapper')
%p
= _('This banner is wrapped in a custom container.')
BannerComponent parameters and functionalityWhen migrating from manual Pajamas::BannerComponent usage:
Before:
= render Pajamas::BannerComponent.new(
button_text: _('Learn more'),
svg_path: 'illustrations/devops-sm.svg',
variant: :promotion,
banner_options: {
class: 'js-persistent-callout',
data: { feature_id: 'my_callout', dismiss_endpoint: callouts_path }
}
) do |c|
- c.with_body do
= _('Banner content')
After:
= render Users::DismissibleBannerComponent.new(
button_text: _('Learn more'),
svg_path: 'illustrations/devops-sm.svg',
variant: :promotion,
dismiss_options: { user: current_user, feature_id: 'my_callout' }
) do |c|
- c.with_title do
= _('Banner title')
= _('Banner content')
This section describes using callouts when they are rendered on the client in .vue components.
For Vue components, we have dismisser wrapper components that integrate with GraphQL API to simplify dismissing and checking the dismissed state of callouts.
For global user callouts that should be dismissed across the entire GitLab instance, use <user-callout-dismisser>. Use
this component when the callout should be dismissed globally for the user across all groups and projects (e.g., feature
announcements, account security reminders).
<user-callout-dismisser feature-name="my_user_callout">
<template #default="{ dismiss, shouldShowCallout }">
<my-callout-component
v-if="shouldShowCallout"
@close="dismiss"
/>
</template>
</user-callout-dismisser>
See app/assets/javascripts/vue_shared/components/user_callout_dismisser.vue for more details.
For group-specific callouts that should only be dismissed within a particular group context, use
<user-group-callout-dismisser>. Use this component when the callout is specific to a group context and should only be
dismissed within that group (e.g., group billing notifications, group-specific feature promotions).
<user-group-callout-dismisser
feature-name="my_group_callout"
:group-id="groupId"
>
<template #default="{ dismiss, shouldShowCallout }">
<my-group-callout-component
v-if="shouldShowCallout"
@close="dismiss"
/>
</template>
</user-group-callout-dismisser>
The group-id prop accepts both numeric IDs (e.g., 123) and GraphQL IDs (e.g., 'gid://gitlab/Group/123'). The
component handles the conversion to GraphQL format internally, so you can pass either format.
See app/assets/javascripts/vue_shared/components/user_group_callout_dismisser.vue for more details.
Both components provide the same slot props:
dismiss: Function to dismiss the calloutshouldShowCallout: Boolean indicating if the callout should be displayed