diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json
index ad05f57b..c97859cc 100644
--- a/web/public/static/langs/en.json
+++ b/web/public/static/langs/en.json
@@ -1,10 +1,16 @@
 {
+  "action_bar_show_menu": "Show menu",
+  "action_bar_logo_alt": "ntfy logo",
   "action_bar_settings": "Settings",
   "action_bar_send_test_notification": "Send test notification",
   "action_bar_clear_notifications": "Clear all notifications",
   "action_bar_unsubscribe": "Unsubscribe",
+  "action_bar_toggle_mute": "Toggle mute notifications",
+  "action_bar_toggle_action_menu": "Toggle action menu",
   "message_bar_type_message": "Type a message here",
   "message_bar_error_publishing": "Error publishing notification",
+  "message_bar_show_dialog": "Show publish dialog",
+  "message_bar_publish": "Publish message",
   "nav_topics_title": "Subscribed topics",
   "nav_button_all_notifications": "All notifications",
   "nav_button_settings": "Settings",
@@ -16,14 +22,25 @@
   "alert_grant_button": "Grant now",
   "alert_not_supported_title": "Notifications not supported",
   "alert_not_supported_description": "Notifications are not supported in your browser.",
+  "notifications_list": "Notifications list",
+  "notifications_list_item": "Notification",
+  "notifications_delete": "Delete notification",
   "notifications_copied_to_clipboard": "Copied to clipboard",
   "notifications_tags": "Tags",
+  "notifications_priority_x": "Priority {{priority}}",
+  "notifications_new_indicator": "New notification",
+  "notifications_attachment_image": "Attachment image",
   "notifications_attachment_copy_url_title": "Copy attachment URL to clipboard",
   "notifications_attachment_copy_url_button": "Copy URL",
   "notifications_attachment_open_title": "Go to {{url}}",
   "notifications_attachment_open_button": "Open attachment",
   "notifications_attachment_link_expires": "link expires {{date}}",
   "notifications_attachment_link_expired": "download link expired",
+  "notifications_attachment_file_image": "image file",
+  "notifications_attachment_file_video": "video file",
+  "notifications_attachment_file_audio": "audio file",
+  "notifications_attachment_file_app": "Android app file",
+  "notifications_attachment_file_document": "other document",
   "notifications_click_copy_url_title": "Copy link URL to clipboard",
   "notifications_click_copy_url_button": "Copy link",
   "notifications_click_open_button": "Open link",
@@ -47,6 +64,7 @@
   "publish_dialog_attachment_limits_file_and_quota_reached": "exceeds {{fileSizeLimit}} file limit and quota, {{remainingBytes}} remaining",
   "publish_dialog_attachment_limits_file_reached": "exceeds {{fileSizeLimit}} file limit",
   "publish_dialog_attachment_limits_quota_reached": "exceeds quota, {{remainingBytes}} remaining",
+  "publish_dialog_emoji_picker_show": "Pick emoji",
   "publish_dialog_priority_min": "Min. priority",
   "publish_dialog_priority_low": "Low priority",
   "publish_dialog_priority_default": "Default priority",
@@ -89,6 +107,7 @@
   "publish_dialog_attached_file_filename_placeholder": "Attachment filename",
   "publish_dialog_drop_file_here": "Drop file here",
   "emoji_picker_search_placeholder": "Search emoji",
+  "emoji_picker_search_clear": "Clear search",
   "subscribe_dialog_subscribe_title": "Subscribe to topic",
   "subscribe_dialog_subscribe_description": "Topics may not be password-protected, so choose a name that's not easy to guess. Once subscribed, you can PUT/POST notifications.",
   "subscribe_dialog_subscribe_topic_placeholder": "Topic name, e.g. phil_alerts",
@@ -108,6 +127,7 @@
   "prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive",
   "prefs_notifications_sound_description_some": "Notifications play the {{sound}} sound when they arrive",
   "prefs_notifications_sound_no_sound": "No sound",
+  "prefs_notifications_sound_play": "Play selected sound",
   "prefs_notifications_min_priority_title": "Minimum priority",
   "prefs_notifications_min_priority_description_any": "Showing all notifications, regardless of priority",
   "prefs_notifications_min_priority_description_x_or_higher": "Show notifications if priority is {{number}} ({{name}}) or above",
@@ -130,7 +150,10 @@
   "prefs_notifications_delete_after_one_month_description": "Notifications are auto-deleted after one month",
   "prefs_users_title": "Manage users",
   "prefs_users_description": "Add/remove users for your protected topics here. Please note that username and password are stored in the browser's local storage.",
+  "prefs_users_table": "Users table",
   "prefs_users_add_button": "Add user",
+  "prefs_users_edit_button": "Edit user",
+  "prefs_users_delete_button": "Delete user",
   "prefs_users_table_user_header": "User",
   "prefs_users_table_base_url_header": "Service URL",
   "prefs_users_dialog_title_add": "Add user",
diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js
index 95e14440..30ab271e 100644
--- a/web/src/components/ActionBar.js
+++ b/web/src/components/ActionBar.js
@@ -44,16 +44,22 @@ const ActionBar = (props) => {
                 <IconButton
                     color="inherit"
                     edge="start"
+                    aria-label={t("action_bar_show_menu")}
                     onClick={props.onMobileDrawerToggle}
                     sx={{ mr: 2, display: { sm: 'none' } }}
                 >
                     <MenuIcon />
                 </IconButton>
-                <Box component="img" src={logo} sx={{
-                    display: { xs: 'none', sm: 'block' },
-                    marginRight: '10px',
-                    height: '28px'
-                }}/>
+                <Box
+                    component="img"
+                    src={logo}
+                    alt={t("action_bar_logo_alt")}
+                    sx={{
+                        display: { xs: 'none', sm: 'block' },
+                        marginRight: '10px',
+                        height: '28px'
+                    }}
+                />
                 <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
                     {title}
                 </Typography>
@@ -173,10 +179,10 @@ const SettingsIcons = (props) => {
 
     return (
         <>
-            <IconButton color="inherit" size="large" edge="end" onClick={handleToggleMute} sx={{marginRight: 0}}>
+            <IconButton color="inherit" size="large" edge="end" onClick={handleToggleMute} sx={{marginRight: 0}} aria-label={t("action_bar_toggle_mute")}>
                 {subscription.mutedUntil ? <NotificationsOffIcon/> : <NotificationsIcon/>}
             </IconButton>
-            <IconButton color="inherit" size="large" edge="end" ref={anchorRef} onClick={handleToggleOpen}>
+            <IconButton color="inherit" size="large" edge="end" ref={anchorRef} onClick={handleToggleOpen} aria-label={t("action_bar_toggle_action_menu")}>
                 <MoreVertIcon/>
             </IconButton>
             <Popper
diff --git a/web/src/components/AttachmentIcon.js b/web/src/components/AttachmentIcon.js
index 30e82273..337760b7 100644
--- a/web/src/components/AttachmentIcon.js
+++ b/web/src/components/AttachmentIcon.js
@@ -5,27 +5,36 @@ import fileImage from "../img/file-image.svg";
 import fileVideo from "../img/file-video.svg";
 import fileAudio from "../img/file-audio.svg";
 import fileApp from "../img/file-app.svg";
+import {useTranslation} from "react-i18next";
 
 const AttachmentIcon = (props) => {
+    const { t } = useTranslation();
     const type = props.type;
-    let imageFile;
+    let imageFile, imageLabel;
     if (!type) {
         imageFile = fileDocument;
+        imageLabel = t("notifications_attachment_file_image");
     } else if (type.startsWith('image/')) {
         imageFile = fileImage;
+        imageLabel = t("notifications_attachment_file_video");
     } else if (type.startsWith('video/')) {
         imageFile = fileVideo;
+        imageLabel = t("notifications_attachment_file_video");
     } else if (type.startsWith('audio/')) {
         imageFile = fileAudio;
+        imageLabel = t("notifications_attachment_file_audio");
     } else if (type === "application/vnd.android.package-archive") {
         imageFile = fileApp;
+        imageLabel = t("notifications_attachment_file_app");
     } else {
         imageFile = fileDocument;
+        imageLabel = t("notifications_attachment_file_document");
     }
     return (
         <Box
             component="img"
             src={imageFile}
+            alt={imageLabel}
             loading="lazy"
             sx={{
                 width: '28px',
diff --git a/web/src/components/EmojiPicker.js b/web/src/components/EmojiPicker.js
index 1392d7e8..9e5da67f 100644
--- a/web/src/components/EmojiPicker.js
+++ b/web/src/components/EmojiPicker.js
@@ -73,6 +73,8 @@ const EmojiPicker = (props) => {
                                 inputRef={searchRef}
                                 margin="dense"
                                 size="small"
+                                role="searchbox" 
+                                aria-label={t("emoji_picker_search_placeholder")}
                                 placeholder={t("emoji_picker_search_placeholder")}
                                 value={search}
                                 onChange={ev => setSearch(ev.target.value)}
@@ -83,7 +85,9 @@ const EmojiPicker = (props) => {
                                 InputProps={{
                                     endAdornment:
                                         <InputAdornment position="end" sx={{ display: (search) ? '' : 'none' }}>
-                                            <IconButton size="small" onClick={handleSearchClear} edge="end"><Close/></IconButton>
+                                            <IconButton size="small" onClick={handleSearchClear} edge="end" aria-label={t("emoji_picker_search_clear")}>
+                                                <Close/>
+                                            </IconButton>
                                         </InputAdornment>
                                 }}
                             />
@@ -130,10 +134,12 @@ const Category = (props) => {
 const Emoji = (props) => {
     const emoji = props.emoji;
     const matches = emojiMatches(emoji, props.search);
+    const title = `${emoji.description} (${emoji.aliases[0]})`;
     return (
         <EmojiDiv
             onClick={props.onClick}
-            title={`${emoji.description} (${emoji.aliases[0]})`}
+            title={title}
+            aria-label={title}
             style={{ display: (matches) ? '' : 'none' }}
         >
             {props.emoji.emoji}
diff --git a/web/src/components/Messaging.js b/web/src/components/Messaging.js
index b4418459..4ba1203f 100644
--- a/web/src/components/Messaging.js
+++ b/web/src/components/Messaging.js
@@ -75,13 +75,15 @@ const MessageBar = (props) => {
                 backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
             }}
         >
-            <IconButton color="inherit" size="large" edge="start" onClick={props.onOpenDialogClick}>
+            <IconButton color="inherit" size="large" edge="start" onClick={props.onOpenDialogClick} aria-label={t("message_bar_show_dialog")}>
                 <KeyboardArrowUpIcon/>
             </IconButton>
             <TextField
                 autoFocus
                 margin="dense"
                 placeholder={t("message_bar_type_message")}
+                aria-label={t("message_bar_type_message")}
+                role="textbox" 
                 type="text"
                 fullWidth
                 variant="standard"
@@ -94,7 +96,7 @@ const MessageBar = (props) => {
                     }
                 }}
             />
-            <IconButton color="inherit" size="large" edge="end" onClick={handleSendClick}>
+            <IconButton color="inherit" size="large" edge="end" onClick={handleSendClick} aria-label={t("message_bar_publish")}>
                 <SendIcon/>
             </IconButton>
             <Portal>
diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js
index 513930b7..b5a9fb19 100644
--- a/web/src/components/Navigation.js
+++ b/web/src/components/Navigation.js
@@ -31,10 +31,15 @@ const navWidth = 280;
 const Navigation = (props) => {
     const navigationList = <NavList {...props}/>;
     return (
-        <Box component="nav" sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}>
+        <Box
+            component="nav"
+            role="navigation" 
+            sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}
+        >
             {/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */}
             <Drawer
                 variant="temporary"
+                role="menubar" 
                 open={props.mobileDrawerOpen}
                 onClose={props.onMobileDrawerToggle}
                 ModalProps={{ keepMounted: true }} // Better open performance on mobile.
@@ -49,6 +54,7 @@ const Navigation = (props) => {
             <Drawer
                 open
                 variant="permanent"
+                role="menubar" 
                 sx={{
                     display: { xs: 'none', sm: 'block' },
                     '& .MuiDrawer-paper': { boxSizing: 'border-box', width: navWidth },
diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js
index 3a952190..b0bbae82 100644
--- a/web/src/components/Notifications.js
+++ b/web/src/components/Notifications.js
@@ -98,6 +98,8 @@ const NotificationList = (props) => {
         >
             <Container
                 maxWidth="md"
+                role="list" 
+                aria-label={t("notifications_list")}
                 sx={{
                     marginTop: 3,
                     marginBottom: (props.messageBar) ? "100px" : 3 // Hack to avoid hiding notifications behind the message bar
@@ -143,9 +145,9 @@ const NotificationItem = (props) => {
     const hasUserActions = notification.actions && notification.actions.length > 0;
     const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
     return (
-        <Card sx={{ minWidth: 275, padding: 1 }}>
+        <Card sx={{ minWidth: 275, padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}>
             <CardContent>
-                <IconButton onClick={handleDelete} sx={{ float: 'right', marginRight: -1, marginTop: -1 }}>
+                <IconButton onClick={handleDelete} sx={{ float: 'right', marginRight: -1, marginTop: -1 }} aria-label={t("notifications_delete")}>
                     <CloseIcon />
                 </IconButton>
                 <Typography sx={{ fontSize: 14 }} color="text.secondary">
@@ -153,15 +155,15 @@ const NotificationItem = (props) => {
                     {[1,2,4,5].includes(notification.priority) &&
                         <img
                             src={priorityFiles[notification.priority]}
-                            alt={`Priority ${notification.priority}`}
+                            alt={t("notifications_priority_x", { priority: notification.priority})}
                             style={{ verticalAlign: 'bottom' }}
                         />}
                     {notification.new === 1 &&
-                        <svg style={{ width: '8px', height: '8px', marginLeft: '4px' }} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
+                        <svg style={{ width: '8px', height: '8px', marginLeft: '4px' }} viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" aria-label={t("notifications_new_indicator")}>
                             <circle cx="50" cy="50" r="50" fill="#338574"/>
                         </svg>}
                 </Typography>
-                {notification.title && <Typography variant="h5" component="div">{formatTitle(notification)}</Typography>}
+                {notification.title && <Typography variant="h5" component="div" role="rowheader">{formatTitle(notification)}</Typography>}
                 <Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>
                     {autolink(maybeAppendActionErrors(formatMessage(notification), notification))}
                 </Typography>
@@ -289,6 +291,7 @@ const Attachment = (props) => {
 };
 
 const Image = (props) => {
+    const { t } = useTranslation();
     const [open, setOpen] = useState(false);
     return (
         <>
@@ -296,6 +299,7 @@ const Image = (props) => {
                 component="img"
                 src={props.attachment.url}
                 loading="lazy"
+                alt={t("notifications_attachment_image")}
                 onClick={() => setOpen(true)}
                 sx={{
                     marginTop: 2,
@@ -316,6 +320,7 @@ const Image = (props) => {
                     <Box
                         component="img"
                         src={props.attachment.url}
+                        alt={t("notifications_attachment_image")}
                         loading="lazy"
                         sx={{
                             maxWidth: 1,
@@ -347,13 +352,16 @@ const UserAction = (props) => {
     if (action.action === "broadcast") {
         return (
             <Tooltip title={t("notifications_actions_not_supported")}>
-                <span><Button disabled>{action.label}</Button></span>
+                <span><Button disabled aria-label={t("notifications_actions_not_supported")}>{action.label}</Button></span>
             </Tooltip>
         );
     } else if (action.action === "view") {
         return (
             <Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
-                <Button onClick={() => openUrl(action.url)}>{action.label}</Button>
+                <Button
+                    onClick={() => openUrl(action.url)}
+                    aria-label={t("notifications_actions_open_url_title", { url: action.url })}
+                >{action.label}</Button>
             </Tooltip>
         );
     } else if (action.action === "http") {
@@ -361,7 +369,10 @@ const UserAction = (props) => {
         const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
         return (
             <Tooltip title={t("notifications_actions_http_request_title", { method: method, url: action.url })}>
-                <Button onClick={() => performHttpAction(notification, action)}>{label}</Button>
+                <Button
+                    onClick={() => performHttpAction(notification, action)}
+                    aria-label={t("notifications_actions_http_request_title", { method: method, url: action.url })}
+                >{label}</Button>
             </Tooltip>
         );
     }
@@ -416,7 +427,7 @@ const NoNotifications = (props) => {
     return (
         <VerticallyCenteredContainer maxWidth="xs">
             <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
-                <img src={logoOutline} height="64" width="64"/><br />
+                <img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")}/><br />
                 {t("notifications_none_for_topic_title")}
             </Typography>
             <Paragraph>
@@ -442,7 +453,7 @@ const NoNotificationsWithoutSubscription = (props) => {
     return (
         <VerticallyCenteredContainer maxWidth="xs">
             <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
-                <img src={logoOutline} height="64" width="64"/><br />
+                <img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")}/><br />
                 {t("notifications_none_for_any_title")}
             </Typography>
             <Paragraph>
@@ -466,7 +477,7 @@ const NoSubscriptions = () => {
     return (
         <VerticallyCenteredContainer maxWidth="xs">
             <Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
-                <img src={logoOutline} height="64" width="64"/><br />
+                <img src={logoOutline} height="64" width="64" alt={t("action_bar_logo_alt")}/><br />
                 {t("notifications_no_subscriptions_title")}
             </Typography>
             <Paragraph>
diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js
index 117f11d1..7d320e39 100644
--- a/web/src/components/Preferences.js
+++ b/web/src/components/Preferences.js
@@ -34,11 +34,6 @@ import DialogActions from "@mui/material/DialogActions";
 import userManager from "../app/UserManager";
 import {playSound, shuffle, sounds, validUrl} from "../app/utils";
 import {useTranslation} from "react-i18next";
-import priority1 from "../img/priority-1.svg";
-import priority2 from "../img/priority-2.svg";
-import priority3 from "../img/priority-3.svg";
-import priority4 from "../img/priority-4.svg";
-import priority5 from "../img/priority-5.svg";
 
 const Preferences = () => {
     return (
@@ -55,7 +50,7 @@ const Preferences = () => {
 const Notifications = () => {
     const { t } = useTranslation();
     return (
-        <Card sx={{p: 3}}>
+        <Card sx={{p: 3}} aria-label={t("prefs_notifications_title")}>
             <Typography variant="h5" sx={{marginBottom: 2}}>
                 {t("prefs_notifications_title")}
             </Typography>
@@ -70,6 +65,7 @@ const Notifications = () => {
 
 const Sound = () => {
     const { t } = useTranslation();
+    const labelId = "prefSound";
     const sound = useLiveQuery(async () => prefs.sound());
     const handleChange = async (ev) => {
         await prefs.setSound(ev.target.value);
@@ -84,15 +80,15 @@ const Sound = () => {
         description = t("prefs_notifications_sound_description_some", { sound: sounds[sound].label });
     }
     return (
-        <Pref title={t("prefs_notifications_sound_title")} description={description}>
+        <Pref labelId={labelId} title={t("prefs_notifications_sound_title")} description={description}>
             <div style={{ display: 'flex', width: '100%' }}>
                 <FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
-                    <Select value={sound} onChange={handleChange}>
+                    <Select value={sound} onChange={handleChange} aria-labelledby={labelId}>
                         <MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem>
                         {Object.entries(sounds).map(s => <MenuItem key={s[0]} value={s[0]}>{s[1].label}</MenuItem>)}
                     </Select>
                 </FormControl>
-                <IconButton onClick={() => playSound(sound)} disabled={sound === "none"}>
+                <IconButton onClick={() => playSound(sound)} disabled={sound === "none"} aria-label={t("prefs_notifications_sound_play")}>
                     <PlayArrowIcon />
                 </IconButton>
             </div>
@@ -102,6 +98,7 @@ const Sound = () => {
 
 const MinPriority = () => {
     const { t } = useTranslation();
+    const labelId = "prefMinPriority";
     const minPriority = useLiveQuery(async () => prefs.minPriority());
     const handleChange = async (ev) => {
         await prefs.setMinPriority(ev.target.value);
@@ -128,9 +125,9 @@ const MinPriority = () => {
         });
     }
     return (
-        <Pref title={t("prefs_notifications_min_priority_title")} description={description}>
+        <Pref labelId={labelId} title={t("prefs_notifications_min_priority_title")} description={description}>
             <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
-                <Select value={minPriority} onChange={handleChange}>
+                <Select value={minPriority} onChange={handleChange} aria-labelledby={labelId}>
                     <MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem>
                     <MenuItem value={2}>{t("prefs_notifications_min_priority_low_and_higher")}</MenuItem>
                     <MenuItem value={3}>{t("prefs_notifications_min_priority_default_and_higher")}</MenuItem>
@@ -144,6 +141,7 @@ const MinPriority = () => {
 
 const DeleteAfter = () => {
     const { t } = useTranslation();
+    const labelId = "prefDeleteAfter";
     const deleteAfter = useLiveQuery(async () => prefs.deleteAfter());
     const handleChange = async (ev) => {
         await prefs.setDeleteAfter(ev.target.value);
@@ -161,9 +159,9 @@ const DeleteAfter = () => {
         }
     })();
     return (
-        <Pref title={t("prefs_notifications_delete_after_title")} description={description}>
+        <Pref labelId={labelId} title={t("prefs_notifications_delete_after_title")} description={description}>
             <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
-                <Select value={deleteAfter} onChange={handleChange}>
+                <Select value={deleteAfter} onChange={handleChange} aria-labelledby={labelId}>
                     <MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem>
                     <MenuItem value={10800}>{t("prefs_notifications_delete_after_three_hours")}</MenuItem>
                     <MenuItem value={86400}>{t("prefs_notifications_delete_after_one_day")}</MenuItem>
@@ -177,7 +175,7 @@ const DeleteAfter = () => {
 
 const PrefGroup = (props) => {
     return (
-        <div>
+        <div role="table">
             {props.children}
         </div>
     )
@@ -185,28 +183,39 @@ const PrefGroup = (props) => {
 
 const Pref = (props) => {
     return (
-        <div style={{
-            display: "flex",
-            flexDirection: "row",
-            marginTop: "10px",
-            marginBottom: "20px",
-        }}>
-            <div style={{
-                flex: '1 0 40%',
-                display: 'flex',
-                flexDirection: 'column',
-                justifyContent: 'center',
-                paddingRight: '30px'
-            }}>
+        <div
+            role="row"
+            style={{
+                display: "flex",
+                flexDirection: "row",
+                marginTop: "10px",
+                marginBottom: "20px",
+            }}
+        >
+            <div
+                role="cell"
+                id={props.labelId}
+                aria-label={props.title}
+                style={{
+                    flex: '1 0 40%',
+                    display: 'flex',
+                    flexDirection: 'column',
+                    justifyContent: 'center',
+                    paddingRight: '30px'
+                }}
+            >
                 <div><b>{props.title}</b></div>
                 {props.description && <div><em>{props.description}</em></div>}
             </div>
-            <div style={{
-                flex: '1 0 calc(60% - 50px)',
-                display: 'flex',
-                flexDirection: 'column',
-                justifyContent: 'center'
-            }}>
+            <div
+                role="cell"
+                style={{
+                    flex: '1 0 calc(60% - 50px)',
+                    display: 'flex',
+                    flexDirection: 'column',
+                    justifyContent: 'center'
+                }}
+            >
                 {props.children}
             </div>
         </div>
@@ -235,7 +244,7 @@ const Users = () => {
         }
     };
     return (
-        <Card sx={{ padding: 1 }}>
+        <Card sx={{ padding: 1 }} aria-label={t("prefs_users_title")}>
             <CardContent sx={{ paddingBottom: 1 }}>
                 <Typography variant="h5" sx={{marginBottom: 2}}>
                     {t("prefs_users_title")}
@@ -291,7 +300,7 @@ const UserTable = (props) => {
         }
     };
     return (
-        <Table size="small">
+        <Table size="small" aria-label={t("prefs_users_table")}>
             <TableHead>
                 <TableRow>
                     <TableCell sx={{paddingLeft: 0}}>{t("prefs_users_table_user_header")}</TableCell>
@@ -305,13 +314,13 @@ const UserTable = (props) => {
                         key={user.baseUrl}
                         sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
                     >
-                        <TableCell component="th" scope="row" sx={{paddingLeft: 0}}>{user.username}</TableCell>
-                        <TableCell>{user.baseUrl}</TableCell>
+                        <TableCell component="th" scope="row" sx={{paddingLeft: 0}} aria-label={t("prefs_users_table_user_header")}>{user.username}</TableCell>
+                        <TableCell aria-label={t("prefs_users_table_base_url_header")}>{user.baseUrl}</TableCell>
                         <TableCell align="right">
-                            <IconButton onClick={() => handleEditClick(user)}>
+                            <IconButton onClick={() => handleEditClick(user)} aria-label={t("prefs_users_edit_button")}>
                                 <EditIcon/>
                             </IconButton>
-                            <IconButton onClick={() => handleDeleteClick(user)}>
+                            <IconButton onClick={() => handleDeleteClick(user)} aria-label={t("prefs_users_delete_button")}>
                                 <CloseIcon />
                             </IconButton>
                         </TableCell>
@@ -371,6 +380,7 @@ const UserDialog = (props) => {
                     margin="dense"
                     id="baseUrl"
                     label={t("prefs_users_dialog_base_url_label")}
+                    aria-label={t("prefs_users_dialog_base_url_label")}
                     value={baseUrl}
                     onChange={ev => setBaseUrl(ev.target.value)}
                     type="url"
@@ -382,6 +392,7 @@ const UserDialog = (props) => {
                     margin="dense"
                     id="username"
                     label={t("prefs_users_dialog_username_label")}
+                    aria-label={t("prefs_users_dialog_username_label")}
                     value={username}
                     onChange={ev => setUsername(ev.target.value)}
                     type="text"
@@ -392,6 +403,7 @@ const UserDialog = (props) => {
                     margin="dense"
                     id="password"
                     label={t("prefs_users_dialog_password_label")}
+                    aria-label={t("prefs_users_dialog_password_label")}
                     type="password"
                     value={password}
                     onChange={ev => setPassword(ev.target.value)}
@@ -410,7 +422,7 @@ const UserDialog = (props) => {
 const Appearance = () => {
     const { t } = useTranslation();
     return (
-        <Card sx={{p: 3}}>
+        <Card sx={{p: 3}} aria-label={t("prefs_appearance_title")}>
             <Typography variant="h5" sx={{marginBottom: 2}}>
                 {t("prefs_appearance_title")}
             </Typography>
@@ -423,6 +435,7 @@ const Appearance = () => {
 
 const Language = () => {
     const { t, i18n } = useTranslation();
+    const labelId = "prefLanguage";
     const randomFlags = shuffle(["๐Ÿ‡ฌ๐Ÿ‡ง", "๐Ÿ‡บ๐Ÿ‡ธ", "๐Ÿ‡ช๐Ÿ‡ธ", "๐Ÿ‡ซ๐Ÿ‡ท", "๐Ÿ‡ง๐Ÿ‡ฌ", "๐Ÿ‡จ๐Ÿ‡ฟ", "๐Ÿ‡ฉ๐Ÿ‡ช", "๐Ÿ‡ฎ๐Ÿ‡ฉ", "๐Ÿ‡ฏ๐Ÿ‡ต", "๐Ÿ‡ท๐Ÿ‡บ", "๐Ÿ‡น๐Ÿ‡ท"]).slice(0, 3);
     const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
     const lang = i18n.language ?? "en";
@@ -432,9 +445,9 @@ const Language = () => {
     // Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l
 
     return (
-        <Pref title={title}>
+        <Pref labelId={labelId} title={title}>
             <FormControl fullWidth variant="standard" sx={{ m: 1 }}>
-                <Select value={lang} onChange={(ev) => i18n.changeLanguage(ev.target.value)}>
+                <Select value={lang} onChange={(ev) => i18n.changeLanguage(ev.target.value)} aria-labelledby={labelId}>
                     <MenuItem value="en">English</MenuItem>
                     <MenuItem value="bg">ะ‘ัŠะปะณะฐั€ัะบะธ</MenuItem>
                     <MenuItem value="cs">ฤŒeลกtina</MenuItem>
diff --git a/web/src/components/PublishDialog.js b/web/src/components/PublishDialog.js
index 00ba3d14..d78e67b8 100644
--- a/web/src/components/PublishDialog.js
+++ b/web/src/components/PublishDialog.js
@@ -240,6 +240,7 @@ const PublishDialog = (props) => {
                             <TextField
                                 margin="dense"
                                 label={t("publish_dialog_base_url_label")}
+                                aria-label={t("publish_dialog_base_url_label")}
                                 placeholder={t("publish_dialog_base_url_placeholder")}
                                 value={baseUrl}
                                 onChange={ev => setBaseUrl(ev.target.value)}
@@ -251,6 +252,7 @@ const PublishDialog = (props) => {
                             <TextField
                                 margin="dense"
                                 label={t("publish_dialog_topic_label")}
+                                aria-label={t("publish_dialog_topic_label")}
                                 placeholder={t("publish_dialog_topic_placeholder")}
                                 value={topic}
                                 onChange={ev => setTopic(ev.target.value)}
@@ -265,6 +267,7 @@ const PublishDialog = (props) => {
                     <TextField
                         margin="dense"
                         label={t("publish_dialog_title_label")}
+                        aria-label={t("publish_dialog_title_label")}
                         placeholder={t("publish_dialog_title_placeholder")}
                         value={title}
                         onChange={ev => setTitle(ev.target.value)}
@@ -276,6 +279,7 @@ const PublishDialog = (props) => {
                     <TextField
                         margin="dense"
                         label={t("publish_dialog_message_label")}
+                        aria-label={t("publish_dialog_message_label")}
                         placeholder={t("publish_dialog_message_placeholder")}
                         value={message}
                         onChange={ev => setMessage(ev.target.value)}
@@ -293,12 +297,13 @@ const PublishDialog = (props) => {
                             onEmojiPick={handleEmojiPick}
                             onClose={handleEmojiClose}
                         />
-                        <DialogIconButton disabled={disabled} onClick={handleEmojiClick}>
+                        <DialogIconButton disabled={disabled} onClick={handleEmojiClick} aria-label={t("publish_dialog_emoji_picker_show")}>
                             <InsertEmoticonIcon/>
                         </DialogIconButton>
                         <TextField
                             margin="dense"
                             label={t("publish_dialog_tags_label")}
+                            aria-label={t("publish_dialog_tags_label")}
                             placeholder={t("publish_dialog_tags_placeholder")}
                             value={tags}
                             onChange={ev => setTags(ev.target.value)}
@@ -315,15 +320,16 @@ const PublishDialog = (props) => {
                             <InputLabel/>
                             <Select
                                 label={t("publish_dialog_priority_label")}
+                                aria-label={t("publish_dialog_priority_label")}
                                 margin="dense"
                                 value={priority}
                                 onChange={(ev) => setPriority(ev.target.value)}
                                 disabled={disabled}
                             >
                                 {[5,4,3,2,1].map(priority =>
-                                    <MenuItem key={`priorityMenuItem${priority}`} value={priority}>
+                                    <MenuItem key={`priorityMenuItem${priority}`} value={priority} aria-label={t("notifications_priority_x", { priority: priority })}>
                                         <div style={{ display: 'flex', alignItems: 'center' }}>
-                                            <img src={priorities[priority].file} style={{marginRight: "8px"}}/>
+                                            <img src={priorities[priority].file} style={{marginRight: "8px"}} alt={t("notifications_priority_x", { priority: priority })}/>
                                             <div>{priorities[priority].label}</div>
                                         </div>
                                     </MenuItem>
@@ -339,6 +345,7 @@ const PublishDialog = (props) => {
                             <TextField
                                 margin="dense"
                                 label={t("publish_dialog_click_label")}
+                                aria-label={t("publish_dialog_click_label")}
                                 placeholder={t("publish_dialog_click_placeholder")}
                                 value={clickUrl}
                                 onChange={ev => setClickUrl(ev.target.value)}
@@ -357,6 +364,7 @@ const PublishDialog = (props) => {
                             <TextField
                                 margin="dense"
                                 label={t("publish_dialog_email_label")}
+                                aria-label={t("publish_dialog_email_label")}
                                 placeholder={t("publish_dialog_email_placeholder")}
                                 value={email}
                                 onChange={ev => setEmail(ev.target.value)}
@@ -377,6 +385,7 @@ const PublishDialog = (props) => {
                             <TextField
                                 margin="dense"
                                 label={t("publish_dialog_attach_label")}
+                                aria-label={t("publish_dialog_attach_label")}
                                 placeholder={t("publish_dialog_attach_placeholder")}
                                 value={attachUrl}
                                 onChange={ev => {
@@ -402,6 +411,7 @@ const PublishDialog = (props) => {
                             <TextField
                                 margin="dense"
                                 label={t("publish_dialog_filename_label")}
+                                aria-label={t("publish_dialog_filename_label")}
                                 placeholder={t("publish_dialog_filename_placeholder")}
                                 value={filename}
                                 onChange={ev => {
@@ -441,6 +451,7 @@ const PublishDialog = (props) => {
                             <TextField
                                 margin="dense"
                                 label={t("publish_dialog_delay_label")}
+                                aria-label={t("publish_dialog_delay_label")}
                                 placeholder={t("publish_dialog_delay_placeholder", {
                                     unixTimestamp: "1649029748",
                                     relativeTime: "30m",
@@ -459,12 +470,12 @@ const PublishDialog = (props) => {
                         {t("publish_dialog_other_features")}
                     </Typography>
                     <div>
-                        {!showClickUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_click_label")} onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
-                        {!showEmail && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_email_label")} onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
-                        {!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_url_label")} onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
-                        {!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_file_label")} onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
-                        {!showDelay && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_delay_label")} onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
-                        {!showTopicUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_topic_label")} onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showClickUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_click_label")} aria-label={t("publish_dialog_chip_click_label")} onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showEmail && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_email_label")} aria-label={t("publish_dialog_chip_email_label")} onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_url_label")} aria-label={t("publish_dialog_chip_attach_url_label")} onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_file_label")} aria-label={t("publish_dialog_chip_attach_file_label")} onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showDelay && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_delay_label")} aria-label={t("publish_dialog_chip_delay_label")} onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
+                        {!showTopicUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_topic_label")} aria-label={t("publish_dialog_chip_topic_label")} onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
                     </div>
                     <Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}>
                         <Trans
@@ -483,7 +494,13 @@ const PublishDialog = (props) => {
                                 label={t("publish_dialog_checkbox_publish_another")}
                                 sx={{marginRight: 2}}
                                 control={
-                                    <Checkbox size="small" checked={publishAnother} onChange={(ev) => setPublishAnother(ev.target.checked)} />
+                                    <Checkbox
+                                        size="small"
+                                        checked={publishAnother}
+                                        onChange={(ev) => setPublishAnother(ev.target.checked)}
+                                        inputProps={{
+                                            "aria-label": t("publish_dialog_checkbox_publish_another")
+                                        }} />
                                 } />
                             <Button onClick={props.onClose}>{t("publish_dialog_button_cancel")}</Button>
                             <Button onClick={handleSubmit} disabled={!sendButtonEnabled}>{t("publish_dialog_button_send")}</Button>
@@ -497,7 +514,7 @@ const PublishDialog = (props) => {
 
 const Row = (props) => {
     return (
-        <div style={{display: 'flex'}}>
+        <div style={{display: 'flex'}} role="row">
             {props.children}
         </div>
     );