diff --git a/tokio-console/src/input.rs b/tokio-console/src/input.rs
index 278991e55..088d5b771 100644
--- a/tokio-console/src/input.rs
+++ b/tokio-console/src/input.rs
@@ -33,3 +33,23 @@ pub(crate) fn is_space(input: &Event) -> bool {
         })
     )
 }
+
+pub(crate) fn is_help_toggle(event: &Event) -> bool {
+    matches!(
+        event,
+        Event::Key(KeyEvent {
+            code: KeyCode::Char('?'),
+            ..
+        })
+    )
+}
+
+pub(crate) fn is_esc(event: &Event) -> bool {
+    matches!(
+        event,
+        Event::Key(KeyEvent {
+            code: KeyCode::Esc,
+            ..
+        })
+    )
+}
diff --git a/tokio-console/src/view/async_ops.rs b/tokio-console/src/view/async_ops.rs
index 000319cc3..380799b8d 100644
--- a/tokio-console/src/view/async_ops.rs
+++ b/tokio-console/src/view/async_ops.rs
@@ -7,6 +7,7 @@ use crate::{
     },
     view::{
         self, bold,
+        controls::Controls,
         table::{TableList, TableListState},
         DUR_LEN, DUR_TABLE_PRECISION,
     },
@@ -194,6 +195,24 @@ impl TableList<9> for AsyncOpsTable {
             table_list_state.len()
         ))]);
 
+        let layout = layout::Layout::default()
+            .direction(layout::Direction::Vertical)
+            .margin(0);
+
+        let controls = Controls::new(view_controls(), &area, styles);
+        let chunks = layout
+            .constraints(
+                [
+                    layout::Constraint::Length(controls.height()),
+                    layout::Constraint::Max(area.height),
+                ]
+                .as_ref(),
+            )
+            .split(area);
+
+        let controls_area = chunks[0];
+        let async_ops_area = chunks[1];
+
         let attributes_width = layout::Constraint::Percentage(100);
         let widths = &[
             id_width.constraint(),
@@ -214,7 +233,8 @@ impl TableList<9> for AsyncOpsTable {
             .highlight_symbol(view::TABLE_HIGHLIGHT_SYMBOL)
             .highlight_style(Style::default().add_modifier(style::Modifier::BOLD));
 
-        frame.render_stateful_widget(table, area, &mut table_list_state.table_state);
+        frame.render_stateful_widget(table, async_ops_area, &mut table_list_state.table_state);
+        frame.render_widget(controls.into_widget(), controls_area);
 
         table_list_state
             .sorted_items
diff --git a/tokio-console/src/view/controls.rs b/tokio-console/src/view/controls.rs
index 81a90a7e2..bf9972532 100644
--- a/tokio-console/src/view/controls.rs
+++ b/tokio-console/src/view/controls.rs
@@ -38,8 +38,8 @@ impl Controls {
         styles: &view::Styles,
     ) -> Self {
         let mut spans_controls = Vec::with_capacity(view_controls.len() + UNIVERSAL_CONTROLS.len());
-        spans_controls.extend(view_controls.iter().map(|c| c.to_spans(styles)));
-        spans_controls.extend(UNIVERSAL_CONTROLS.iter().map(|c| c.to_spans(styles)));
+        spans_controls.extend(view_controls.iter().map(|c| c.to_spans(styles, 0)));
+        spans_controls.extend(UNIVERSAL_CONTROLS.iter().map(|c| c.to_spans(styles, 0)));
 
         let mut lines = vec![Spans::from(vec![Span::from("controls: ")])];
         let mut current_line = lines.last_mut().expect("This vector is never empty");
@@ -101,6 +101,18 @@ impl Controls {
     }
 }
 
+pub(crate) fn controls_paragraph<'a>(
+    view_controls: &[ControlDisplay],
+    styles: &view::Styles,
+) -> Paragraph<'a> {
+    let mut spans = Vec::with_capacity(1 + view_controls.len() + UNIVERSAL_CONTROLS.len());
+    spans.push(Spans::from(vec![Span::raw("controls:")]));
+    spans.extend(view_controls.iter().map(|c| c.to_spans(styles, 2)));
+    spans.extend(UNIVERSAL_CONTROLS.iter().map(|c| c.to_spans(styles, 2)));
+
+    Paragraph::new(spans)
+}
+
 /// Construct span to display a control.
 ///
 /// A control is made up of an action and one or more keys that will trigger
@@ -125,9 +137,10 @@ pub(crate) struct KeyDisplay {
 }
 
 impl ControlDisplay {
-    pub(crate) fn to_spans(&self, styles: &view::Styles) -> Spans<'static> {
+    pub(crate) fn to_spans(&self, styles: &view::Styles, indent: usize) -> Spans<'static> {
         let mut spans = Vec::new();
 
+        spans.push(Span::from(" ".repeat(indent)));
         spans.push(Span::from(self.action));
         spans.push(Span::from(" = "));
         for (idx, key_display) in self.keys.iter().enumerate() {
diff --git a/tokio-console/src/view/help.rs b/tokio-console/src/view/help.rs
new file mode 100644
index 000000000..de249db61
--- /dev/null
+++ b/tokio-console/src/view/help.rs
@@ -0,0 +1,67 @@
+use ratatui::{
+    layout::{self, Constraint, Direction, Layout},
+    widgets::{Clear, Paragraph},
+};
+
+use crate::{state::State, view};
+
+pub(crate) trait HelpText {
+    fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static>;
+}
+
+/// Simple view for help popup
+pub(crate) struct HelpView<'a> {
+    help_text: Option<Paragraph<'a>>,
+}
+
+impl<'a> HelpView<'a> {
+    pub(super) fn new(help_text: Paragraph<'a>) -> Self {
+        HelpView {
+            help_text: Some(help_text),
+        }
+    }
+
+    pub(crate) fn render<B: ratatui::backend::Backend>(
+        &mut self,
+        styles: &view::Styles,
+        frame: &mut ratatui::terminal::Frame<B>,
+        _area: layout::Rect,
+        _state: &mut State,
+    ) {
+        let r = frame.size();
+        let content = self
+            .help_text
+            .take()
+            .expect("help_text should be initialized");
+
+        let popup_layout = Layout::default()
+            .direction(Direction::Vertical)
+            .constraints(
+                [
+                    Constraint::Percentage(20),
+                    Constraint::Min(15),
+                    Constraint::Percentage(20),
+                ]
+                .as_ref(),
+            )
+            .split(r);
+
+        let popup_area = Layout::default()
+            .direction(Direction::Horizontal)
+            .constraints(
+                [
+                    Constraint::Percentage(20),
+                    Constraint::Percentage(60),
+                    Constraint::Percentage(20),
+                ]
+                .as_ref(),
+            )
+            .split(popup_layout[1])[1];
+
+        let display_text = content.block(styles.border_block().title("Help"));
+
+        // Clear the help block area and render the popup
+        frame.render_widget(Clear, popup_area);
+        frame.render_widget(display_text, popup_area);
+    }
+}
diff --git a/tokio-console/src/view/mod.rs b/tokio-console/src/view/mod.rs
index ca9e87fcc..72221ac73 100644
--- a/tokio-console/src/view/mod.rs
+++ b/tokio-console/src/view/mod.rs
@@ -1,4 +1,7 @@
-use crate::view::{resources::ResourcesTable, table::TableListState, tasks::TasksTable};
+use crate::view::help::HelpView;
+use crate::view::{
+    help::HelpText, resources::ResourcesTable, table::TableListState, tasks::TasksTable,
+};
 use crate::{input, state::State};
 use ratatui::{
     layout,
@@ -10,6 +13,7 @@ use std::{borrow::Cow, cmp};
 mod async_ops;
 mod controls;
 mod durations;
+mod help;
 mod mini_histogram;
 mod percentiles;
 mod resource;
@@ -43,6 +47,7 @@ pub struct View {
     tasks_list: TableListState<TasksTable, 12>,
     resources_list: TableListState<ResourcesTable, 9>,
     state: ViewState,
+    show_help_modal: bool,
     pub(crate) styles: Styles,
 }
 
@@ -96,6 +101,7 @@ impl View {
             state: ViewState::TasksList,
             tasks_list: TableListState::<TasksTable, 12>::default(),
             resources_list: TableListState::<ResourcesTable, 9>::default(),
+            show_help_modal: false,
             styles,
         }
     }
@@ -104,6 +110,11 @@ impl View {
         use ViewState::*;
         let mut update_kind = UpdateKind::Other;
 
+        if self.should_toggle_help_modal(&event) {
+            self.show_help_modal = !self.show_help_modal;
+            return update_kind;
+        }
+
         if matches!(event, key!(Char('t'))) {
             self.state = TasksList;
             return update_kind;
@@ -180,32 +191,46 @@ impl View {
         update_kind
     }
 
+    /// The help modal should toggle on the `?` key and should exit on `Esc`
+    fn should_toggle_help_modal(&mut self, event: &crossterm::event::Event) -> bool {
+        input::is_help_toggle(event) || (self.show_help_modal && input::is_esc(event))
+    }
+
     pub(crate) fn render<B: ratatui::backend::Backend>(
         &mut self,
         frame: &mut ratatui::terminal::Frame<B>,
         area: layout::Rect,
         state: &mut State,
     ) {
-        match self.state {
+        let help_text: &dyn HelpText = match self.state {
             ViewState::TasksList => {
                 self.tasks_list.render(&self.styles, frame, area, state, ());
+                &self.tasks_list
             }
             ViewState::ResourcesList => {
                 self.resources_list
                     .render(&self.styles, frame, area, state, ());
+                &self.resources_list
             }
             ViewState::TaskInstance(ref mut view) => {
                 let now = state
                     .last_updated_at()
                     .expect("task view implies we've received an update");
                 view.render(&self.styles, frame, area, now);
+                view
             }
             ViewState::ResourceInstance(ref mut view) => {
                 view.render(&self.styles, frame, area, state);
+                view
             }
-        }
+        };
 
         state.retain_active();
+
+        if self.show_help_modal {
+            let mut help_view = HelpView::new(help_text.render_help_content(&self.styles));
+            help_view.render(&self.styles, frame, area, state);
+        }
     }
 
     pub(crate) fn current_view(&self) -> &ViewState {
diff --git a/tokio-console/src/view/resource.rs b/tokio-console/src/view/resource.rs
index 05bc7f68b..25f3c45e0 100644
--- a/tokio-console/src/view/resource.rs
+++ b/tokio-console/src/view/resource.rs
@@ -6,7 +6,8 @@ use crate::{
         self,
         async_ops::{self, AsyncOpsTable, AsyncOpsTableCtx},
         bold,
-        controls::{ControlDisplay, Controls, KeyDisplay},
+        controls::{controls_paragraph, ControlDisplay, Controls, KeyDisplay},
+        help::HelpText,
         TableListState,
     },
 };
@@ -116,6 +117,12 @@ impl ResourceView {
     }
 }
 
+impl HelpText for ResourceView {
+    fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static> {
+        controls_paragraph(view_controls(), styles)
+    }
+}
+
 fn view_controls() -> &'static [ControlDisplay] {
     static VIEW_CONTROLS: OnceCell<Vec<ControlDisplay>> = OnceCell::new();
 
diff --git a/tokio-console/src/view/table.rs b/tokio-console/src/view/table.rs
index 1f36a25b8..10fdd16dc 100644
--- a/tokio-console/src/view/table.rs
+++ b/tokio-console/src/view/table.rs
@@ -2,13 +2,17 @@ use crate::{
     input, state,
     view::{
         self,
-        controls::{ControlDisplay, KeyDisplay},
+        controls::{controls_paragraph, ControlDisplay, KeyDisplay},
+        help::HelpText,
     },
 };
+use ratatui::{
+    layout,
+    widgets::{Paragraph, TableState},
+};
+use std::convert::TryFrom;
 
-use ratatui::{layout, widgets::TableState};
 use std::cell::RefCell;
-use std::convert::TryFrom;
 use std::rc::Weak;
 
 pub(crate) trait TableList<const N: usize> {
@@ -195,6 +199,15 @@ where
     }
 }
 
+impl<T, const N: usize> HelpText for TableListState<T, N>
+where
+    T: TableList<N>,
+{
+    fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static> {
+        controls_paragraph(view_controls(), styles)
+    }
+}
+
 pub(crate) const fn view_controls() -> &'static [ControlDisplay] {
     &[
         ControlDisplay {
diff --git a/tokio-console/src/view/task.rs b/tokio-console/src/view/task.rs
index c3c50fae5..368fc8abc 100644
--- a/tokio-console/src/view/task.rs
+++ b/tokio-console/src/view/task.rs
@@ -4,8 +4,9 @@ use crate::{
     util::Percentage,
     view::{
         self, bold,
-        controls::{ControlDisplay, Controls, KeyDisplay},
+        controls::{controls_paragraph, ControlDisplay, Controls, KeyDisplay},
         durations::Durations,
+        help::HelpText,
     },
 };
 use ratatui::{
@@ -253,6 +254,12 @@ impl TaskView {
     }
 }
 
+impl HelpText for TaskView {
+    fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static> {
+        controls_paragraph(view_controls(), styles)
+    }
+}
+
 const fn view_controls() -> &'static [ControlDisplay] {
     &[ControlDisplay {
         action: "return to task list",