diff --git a/common/Kconfig b/common/Kconfig
index 0db3b6babca..048530adff3 100644
--- a/common/Kconfig
+++ b/common/Kconfig
@@ -332,6 +332,17 @@ config SYS_DEVICE_NULLDEV
 	  operation of the console by setting stdout to "nulldev". Enable
 	  this to use a serial console under board control.
 
+config CONSOLE_PAGER
+	bool "Enable console output paging"
+	depends on CONSOLE_MUX
+	default y if SANDBOX
+	help
+	  Enable pager functionality for console output. When enabled, long
+	  output will be paused after a configurable number of lines, waiting
+	  for user input (SPACE) to continue. The number of lines per page is
+	  controlled by the 'pager' environment variable. If the variable is
+	  not set or is empty, paging is disabled.
+
 endmenu
 
 menu "Logging"
diff --git a/common/Makefile b/common/Makefile
index 048e4a6b3e2..7270af457f5 100644
--- a/common/Makefile
+++ b/common/Makefile
@@ -26,6 +26,8 @@ obj-$(CONFIG_MII) += miiphyutil.o
 obj-$(CONFIG_CMD_MII) += miiphyutil.o
 obj-$(CONFIG_PHYLIB) += miiphyutil.o
 
+obj-$(CONFIG_CONSOLE_PAGER) += pager.o
+
 obj-$(CONFIG_USB_HOST) += usb.o usb_hub.o
 obj-$(CONFIG_USB_GADGET) += usb.o
 obj-$(CONFIG_USB_STORAGE) += usb_storage.o
diff --git a/common/pager.c b/common/pager.c
new file mode 100644
index 00000000000..084c741989f
--- /dev/null
+++ b/common/pager.c
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Deals with splitting up text output into separate screenfuls
+ *
+ * Copyright 2025 Simon Glass <sjg@chromium.org>
+ */
+
+#define LOG_CATEGORY LOGC_CONSOLE
+
+#include <errno.h>
+#include <malloc.h>
+#include <pager.h>
+#include <asm/global_data.h>
+
+DECLARE_GLOBAL_DATA_PTR;
+
+const char *pager_post(struct pager *pag, const char *s)
+{
+	struct membuf old;
+	int ret, len;
+
+	if (!pag)
+		return s;
+
+	len = strlen(s);
+	if (!len)
+		return NULL;
+
+	old = pag->mb;
+	ret = membuf_put(&pag->mb, s, len);
+	if (ret == len) {
+		/* all is well */
+	} else {
+		/*
+		 * We couldn't store any of the text, so we'll store none of
+		 * it. The pager is now in an non-functional state until it
+		 * can eject the overflow text.
+		 *
+		 * The buffer is presumably empty, since callers are not allowed
+		 * to call pager_post() unless all the output from the previous
+		 * call was provided via pager_next().
+		 */
+		pag->overflow = s;
+		pag->mb = old;
+	}
+
+	return pager_next(pag, 0);
+}
+
+const char *pager_next(struct pager *pag, int key)
+{
+	char *str, *p, *end;
+	int ret;
+
+	/* replace the real character we overwrite with nul, if needed */
+	if (pag->nulch) {
+		*pag->nulch = pag->oldch;
+		pag->nulch = NULL;
+	}
+
+	/* if we're at the limit, wait */
+	switch (pag->state) {
+	case PAGERST_OK:
+		break;
+	case PAGERST_AT_LIMIT:
+		pag->state = PAGERST_WAIT_USER;
+		return "\n: Press SPACE to continue";
+	case PAGERST_WAIT_USER:
+		if (key != ' ')
+			return PAGER_WAITING;
+		pag->state = PAGERST_CLEAR_PROMPT;
+		return "\r                         \r";
+	case PAGERST_CLEAR_PROMPT:
+		pag->state = PAGERST_OK;
+		break;
+	}
+
+	ret = membuf_getraw(&pag->mb, pag->buf.size - 1, false, &str);
+	if (!ret) {
+		if (pag->overflow) {
+			const char *oflow = pag->overflow;
+
+			pag->overflow = NULL;
+			return oflow;
+		}
+		return NULL;
+	}
+
+	/* return lines until we reach the limit */
+	for (p = str, end = str + ret; p < end; p++) {
+		if (*p == '\n' && ++pag->line_count == pag->page_len - 1) {
+			/* remember to display the pager message next time */
+			pag->state = PAGERST_AT_LIMIT;
+			pag->line_count = 0;
+
+			/* skip the newline, since our prompt has one */
+			p++;
+			break;
+		}
+	}
+
+	/* remove the used bytes from the membuf */
+	ret = membuf_getraw(&pag->mb, p - str, true, &str);
+
+	/* don't output the newline, since our prompt has one */
+	if (pag->state == PAGERST_AT_LIMIT)
+		p--;
+
+	/* terminate the string */
+	pag->nulch = p;
+	pag->oldch = *pag->nulch;
+	*pag->nulch = '\0';
+
+	return str;
+}
+
+void pager_uninit(struct pager *pag)
+{
+	abuf_uninit(&pag->buf);
+	free(pag);
+}
+
+int pager_init(struct pager **pagp, int page_len, int buf_size)
+{
+	struct pager *pag;
+
+	pag = malloc(sizeof(struct pager));
+	if (!pag)
+		return log_msg_ret("pag", -ENOMEM);
+	memset(pag, '\0', sizeof(struct pager));
+	pag->page_len = page_len;
+	if (!abuf_init_size(&pag->buf, buf_size))
+		return log_msg_ret("pah", -ENOMEM);
+
+	/*
+	 * nul-terminate the buffer, which will come in handy if we need to
+	 * return up to the last byte
+	 */
+	((char *)pag->buf.data)[buf_size - 1] = '\0';
+	membuf_init(&pag->mb, pag->buf.data, buf_size);
+	*pagp = pag;
+
+	return 0;
+}
diff --git a/include/pager.h b/include/pager.h
new file mode 100644
index 00000000000..16739d41119
--- /dev/null
+++ b/include/pager.h
@@ -0,0 +1,141 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Deals with splitting up text output into separate screenfuls
+ *
+ * Copyright 2025 Simon Glass <sjg@chromium.org>
+ */
+
+#ifndef __PAGER_H
+#define __PAGER_H
+
+#include <stdbool.h>
+#include <abuf.h>
+#include <membuf.h>
+#include <linux/sizes.h>
+
+#define PAGER_BUF_SIZE	SZ_4K
+
+/* Special return value from pager_next() indicating it's waiting for user input */
+#define PAGER_WAITING	((const char *)1)
+
+/**
+ * enum pager_state: Tracks the state of the pager
+ *
+ * @PAGERST_OK: Normal output is happening
+ * @PAGERST_AT_LIMIT: No more output can be provided; the next call to
+ * pager_next() will return a user prompt
+ * @PAGERST_WAIT_USER: Waiting for the user to press a key
+ * @PAGERST_CLEAR_PROMPT: Clearing the prompt ready for more output
+ */
+enum pager_state {
+	PAGERST_OK,
+	PAGERST_AT_LIMIT,
+	PAGERST_WAIT_USER,
+	PAGERST_CLEAR_PROMPT,
+};
+
+/**
+ * struct pager - pager state
+ *
+ * The pager uses a buffer @buf to hold text that it is in the process of
+ * sending out. This helps deal with the stdio puts() interface, which does not
+ * permit passing a string length, only a string, which means that strings must
+ * be nul-terminated. The termination is handled automatically by the pager.
+ *
+ * If the text passed to pager_post() is too large for @buf then all the next
+ * will be written at once, without any paging, in the next call to
+ * pager_next().
+ *
+ * The membuf @mb is only used to feed out text in chunks, with a pager message
+ * (and a keypress wait) inserted between each chunk.
+ *
+ * @line_count: Number of lines output since last pause
+ * @page_len: Sets the height of the page in lines. The maximum lines to display
+ * before pausing is one less than this. Set from 'pager' env variable
+ * @buf: Buffer containing text to eventually be returned
+ * @mb: Circular buffer to manage @buf
+ * @overflow: pointer to overflow text to send nexts
+ * @nulch: pointer to where a nul character was written, NULL if none
+ * @oldch: old character that was at @nulch
+ */
+struct pager {
+	int line_count;
+	int page_len;
+	struct abuf buf;
+	struct membuf mb;
+	const char *overflow;
+	char *nulch;
+	int oldch;
+	enum pager_state state;
+};
+
+#if CONFIG_IS_ENABLED(CONSOLE_PAGER)
+
+/**
+ * pager_post() - Add text to the input buffer for later handling
+ *
+ * The text is added to the pager buffer and fed out a screenful
+ * at a time. This function calls pager_post() after storing the text.
+ *
+ * After calling pager_post(), if it returns anything other than NULL, you must
+ * repeatedly call pager_next() until it returns NULL, otherwise text may be
+ * lost
+ *
+ * If @pag is NULL, this does nothing but return @s
+ *
+ * @pag: Pager to use, may be NULL
+ * @s: Text to add
+ * Return: text which should be sent to output, or NULL if there is no more.
+ */
+const char *pager_post(struct pager *pag, const char *s);
+
+/**
+ * pager_next() - Returns the next screenful of text to show
+ *
+ * If this function returns PAGER_WAITING then the caller must check for user
+ * input and pass in the keypress in the next call to pager_next(). It can
+ * busy-wait for a keypress, if desired, since pager_next() will only ever
+ * return PAGER_WAITING until @ch is non-zero.
+ *
+ * @pag: Pager to use
+ * @ch: Key that the user has pressed, or 0 if none
+ *
+ * Return: text which should be sent to output, or PAGER_WAITING if waiting for
+ * the user to press a key, or NULL if there is no more text.
+ */
+const char *pager_next(struct pager *pag, int ch);
+
+/**
+ * pager_uninit() - Uninit the pager
+ *
+ * Frees all memory and also @pag
+ *
+ * @pag: Pager to uninit
+ */
+void pager_uninit(struct pager *pag);
+
+#else
+static inline const char *pager_post(struct pager *pag, const char *s)
+{
+	return s;
+}
+
+static inline const char *pager_next(struct pager *pag, int ch)
+{
+	return NULL;
+}
+
+#endif
+
+/**
+ * pager_init() - Set up a new pager
+ *
+ * @pagp: Returns allocaed pager, on success
+ * @pagelen: Number of lines per page
+ * @buf_size: Buffer size to use in bytes, this is the maximum amount of output
+ * that can be paged
+ * Return: 0 if OK, -ENOMEM if out of memory
+ */
+int pager_init(struct pager **pagp, int page_len, int buf_size);
+
+#endif
