diff --git a/Telegram/Resources/iv_html/page.css b/Telegram/Resources/iv_html/page.css
index 9c0ab832f..cdbfc3108 100644
--- a/Telegram/Resources/iv_html/page.css
+++ b/Telegram/Resources/iv_html/page.css
@@ -12,6 +12,7 @@ body {
 	margin: 0;
 	background-color: var(--td-window-bg);
 	color: var(--td-window-fg);
+	zoom: var(--td-zoom-percentage);
 }
 
 html.custom_scroll ::-webkit-scrollbar {
diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp
index 5f304356d..71d9ae0e8 100644
--- a/Telegram/SourceFiles/core/core_settings.cpp
+++ b/Telegram/SourceFiles/core/core_settings.cpp
@@ -222,7 +222,7 @@ QByteArray Settings::serialize() const {
 		+ Serialize::stringSize(_customFontFamily)
 		+ sizeof(qint32) * 3
 		+ Serialize::bytearraySize(_tonsiteStorageToken)
-		+ sizeof(qint32);
+		+ sizeof(qint32) * 2;
 
 	auto result = QByteArray();
 	result.reserve(size);
@@ -377,7 +377,8 @@ QByteArray Settings::serialize() const {
 			<< qint32(_systemUnlockEnabled ? 1 : 0)
 			<< qint32(!_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2)
 			<< _tonsiteStorageToken
-			<< qint32(_includeMutedCounterFolders ? 1 : 0);
+			<< qint32(_includeMutedCounterFolders ? 1 : 0)
+			<< qint32(_ivZoom.current());
 	}
 
 	Ensures(result.size() == size);
@@ -501,6 +502,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	qint32 systemUnlockEnabled = _systemUnlockEnabled ? 1 : 0;
 	qint32 weatherInCelsius = !_weatherInCelsius ? 0 : *_weatherInCelsius ? 1 : 2;
 	QByteArray tonsiteStorageToken = _tonsiteStorageToken;
+	qint32 ivZoom = _ivZoom.current();
 
 	stream >> themesAccentColors;
 	if (!stream.atEnd()) {
@@ -810,6 +812,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	if (!stream.atEnd()) {
 		stream >> includeMutedCounterFolders;
 	}
+	if (!stream.atEnd()) {
+		stream >> ivZoom;
+	}
 	if (stream.status() != QDataStream::Ok) {
 		LOG(("App Error: "
 			"Bad data for Core::Settings::constructFromSerialized()"));
@@ -1021,6 +1026,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 		? std::optional<bool>()
 		: (weatherInCelsius == 1);
 	_tonsiteStorageToken = tonsiteStorageToken;
+	_ivZoom = ivZoom;
 }
 
 QString Settings::getSoundPath(const QString &key) const {
@@ -1408,6 +1414,7 @@ void Settings::resetOnLastLogout() {
 	_hiddenGroupCallTooltips = 0;
 	_storiesClickTooltipHidden = false;
 	_ttlVoiceClickTooltipHidden = false;
+	_ivZoom = 100;
 
 	_recentEmojiPreload.clear();
 	_recentEmoji.clear();
@@ -1547,4 +1554,16 @@ bool Settings::rememberedDeleteMessageOnlyForYou() const {
 	return _rememberedDeleteMessageOnlyForYou;
 }
 
+int Settings::ivZoom() const {
+	return _ivZoom.current();
+}
+rpl::producer<int> Settings::ivZoomValue() const {
+	return _ivZoom.value();
+}
+void Settings::setIvZoom(int value) {
+	constexpr auto kMin = 30;
+	constexpr auto kMax = 200;
+	_ivZoom = std::clamp(value, kMin, kMax);
+}
+
 } // namespace Core
diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h
index ab5f999a6..54199d8e5 100644
--- a/Telegram/SourceFiles/core/core_settings.h
+++ b/Telegram/SourceFiles/core/core_settings.h
@@ -915,6 +915,10 @@ public:
 		_tonsiteStorageToken = value;
 	}
 
+	[[nodiscard]] int ivZoom() const;
+	[[nodiscard]] rpl::producer<int> ivZoomValue() const;
+	void setIvZoom(int value);
+
 	[[nodiscard]] static bool ThirdColumnByDefault();
 	[[nodiscard]] static float64 DefaultDialogsWidthRatio();
 
@@ -1050,6 +1054,7 @@ private:
 	bool _systemUnlockEnabled = false;
 	std::optional<bool> _weatherInCelsius;
 	QByteArray _tonsiteStorageToken;
+	rpl::variable<int> _ivZoom = 100;
 
 	bool _tabbedReplacedWithInfo = false; // per-window
 	rpl::event_stream<bool> _tabbedReplacedWithInfoValue; // per-window
diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style
index 6a5757276..cea363566 100644
--- a/Telegram/SourceFiles/iv/iv.style
+++ b/Telegram/SourceFiles/iv/iv.style
@@ -29,6 +29,38 @@ ivBack: IconButton(ivMenuToggle) {
 	iconOver: ivBackIcon;
 	rippleAreaPosition: point(12px, 6px);
 }
+ivZoomButtonsSize: 26px;
+ivPlusMinusZoom: IconButton(ivMenuToggle) {
+	width: ivZoomButtonsSize;
+	height: ivZoomButtonsSize;
+
+	rippleAreaPosition: point(0px, 0px);
+	rippleAreaSize: ivZoomButtonsSize;
+	ripple: RippleAnimation(defaultRippleAnimation) {
+		color: windowBgOver;
+	}
+}
+ivResetZoomStyle: TextStyle(defaultTextStyle) {
+	font: font(12px);
+}
+ivResetZoom: RoundButton(defaultActiveButton) {
+	textFg: windowFg;
+	textFgOver: windowFgOver;
+	textBg: windowBg;
+	textBgOver: windowBgOver;
+
+	height: ivZoomButtonsSize;
+	padding: margins(0px, 0px, 0px, 0px);
+
+	style: ivResetZoomStyle;
+
+	ripple: defaultRippleAnimation;
+}
+ivResetZoomLabel: FlatLabel(defaultFlatLabel) {
+	textFg: windowFg;
+	style: ivResetZoomStyle;
+}
+ivResetZoomInnerPadding: 20px;
 ivBackIconDisabled: icon {{ "box_button_back", menuIconFg }};
 ivForwardIcon: icon {{ "box_button_back-flip_horizontal", menuIconColor }};
 ivForward: IconButton(ivBack) {
diff --git a/Telegram/SourceFiles/iv/iv_controller.cpp b/Telegram/SourceFiles/iv/iv_controller.cpp
index b864833dd..486778ef7 100644
--- a/Telegram/SourceFiles/iv/iv_controller.cpp
+++ b/Telegram/SourceFiles/iv/iv_controller.cpp
@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/platform/ui_platform_window_title.h"
 #include "ui/widgets/buttons.h"
 #include "ui/widgets/labels.h"
+#include "ui/widgets/menu/menu_action.h"
 #include "ui/widgets/rp_window.h"
 #include "ui/widgets/popup_menu.h"
 #include "ui/wrap/fade_wrap.h"
@@ -50,7 +51,145 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Iv {
 namespace {
 
-[[nodiscard]] QByteArray ComputeStyles() {
+constexpr auto kZoomStep = int(10);
+constexpr auto kDefaultZoom = int(100);
+
+class ItemZoom final : public Ui::Menu::Action {
+public:
+	ItemZoom(
+		not_null<RpWidget*> parent,
+		const not_null<Delegate*> delegate,
+		const style::Menu &st)
+	: Ui::Menu::Action(
+		parent,
+		st,
+		Ui::CreateChild<QAction>(parent),
+		nullptr,
+		nullptr)
+	, _delegate(delegate)
+	, _st(st) {
+		init();
+	}
+
+	void init() {
+		enableMouseSelecting();
+
+		AbstractButton::setDisabled(true);
+
+		class SmallButton final : public Ui::IconButton {
+		public:
+			SmallButton(
+				not_null<Ui::RpWidget*> parent,
+				QChar c,
+				float64 skip,
+				const style::color &color)
+			: Ui::IconButton(parent, st::ivPlusMinusZoom)
+			, _color(color)
+			, _skip(style::ConvertFloatScale(skip))
+			, _c(c) {
+			}
+
+			void paintEvent(QPaintEvent *event) override {
+				auto p = Painter(this);
+				Ui::RippleButton::paintRipple(
+					p,
+					st::ivPlusMinusZoom.rippleAreaPosition);
+				p.setPen(_color);
+				p.setFont(st::normalFont);
+				p.drawText(
+					QRectF(rect()).translated(0, _skip),
+					_c,
+					style::al_center);
+			}
+
+		private:
+			const style::color _color;
+			const float64 _skip;
+			const QChar _c;
+
+		};
+
+		const auto reset = Ui::CreateChild<Ui::RoundButton>(
+			this,
+			rpl::single<QString>(QString()),
+			st::ivResetZoom);
+		const auto resetLabel = Ui::CreateChild<Ui::FlatLabel>(
+			reset,
+			tr::lng_background_reset_default(),
+			st::ivResetZoomLabel);
+		resetLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
+		reset->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
+		reset->setClickedCallback([this] {
+			_delegate->ivSetZoom(kDefaultZoom);
+		});
+		reset->show();
+		const auto plus = Ui::CreateChild<SmallButton>(
+			this,
+			'+',
+			0,
+			_st.itemFg);
+		plus->setClickedCallback([this] {
+			_delegate->ivSetZoom(_delegate->ivZoom() + kZoomStep);
+		});
+		plus->show();
+		const auto minus = Ui::CreateChild<SmallButton>(
+			this,
+			QChar(0x2013),
+			-1,
+			_st.itemFg);
+		minus->setClickedCallback([this] {
+			_delegate->ivSetZoom(_delegate->ivZoom() - kZoomStep);
+		});
+		minus->show();
+
+		_delegate->ivZoomValue(
+		) | rpl::start_with_next([this](int value) {
+			_text.setText(_st.itemStyle, QString::number(value) + '%');
+			update();
+		}, lifetime());
+
+		rpl::combine(
+			sizeValue(),
+			reset->sizeValue()
+		) | rpl::start_with_next([=, this](const QSize &size, const QSize &) {
+			reset->setFullWidth(0
+				+ resetLabel->width()
+				+ st::ivResetZoomInnerPadding);
+			resetLabel->moveToLeft(
+				(reset->width() - resetLabel->width()) / 2,
+				(reset->height() - resetLabel->height()) / 2);
+			reset->moveToRight(
+				_st.itemPadding.right(),
+				(size.height() - reset->height()) / 2);
+			plus->moveToRight(
+				_st.itemPadding.right() + reset->width(),
+				(size.height() - plus->height()) / 2);
+			minus->moveToRight(
+				_st.itemPadding.right() + plus->width() + reset->width(),
+				(size.height() - minus->height()) / 2);
+		}, lifetime());
+	}
+
+	void paintEvent(QPaintEvent *event) override {
+		auto p = QPainter(this);
+		p.setPen(_st.itemFg);
+		_text.draw(p, {
+			.position = QPoint(
+				_st.itemIconPosition.x(),
+				(height() - _text.minHeight()) / 2),
+			.outerWidth = width(),
+			.availableWidth = width(),
+		});
+	}
+
+private:
+	const not_null<Delegate*> _delegate;
+	const style::Menu &_st;
+	Ui::Text::String _text;
+
+};
+
+[[nodiscard]] QByteArray ComputeStyles(int zoom) {
 	static const auto map = base::flat_map<QByteArray, const style::color*>{
 		{ "shadow-fg", &st::shadowFg },
 		{ "scroll-bg", &st::scrollBg },
@@ -85,7 +224,7 @@ namespace {
 	static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
 		{ "iv-join-channel", tr::lng_iv_join_channel },
 	};
-	return Ui::ComputeStyles(map, phrases)
+	return Ui::ComputeStyles(map, phrases, zoom)
 		+ ';'
 		+ Ui::ComputeSemiTransparentOverStyle(
 			"light-button-bg-over",
@@ -93,7 +232,7 @@ namespace {
 			st::windowBg);
 }
 
-[[nodiscard]] QByteArray WrapPage(const Prepared &page) {
+[[nodiscard]] QByteArray WrapPage(const Prepared &page, int zoom) {
 #ifdef Q_OS_MAC
 	const auto classAttribute = ""_q;
 #else // Q_OS_MAC
@@ -110,7 +249,7 @@ namespace {
 <html)"_q
 	+ classAttribute
 	+ R"( style=")"
-	+ Ui::EscapeForAttribute(ComputeStyles())
+	+ Ui::EscapeForAttribute(ComputeStyles(zoom))
 	+ R"(">
 	<head>
 		<meta charset="utf-8">
@@ -206,7 +345,8 @@ Controller::Controller(
 	Fn<ShareBoxResult(ShareBoxDescriptor)> showShareBox)
 : _delegate(delegate)
 , _updateStyles([=] {
-	const auto str = Ui::EscapeForScriptString(ComputeStyles());
+	const auto zoom = _delegate->ivZoom();
+	const auto str = Ui::EscapeForScriptString(ComputeStyles(zoom));
 	if (_webview) {
 		_webview->eval("IV.updateStyles('" + str + "');");
 	}
@@ -484,6 +624,16 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 			if (event->key() == Qt::Key_Escape) {
 				escape();
 			}
+			if (event->modifiers() & Qt::ControlModifier) {
+				if (event->key() == Qt::Key_Plus
+					|| event->key() == Qt::Key_Equal) {
+					_delegate->ivSetZoom(_delegate->ivZoom() + kZoomStep);
+				} else if (event->key() == Qt::Key_Minus) {
+					_delegate->ivSetZoom(_delegate->ivZoom() - kZoomStep);
+				} else if (event->key() == Qt::Key_0) {
+					_delegate->ivSetZoom(kDefaultZoom);
+				}
+			}
 		}
 	}, window->lifetime());
 
@@ -595,7 +745,8 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 
 				rpl::merge(
 					Lang::Updated(),
-					style::PaletteChanged()
+					style::PaletteChanged(),
+					_delegate->ivZoomValue() | rpl::to_empty
 				) | rpl::start_with_next([=] {
 					_updateStyles.call();
 				}, _webview->lifetime());
@@ -611,7 +762,8 @@ void Controller::createWebview(const Webview::StorageId &storageId) {
 				return Webview::DataResult::Failed;
 			}
 			return finishWith(
-				WrapPage(_pages[index]), "text/html; charset=utf-8");
+				WrapPage(_pages[index], _delegate->ivZoom()),
+				"text/html; charset=utf-8");
 		} else if (id.starts_with("page") && id.ends_with(".json")) {
 			auto index = 0;
 			const auto result = std::from_chars(
@@ -897,6 +1049,10 @@ void Controller::showMenu() {
 		showShareMenu();
 	}, &st::menuIconShare);
 
+	_menu->addSeparator();
+	_menu->addAction(
+		base::make_unique_q<ItemZoom>(_menu, _delegate, _menu->menu()->st()));
+
 	_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
 	_menu->popup(_window->body()->mapToGlobal(
 		QPoint(_window->body()->width(), 0) + st::ivMenuPosition));
diff --git a/Telegram/SourceFiles/iv/iv_delegate.h b/Telegram/SourceFiles/iv/iv_delegate.h
index 09374fa17..d722d19f0 100644
--- a/Telegram/SourceFiles/iv/iv_delegate.h
+++ b/Telegram/SourceFiles/iv/iv_delegate.h
@@ -18,6 +18,10 @@ public:
 	virtual void ivSetLastSourceWindow(not_null<QWidget*> window) = 0;
 	[[nodiscard]] virtual QRect ivGeometry() const = 0;
 	virtual void ivSaveGeometry(not_null<Ui::RpWindow*> window) = 0;
+
+	[[nodiscard]] virtual int ivZoom() const = 0;
+	[[nodiscard]] virtual rpl::producer<int> ivZoomValue() const = 0;
+	virtual void ivSetZoom(int value) = 0;
 };
 
 } // namespace Iv
diff --git a/Telegram/SourceFiles/iv/iv_delegate_impl.cpp b/Telegram/SourceFiles/iv/iv_delegate_impl.cpp
index 4de0fa03c..e97de87bf 100644
--- a/Telegram/SourceFiles/iv/iv_delegate_impl.cpp
+++ b/Telegram/SourceFiles/iv/iv_delegate_impl.cpp
@@ -117,4 +117,15 @@ void DelegateImpl::ivSaveGeometry(not_null<Ui::RpWindow*> window) {
 	}
 }
 
+int DelegateImpl::ivZoom() const {
+	return Core::App().settings().ivZoom();
+}
+rpl::producer<int> DelegateImpl::ivZoomValue() const {
+	return Core::App().settings().ivZoomValue();
+}
+void DelegateImpl::ivSetZoom(int value) {
+	Core::App().settings().setIvZoom(value);
+	Core::App().saveSettingsDelayed();
+}
+
 } // namespace Iv
diff --git a/Telegram/SourceFiles/iv/iv_delegate_impl.h b/Telegram/SourceFiles/iv/iv_delegate_impl.h
index 9c7c0fb9d..f564f237a 100644
--- a/Telegram/SourceFiles/iv/iv_delegate_impl.h
+++ b/Telegram/SourceFiles/iv/iv_delegate_impl.h
@@ -19,6 +19,10 @@ public:
 	[[nodiscard]] QRect ivGeometry() const override;
 	void ivSaveGeometry(not_null<Ui::RpWindow*> window) override;
 
+	[[nodiscard]] int ivZoom() const;
+	[[nodiscard]] rpl::producer<int> ivZoomValue() const;
+	void ivSetZoom(int value);
+
 private:
 	QPointer<QWidget> _lastSourceWindow;
 
diff --git a/Telegram/SourceFiles/ui/controls/location_picker.cpp b/Telegram/SourceFiles/ui/controls/location_picker.cpp
index 945e5cf8f..71b3c6f42 100644
--- a/Telegram/SourceFiles/ui/controls/location_picker.cpp
+++ b/Telegram/SourceFiles/ui/controls/location_picker.cpp
@@ -379,7 +379,7 @@ void VenuesController::rowPaintIcon(
 	static const auto phrases = base::flat_map<QByteArray, tr::phrase<>>{
 		{ "maps-places-in-area", tr::lng_maps_places_in_area },
 	};
-	return Ui::ComputeStyles(map, phrases, Window::Theme::IsNightMode());
+	return Ui::ComputeStyles(map, phrases, 100, Window::Theme::IsNightMode());
 }
 
 [[nodiscard]] QByteArray ReadResource(const QString &name) {
diff --git a/Telegram/SourceFiles/ui/webview_helpers.cpp b/Telegram/SourceFiles/ui/webview_helpers.cpp
index 3be7a6359..a0369421b 100644
--- a/Telegram/SourceFiles/ui/webview_helpers.cpp
+++ b/Telegram/SourceFiles/ui/webview_helpers.cpp
@@ -31,6 +31,7 @@ namespace {
 QByteArray ComputeStyles(
 		const base::flat_map<QByteArray, const style::color*> &colors,
 		const base::flat_map<QByteArray, tr::phrase<>> &phrases,
+		int zoom,
 		bool nightTheme) {
 	static const auto serialize = [](const style::color *color) {
 		return Serialize((*color)->c);
@@ -66,6 +67,9 @@ QByteArray ComputeStyles(
 		result += "--td-"_q + name + ':' + serialize(color) + ';';
 	}
 	result += "--td-night:"_q + (nightTheme ? "1" : "0") + ';';
+	result += "--td-zoom-percentage:"_q
+		+ (QString::number(zoom).toUtf8() + '%')
+		+ ';';
 	return result;
 }
 
diff --git a/Telegram/SourceFiles/ui/webview_helpers.h b/Telegram/SourceFiles/ui/webview_helpers.h
index 518096479..c75f69e89 100644
--- a/Telegram/SourceFiles/ui/webview_helpers.h
+++ b/Telegram/SourceFiles/ui/webview_helpers.h
@@ -19,6 +19,7 @@ namespace Ui {
 [[nodiscard]] QByteArray ComputeStyles(
 	const base::flat_map<QByteArray, const style::color*> &colors,
 	const base::flat_map<QByteArray, tr::phrase<>> &phrases,
+	int zoom,
 	bool nightTheme = false);
 [[nodiscard]] QByteArray ComputeSemiTransparentOverStyle(
 	const QByteArray &name,