diff --git a/web/source/settings-panel/admin/emoji.js b/web/source/settings-panel/admin/emoji.js
new file mode 100644
index 000000000..d19814f74
--- /dev/null
+++ b/web/source/settings-panel/admin/emoji.js
@@ -0,0 +1,23 @@
+/*
+	GoToSocial
+	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+	This program is free software: you can redistribute it and/or modify
+	it under the terms of the GNU Affero General Public License as published by
+	the Free Software Foundation, either version 3 of the License, or
+	(at your option) any later version.
+
+	This program is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	GNU Affero General Public License for more details.
+
+	You should have received a copy of the GNU Affero General Public License
+	along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+"use strict";
+
+module.exports = function AdminCustomization() {
+	return "custom emoji";
+};
diff --git a/web/source/settings-panel/admin/settings.js b/web/source/settings-panel/admin/settings.js
index 2158a43ff..eb99c1747 100644
--- a/web/source/settings-panel/admin/settings.js
+++ b/web/source/settings-panel/admin/settings.js
@@ -18,6 +18,51 @@
 
 "use strict";
 
+const Promise = require("bluebird");
+const React = require("react");
+const Redux = require("react-redux");
+
+const Submit = require("../components/submit");
+
+const api = require("../lib/api");
+const formFields = require("../lib/form-fields");
+const adminActions = require("../redux/reducers/instances").actions;
+
 module.exports = function AdminSettings() {
-	return "admin settings";
+	const dispatch = Redux.useDispatch();
+	const instance = Redux.useSelector(state => state.instances.adminSettings);
+
+	const { onTextChange, onCheckChange, onFileChange } = formFields(dispatch, adminActions.setAdminSettingsVal, instance);
+
+	const [errorMsg, setError] = React.useState("");
+	const [statusMsg, setStatus] = React.useState("");
+
+	function submit() {
+		setStatus("PATCHing");
+		setError("");
+		return Promise.try(() => {
+			return dispatch(api.user.updateProfile());
+		}).then(() => {
+			setStatus("Saved!");
+		}).catch((e) => {
+			setError(e.message);
+			setStatus("");
+		});
+	}
+
+	// function removeFile(name) {
+	// 	return function(e) {
+	// 		e.preventDefault();
+	// 		dispatch(user.setProfileVal([name, ""]));
+	// 		dispatch(user.setProfileVal([`${name}File`, ""]));
+	// 	};
+	// }
+
+	return (
+		<div className="user-profile">
+			<h1>Instance Settings</h1>
+
+			<Submit onClick={submit} label="Save" errorMsg={errorMsg} statusMsg={statusMsg} />
+		</div>
+	);
 };
\ No newline at end of file
diff --git a/web/source/settings-panel/index.js b/web/source/settings-panel/index.js
index 56d3aa9ef..da0eec34d 100644
--- a/web/source/settings-panel/index.js
+++ b/web/source/settings-panel/index.js
@@ -42,6 +42,7 @@ const nav = {
 	"Admin": {
 		"Instance Settings": require("./admin/settings.js"),
 		"Federation": require("./admin/federation.js"),
+		"Custom Emoji": require("./admin/emoji.js"),
 		"Customization": require("./admin/customization.js")
 	}
 };
diff --git a/web/source/settings-panel/lib/api/admin.js b/web/source/settings-panel/lib/api/admin.js
new file mode 100644
index 000000000..b43121d22
--- /dev/null
+++ b/web/source/settings-panel/lib/api/admin.js
@@ -0,0 +1,70 @@
+/*
+	GoToSocial
+	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+	This program is free software: you can redistribute it and/or modify
+	it under the terms of the GNU Affero General Public License as published by
+	the Free Software Foundation, either version 3 of the License, or
+	(at your option) any later version.
+
+	This program is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	GNU Affero General Public License for more details.
+
+	You should have received a copy of the GNU Affero General Public License
+	along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const d = require("dotty");
+
+const user = require("../../redux/reducers/user").actions;
+
+module.exports = function ({ apiCall, getChanges }) {
+	function updateCredentials(selector, keys) {
+		return function (dispatch, getState) {
+			return Promise.try(() => {
+				const state = selector(getState());
+
+				const update = getChanges(state, keys);
+
+				return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form"));
+			}).then((account) => {
+				return dispatch(user.setAccount(account));
+			});
+		};
+	}
+
+	return {
+		fetchAccount: function fetchAccount() {
+			return function (dispatch, _getState) {
+				return Promise.try(() => {
+					return dispatch(apiCall("GET", "/api/v1/accounts/verify_credentials"));
+				}).then((account) => {
+					return dispatch(user.setAccount(account));
+				});
+			};
+		},
+
+		updateProfile: function updateProfile() {
+			const formKeys = ["display_name", "locked", "source", "custom_css", "note"];
+
+			const renamedKeys = {
+				note: "source.note"
+			};
+
+			const fileKeys = ["header", "avatar"];
+
+			return updateCredentials((state) => state.user.profile, {formKeys, renamedKeys, fileKeys});
+		},
+
+		updateSettings: function updateProfile() {
+			const formKeys = ["source"];
+
+			return updateCredentials((state) => state.user.settings, {formKeys});
+		}
+	};
+};
\ No newline at end of file
diff --git a/web/source/settings-panel/redux/reducers/instances.js b/web/source/settings-panel/redux/reducers/instances.js
index de874662b..3ad5bb7cb 100644
--- a/web/source/settings-panel/redux/reducers/instances.js
+++ b/web/source/settings-panel/redux/reducers/instances.js
@@ -19,6 +19,7 @@
 "use strict";
 
 const {createSlice} = require("@reduxjs/toolkit");
+const d = require("dotty");
 
 module.exports = createSlice({
 	name: "instances",
@@ -32,6 +33,10 @@ module.exports = createSlice({
 		},
 		setInstanceInfo: (state, {payload}) => {
 			state.current = payload;
+			state.adminSettings = payload;
+		},
+		setAdminSettingsVal: (state, {payload: [key, val]}) => {
+			d.put(state.adminSettings, key, val);
 		}
 	}
 });
\ No newline at end of file