Define a new widget for entities that have a dropdown menu that are not links (anchors)
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 7364327..943a5f2 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -56,6 +56,7 @@
 @import "widgets/hotkey_context";
 @import "widgets/hotkey";
 @import "widgets/icon";
+@import "widgets/interactive_text";
 @import "widgets/linear_progress";
 @import "widgets/menu";
 @import "widgets/middle_ellipsis";
diff --git a/ui/src/assets/widgets/interactive_text.scss b/ui/src/assets/widgets/interactive_text.scss
new file mode 100644
index 0000000..bd1b1c5
--- /dev/null
+++ b/ui/src/assets/widgets/interactive_text.scss
@@ -0,0 +1,38 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@import "../theme";
+
+.pf-interactive-text {
+  color: inherit;
+  border-radius: $border-radius;
+  cursor: pointer;
+  text-decoration: none;
+
+  &__icon--end {
+    margin-left: 1px;
+  }
+
+  &__icon--start {
+    margin-right: 1px;
+  }
+
+  &:hover {
+    background: color-hover-surface();
+  }
+
+  &:focus-visible {
+    @include focus;
+  }
+}
diff --git a/ui/src/components/details/args.ts b/ui/src/components/details/args.ts
index 9fd4ff3..d3b5248 100644
--- a/ui/src/components/details/args.ts
+++ b/ui/src/components/details/args.ts
@@ -21,6 +21,7 @@
 import {TreeNode} from '../../widgets/tree';
 import {Args, ArgsDict, ArgValue} from '../sql_utils/args';
 import {Trace} from '../../public/trace';
+import {InteractiveText} from '../../widgets/interactive_text';
 
 // Renders slice arguments (key/value pairs) as a subtree.
 export function renderArguments(
@@ -110,7 +111,7 @@
   } else {
     return m(
       PopupMenu,
-      {trigger: m(Anchor, {icon: Icons.ContextMenu}, key)},
+      {trigger: m(InteractiveText, {icon: Icons.ContextMenu}, key)},
       m(MenuItem, {
         label: 'Copy full key',
         icon: 'content_copy',
diff --git a/ui/src/components/details/slice_details.ts b/ui/src/components/details/slice_details.ts
index 8929aa2..ffed05c 100644
--- a/ui/src/components/details/slice_details.ts
+++ b/ui/src/components/details/slice_details.ts
@@ -17,7 +17,6 @@
 import {sqliteString} from '../../base/string_utils';
 import {exists} from '../../base/utils';
 import {SliceDetails} from '../sql_utils/slice';
-import {Anchor} from '../../widgets/anchor';
 import {MenuItem, PopupMenu} from '../../widgets/menu';
 import {Section} from '../../widgets/section';
 import {SqlRef} from '../../widgets/sql_ref';
@@ -33,6 +32,7 @@
 import {Trace} from '../../public/trace';
 import {extensions} from '../extensions';
 import {SLICE_TABLE} from '../widgets/sql/table_definitions';
+import {InteractiveText} from '../../widgets/interactive_text';
 
 // Renders a widget storing all of the generic details for a slice from the
 // slice table.
@@ -51,7 +51,7 @@
         right: m(
           PopupMenu,
           {
-            trigger: m(Anchor, slice.name),
+            trigger: m(InteractiveText, slice.name),
           },
           m(MenuItem, {
             label: 'Slices with the same name',
diff --git a/ui/src/components/widgets/datagrid/datagrid.ts b/ui/src/components/widgets/datagrid/datagrid.ts
index 152a0db..be5d249 100644
--- a/ui/src/components/widgets/datagrid/datagrid.ts
+++ b/ui/src/components/widgets/datagrid/datagrid.ts
@@ -20,7 +20,7 @@
 import {shortUuid} from '../../../base/uuid';
 import {exists, isNumeric, maybeUndefined} from '../../../base/utils';
 import {Row, SqlValue} from '../../../trace_processor/query_result';
-import {Anchor} from '../../../widgets/anchor';
+import {InteractiveText} from '../../../widgets/interactive_text';
 import {Button, ButtonGroup, ButtonVariant} from '../../../widgets/button';
 import {EmptyState} from '../../../widgets/empty_state';
 import {
@@ -2305,7 +2305,7 @@
     return '';
   } else if (value instanceof Uint8Array) {
     return m(
-      Anchor,
+      InteractiveText,
       {
         icon: Icons.Download,
         onclick: () =>
diff --git a/ui/src/components/widgets/duration.ts b/ui/src/components/widgets/duration.ts
index a6bd3d1..b934a3c 100644
--- a/ui/src/components/widgets/duration.ts
+++ b/ui/src/components/widgets/duration.ts
@@ -16,12 +16,12 @@
 import {copyToClipboard} from '../../base/clipboard';
 import {Icons} from '../../base/semantic_icons';
 import {duration} from '../../base/time';
-import {Anchor} from '../../widgets/anchor';
 import {MenuDivider, MenuItem, PopupMenu} from '../../widgets/menu';
 import {Trace} from '../../public/trace';
 import {formatDuration} from '../time_utils';
 import {DurationPrecisionMenuItem} from './duration_precision_menu_items';
 import {TimestampFormatMenuItem} from './timestamp_format_menu';
+import {InteractiveText} from '../../widgets/interactive_text';
 
 interface DurationWidgetAttrs {
   trace: Trace;
@@ -39,7 +39,7 @@
     return m(
       PopupMenu,
       {
-        trigger: m(Anchor, value),
+        trigger: m(InteractiveText, {icon: Icons.ContextMenu}, value),
       },
       m(MenuItem, {
         icon: Icons.Copy,
diff --git a/ui/src/components/widgets/process.ts b/ui/src/components/widgets/process.ts
index 943afe3..ad5fe2e 100644
--- a/ui/src/components/widgets/process.ts
+++ b/ui/src/components/widgets/process.ts
@@ -24,7 +24,6 @@
   ProcessInfo,
 } from '../sql_utils/process';
 import {Trace} from '../../public/trace';
-import {Anchor} from '../../widgets/anchor';
 import {MenuItem, PopupMenu} from '../../widgets/menu';
 import {ProcessDetailsTab} from '../details/process_details_tab';
 import {
@@ -32,6 +31,7 @@
   sqlIdRegistry,
 } from '../widgets/sql/details/sql_ref_renderer_registry';
 import {asUpid} from '../sql_utils/core_types';
+import {InteractiveText} from '../../widgets/interactive_text';
 
 export function showProcessDetailsMenuItem(
   trace: Trace,
@@ -91,7 +91,7 @@
   return m(
     PopupMenu,
     {
-      trigger: m(Anchor, getProcessName(info)),
+      trigger: m(InteractiveText, getProcessName(info)),
     },
     processRefMenuItems(trace, info),
   );
diff --git a/ui/src/components/widgets/sched.ts b/ui/src/components/widgets/sched.ts
index c01c19a..ab39c55 100644
--- a/ui/src/components/widgets/sched.ts
+++ b/ui/src/components/widgets/sched.ts
@@ -14,9 +14,9 @@
 
 import m from 'mithril';
 import {SchedSqlId} from '../sql_utils/core_types';
-import {Anchor} from '../../widgets/anchor';
 import {Icons} from '../../base/semantic_icons';
 import {Trace} from '../../public/trace';
+import {InteractiveText} from '../../widgets/interactive_text';
 
 interface SchedRefAttrs {
   readonly trace: Trace;
@@ -42,7 +42,7 @@
 export class SchedRef implements m.ClassComponent<SchedRefAttrs> {
   view(vnode: m.Vnode<SchedRefAttrs>) {
     return m(
-      Anchor,
+      InteractiveText,
       {
         icon: Icons.UpdateSelection,
         onclick: () => {
diff --git a/ui/src/components/widgets/sql/table/columns.ts b/ui/src/components/widgets/sql/table/columns.ts
index 0527041..2467f90 100644
--- a/ui/src/components/widgets/sql/table/columns.ts
+++ b/ui/src/components/widgets/sql/table/columns.ts
@@ -46,7 +46,7 @@
   PerfettoSqlTypes,
 } from '../../../../trace_processor/perfetto_sql_type';
 import {parseJsonWithBigints} from '../../../../base/json_utils';
-import {Anchor} from '../../../../widgets/anchor';
+import {InteractiveText} from '../../../../widgets/interactive_text';
 import {MenuItem, PopupMenu} from '../../../../widgets/menu';
 import {Icons} from '../../../../base/semantic_icons';
 import {copyToClipboard} from '../../../../base/clipboard';
@@ -724,7 +724,7 @@
               ? m(
                   PopupMenu,
                   {
-                    trigger: m(Anchor, key),
+                    trigger: m(InteractiveText, key),
                   },
                   m(MenuItem, {
                     icon: Icons.Add,
diff --git a/ui/src/components/widgets/thread.ts b/ui/src/components/widgets/thread.ts
index 71a06d4..3120953 100644
--- a/ui/src/components/widgets/thread.ts
+++ b/ui/src/components/widgets/thread.ts
@@ -18,7 +18,6 @@
 import {exists} from '../../base/utils';
 import {addEphemeralTab} from '../details/add_ephemeral_tab';
 import {getThreadInfo, getThreadName, ThreadInfo} from '../sql_utils/thread';
-import {Anchor} from '../../widgets/anchor';
 import {MenuItem, PopupMenu} from '../../widgets/menu';
 import {ThreadDetailsTab} from '../details/thread_details_tab';
 import {
@@ -28,6 +27,7 @@
 import {asUtid} from '../sql_utils/core_types';
 import {Utid} from '../sql_utils/core_types';
 import {Trace} from '../../public/trace';
+import {InteractiveText} from '../../widgets/interactive_text';
 
 export function showThreadDetailsMenuItem(
   trace: Trace,
@@ -95,7 +95,7 @@
   return m(
     PopupMenu,
     {
-      trigger: m(Anchor, getThreadName(info)),
+      trigger: m(InteractiveText, getThreadName(info)),
     },
     threadRefMenuItems(trace, info),
   );
diff --git a/ui/src/components/widgets/thread_state.ts b/ui/src/components/widgets/thread_state.ts
index 9c92280..37fe3f8 100644
--- a/ui/src/components/widgets/thread_state.ts
+++ b/ui/src/components/widgets/thread_state.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 import {ThreadStateSqlId} from '../sql_utils/core_types';
-import {Anchor} from '../../widgets/anchor';
+import {InteractiveText} from '../../widgets/interactive_text';
 import {Icons} from '../../base/semantic_icons';
 import {ThreadState} from '../sql_utils/thread_state';
 import {Trace} from '../../public/trace';
@@ -35,7 +35,7 @@
 export class ThreadStateRef implements m.ClassComponent<ThreadStateRefAttrs> {
   view(vnode: m.Vnode<ThreadStateRefAttrs>) {
     return m(
-      Anchor,
+      InteractiveText,
       {
         icon: Icons.UpdateSelection,
         onclick: () => {
diff --git a/ui/src/components/widgets/timestamp.ts b/ui/src/components/widgets/timestamp.ts
index c6e20bd..60f6394 100644
--- a/ui/src/components/widgets/timestamp.ts
+++ b/ui/src/components/widgets/timestamp.ts
@@ -16,24 +16,25 @@
 import {copyToClipboard} from '../../base/clipboard';
 import {Icons} from '../../base/semantic_icons';
 import {time, Time} from '../../base/time';
-import {Anchor} from '../../widgets/anchor';
 import {MenuDivider, MenuItem, PopupMenu} from '../../widgets/menu';
 import {Trace} from '../../public/trace';
 import {TimestampFormatMenuItem} from './timestamp_format_menu';
 import {renderTimecode} from '../time_utils';
 import {TimestampFormat} from '../../public/timeline';
+import {InteractiveText} from '../../widgets/interactive_text';
 
 // import {MenuItem, PopupMenu2} from './menu';
 
 interface TimestampAttrs {
-  trace: Trace;
+  readonly trace: Trace;
   // The timestamp to print, this should be the absolute, raw timestamp as
   // found in trace processor.
-  ts: time;
+  readonly ts: time;
   // Custom text value to show instead of the default HH:MM:SS.mmm uuu nnn
   // formatting.
-  display?: m.Children;
-  extraMenuItems?: m.Child[];
+  readonly display?: m.Children;
+  // Additional menu items to show in the popup menu
+  readonly extraMenuItems?: m.Child[];
 }
 
 export class Timestamp implements m.ClassComponent<TimestampAttrs> {
@@ -44,8 +45,9 @@
       PopupMenu,
       {
         trigger: m(
-          Anchor,
+          InteractiveText,
           {
+            icon: Icons.ContextMenu,
             onmouseover: () => (timeline.hoverCursorTimestamp = ts),
             onmouseout: () => (timeline.hoverCursorTimestamp = undefined),
           },
diff --git a/ui/src/components/widgets/track_event_ref.ts b/ui/src/components/widgets/track_event_ref.ts
index 19eae5b..d756cd6 100644
--- a/ui/src/components/widgets/track_event_ref.ts
+++ b/ui/src/components/widgets/track_event_ref.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Anchor} from '../../widgets/anchor';
+import {InteractiveText} from '../../widgets/interactive_text';
 import {Icons} from '../../base/semantic_icons';
 import {Trace} from '../../public/trace';
 
@@ -34,7 +34,7 @@
 export class TrackEventRef implements m.ClassComponent<TrackEventRefAttrs> {
   view(vnode: m.Vnode<TrackEventRefAttrs>) {
     return m(
-      Anchor,
+      InteractiveText,
       {
         icon: Icons.UpdateSelection,
         onclick: () => {
diff --git a/ui/src/plugins/com.android.AndroidInputLifecycle/tab.ts b/ui/src/plugins/com.android.AndroidInputLifecycle/tab.ts
index 88232db..2bbd80f 100644
--- a/ui/src/plugins/com.android.AndroidInputLifecycle/tab.ts
+++ b/ui/src/plugins/com.android.AndroidInputLifecycle/tab.ts
@@ -28,7 +28,7 @@
   UNKNOWN,
 } from '../../trace_processor/query_result';
 import {Dataset, UnionDatasetWithLineage} from '../../trace_processor/dataset';
-import {Anchor} from '../../widgets/anchor';
+import {InteractiveText} from '../../widgets/interactive_text';
 import {Icons} from '../../base/semantic_icons';
 import {Checkbox} from '../../widgets/checkbox';
 import {LifecycleOverlay} from './overlay';
@@ -572,7 +572,7 @@
         ? m(DurationWidget, {dur, trace: this.trace})
         : m('span', '-'),
       nav &&
-        m(Anchor, {
+        m(InteractiveText, {
           icon: Icons.GoTo,
           onclick: () => this.goTo(nav),
           title: 'Go to event slice',
diff --git a/ui/src/plugins/dev.perfetto.QueryPage/query_table.ts b/ui/src/plugins/dev.perfetto.QueryPage/query_table.ts
index 8379bff..a0e9f0b 100644
--- a/ui/src/plugins/dev.perfetto.QueryPage/query_table.ts
+++ b/ui/src/plugins/dev.perfetto.QueryPage/query_table.ts
@@ -19,14 +19,18 @@
 import {DetailsShell} from '../../widgets/details_shell';
 import {Trace} from '../../public/trace';
 import {Icons} from '../../base/semantic_icons';
-import {DataGrid, renderCell, DataGridApi} from '../../components/widgets/datagrid/datagrid';
+import {
+  DataGrid,
+  renderCell,
+  DataGridApi,
+} from '../../components/widgets/datagrid/datagrid';
 import {
   CellRenderer,
   ColumnSchema,
   SchemaRegistry,
 } from '../../components/widgets/datagrid/datagrid_schema';
 import {InMemoryDataSource} from '../../components/widgets/datagrid/in_memory_data_source';
-import {Anchor} from '../../widgets/anchor';
+import {InteractiveText} from '../../widgets/interactive_text';
 import {Box} from '../../widgets/box';
 import {DataGridExportButton} from '../../components/widgets/datagrid/export_button';
 import {CopyToClipboardButton} from '../../widgets/copy_to_clipboard_button';
@@ -281,7 +285,7 @@
               const resolved = this.resolveIdTable(row, value);
               if (resolved !== undefined) {
                 return m(
-                  Anchor,
+                  InteractiveText,
                   {
                     title: `Go to ${resolved.table} on the timeline`,
                     icon: Icons.UpdateSelection,
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/interactive_text_demo.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/interactive_text_demo.ts
new file mode 100644
index 0000000..a2df24f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/interactive_text_demo.ts
@@ -0,0 +1,57 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {InteractiveText} from '../../../widgets/interactive_text';
+import {renderWidgetShowcase} from '../widgets_page_utils';
+import {Icons} from '../../../base/semantic_icons';
+
+export function renderInteractiveText(): m.Children {
+  return [
+    m(
+      '.pf-widget-intro',
+      m('h1', 'InteractiveText'),
+      m('p', [
+        'A clickable widget that looks like regular text (no blue color or ' +
+          'underline) but shows a gray background on hover.',
+      ]),
+    ),
+
+    renderWidgetShowcase({
+      renderWidget: ({icon, showInlineWithText, long}) =>
+        m('', [
+          showInlineWithText && 'Inline ',
+          m(
+            InteractiveText,
+            {
+              icon: icon ? Icons.ExternalLink : undefined,
+              onclick: () => alert('Clicked!'),
+            },
+            long
+              ? 'This is some really really really long text and hopefully it ' +
+                  'will overflow the container in order to demonstrate how long ' +
+                  'text within interactive texts behaves.'
+              : 'InteractiveText',
+          ),
+          showInlineWithText && ' text',
+        ]),
+
+      initialOpts: {
+        icon: true,
+        showInlineWithText: false,
+        long: false,
+      },
+    }),
+  ];
+}
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/tree_demo.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/tree_demo.ts
index c7d0462..f7d2ba1 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/demos/tree_demo.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/demos/tree_demo.ts
@@ -16,7 +16,7 @@
 import {LazyTreeNode, Tree, TreeNode} from '../../../widgets/tree';
 import {PopupMenu} from '../../../widgets/menu';
 import {MenuItem} from '../../../widgets/menu';
-import {Anchor} from '../../../widgets/anchor';
+import {InteractiveText} from '../../../widgets/interactive_text';
 import {PopupPosition} from '../../../widgets/popup';
 import {Icons} from '../../../base/semantic_icons';
 import {renderWidgetShowcase} from '../widgets_page_utils';
@@ -62,7 +62,7 @@
               {
                 position: PopupPosition.RightStart,
                 trigger: m(
-                  Anchor,
+                  InteractiveText,
                   {
                     icon: Icons.ContextMenu,
                   },
@@ -82,11 +82,11 @@
           m(TreeNode, {
             icon: 'account_tree',
             left: 'Process',
-            right: m(Anchor, {icon: 'open_in_new'}, '/bin/foo[789]'),
+            right: m(InteractiveText, {icon: 'open_in_new'}, '/bin/foo[789]'),
           }),
           m(TreeNode, {
             left: 'Thread',
-            right: m(Anchor, {icon: 'open_in_new'}, 'my_thread[456]'),
+            right: m(InteractiveText, {icon: 'open_in_new'}, 'my_thread[456]'),
           }),
           m(
             TreeNode,
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
index ce9cb39..07f0bea 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
@@ -33,6 +33,7 @@
 import {renderEmptyState} from './demos/empty_state_demo';
 import {renderForm} from './demos/form_demo';
 import {renderGrid} from './demos/grid_demo';
+import {renderInteractiveText} from './demos/interactive_text_demo';
 import {renderCharts} from './demos/charts_demo';
 import {renderHotkey} from './demos/hotkey_demo';
 import {renderIcon} from './demos/icon_demo';
@@ -87,6 +88,11 @@
   {id: 'emptystate', label: 'EmptyState', view: renderEmptyState},
   {id: 'form', label: 'Form', view: renderForm},
   {id: 'grid', label: 'Grid', view: renderGrid},
+  {
+    id: 'interactive-text',
+    label: 'InteractiveText',
+    view: renderInteractiveText,
+  },
   {id: 'hotkey', label: 'Hotkey', view: renderHotkey},
   {id: 'icon', label: 'Icon', view: renderIcon},
   {id: 'menu', label: 'Menu', view: renderMenu},
diff --git a/ui/src/widgets/button.ts b/ui/src/widgets/button.ts
index 1aa2854..43e4e3e 100644
--- a/ui/src/widgets/button.ts
+++ b/ui/src/widgets/button.ts
@@ -18,7 +18,7 @@
 import {Icon} from './icon';
 import {Popup, PopupPosition} from './popup';
 import {assertUnreachable} from '../base/assert';
-import {isEmptyVnodes} from '../base/mithril_utils';
+import {hasChildren, isEmptyVnodes} from '../base/mithril_utils';
 import {Tooltip} from './tooltip';
 
 export enum ButtonVariant {
@@ -78,7 +78,7 @@
 
 interface LabelButtonAttrs extends CommonAttrs {
   // Label buttons require a label.
-  readonly label: string;
+  readonly label?: string;
   // Label buttons can have an optional icon.
   readonly icon?: string;
 }
@@ -90,7 +90,8 @@
 }
 
 export class Button implements m.ClassComponent<ButtonAttrs> {
-  view({attrs}: m.CVnode<ButtonAttrs>) {
+  view(vnode: m.CVnode<ButtonAttrs>) {
+    const {attrs} = vnode;
     const {
       icon,
       active,
@@ -108,8 +109,11 @@
       ...htmlAttrs
     } = attrs;
 
-    const label = isLabelButtonAttrs(attrs) ? attrs.label : undefined;
-    const iconOnly = Boolean(icon && !label);
+    let label: m.Children = isLabelButtonAttrs(attrs) ? attrs.label : undefined;
+    if (hasChildren(vnode)) {
+      label = vnode.children;
+    }
+    const iconOnly = Boolean(icon && !Boolean(label));
 
     const classes = classNames(
       active && 'pf-active',
diff --git a/ui/src/widgets/copyable_link.ts b/ui/src/widgets/copyable_link.ts
index 9244107..6ae96ba 100644
--- a/ui/src/widgets/copyable_link.ts
+++ b/ui/src/widgets/copyable_link.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 import {copyToClipboard} from '../base/clipboard';
-import {Anchor} from './anchor';
+import {InteractiveText} from './interactive_text';
 
 interface CopyableLinkAttrs {
   url: string;
@@ -28,7 +28,7 @@
     return m(
       'div',
       m(
-        Anchor,
+        InteractiveText,
         {
           href: url,
           title: 'Click to copy the URL into the clipboard',
diff --git a/ui/src/widgets/interactive_text.ts b/ui/src/widgets/interactive_text.ts
new file mode 100644
index 0000000..ad6d330
--- /dev/null
+++ b/ui/src/widgets/interactive_text.ts
@@ -0,0 +1,47 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {HTMLAnchorAttrs} from './common';
+import {Icon} from './icon';
+
+export interface InteractiveTextAttrs extends HTMLAnchorAttrs {
+  // Optional icon to show at the end of the content.
+  readonly icon?: string;
+
+  // Optional icon to show at the start of the content.
+  readonly startIcon?: string;
+}
+
+/**
+ * A clickable widget that looks like regular text (no blue color or underline)
+ * but shows a gray background on hover.
+ */
+export class InteractiveText implements m.ClassComponent<InteractiveTextAttrs> {
+  view({attrs, children}: m.CVnode<InteractiveTextAttrs>) {
+    const {icon, startIcon, ...htmlAttrs} = attrs;
+
+    return m(
+      'a.pf-interactive-text',
+      htmlAttrs,
+      startIcon &&
+        m(Icon, {
+          icon: startIcon,
+          className: 'pf-interactive-text__icon--start',
+        }),
+      children,
+      icon && m(Icon, {icon, className: 'pf-interactive-text__icon--end'}),
+    );
+  }
+}
diff --git a/ui/src/widgets/sql_ref.ts b/ui/src/widgets/sql_ref.ts
index fbefab0..63db361 100644
--- a/ui/src/widgets/sql_ref.ts
+++ b/ui/src/widgets/sql_ref.ts
@@ -15,8 +15,8 @@
 import m from 'mithril';
 import {copyToClipboard} from '../base/clipboard';
 import {Icons} from '../base/semantic_icons';
-import {Anchor} from './anchor';
 import {MenuItem, PopupMenu} from './menu';
+import {InteractiveText} from './interactive_text';
 
 // This widget provides common styling and popup menu options for a SQL row,
 // given a table name and an ID.
@@ -41,7 +41,7 @@
         PopupMenu,
         {
           trigger: m(
-            Anchor,
+            InteractiveText,
             {icon: Icons.ContextMenu},
             `${table}[${idColumnName === 'id' ? '' : `${idColumnName}=`}${id}]`,
           ),