A help button at the bottom of the settings dropdown, and the modal it opens. The modal answers common questions and hands off to email.
PLANS-PRICING.md and the dev docs — swap in the real top themes from beta feedback before this ships. The answers are already correct; which eight make the cut is an editorial pick.
<SettingsMenu> · ui/blocks/SettingsMenu.vue — unchanged; the treatment is one more child in its default slot<TextButton> · ui/buttons/TextButton.vue — variant="green" already ships (--c-button-text-green)<MenuDivider> · ui/menus/MenuDivider.vue — not used by this option (no rule above the link)<HelpModal> — the modal it opens, specced in section 2<template #settingsMenu>
<SettingsMenu v-model:popped="menuTriggered">
<template #default="{ unpop }">
<!-- ...Skin Tone / Theme / Emoji Size rows unchanged... -->
<MenuDivider />
<PrimaryButton
:disabled="!account.isAuthenticated"
@click="openAllSettings(); unpop()"
iconStart="gear" variant="green" size="md" label="View all settings" />
<!-- NEW: help entry point, always enabled (signed out too) -->
<div class="settings-help--link">
<TextButton size="md" variant="green" label="Need help?"
@click="helpPopped = true; unpop()" />
</div>
</template>
</SettingsMenu>
</template>
<!-- NEW: the modal itself, a sibling of the menu (see section 2) -->
<HelpModal v-model:popped="helpPopped" />
.settings-help--link {
display: flex;
justify-content: center;
}
<SettingsMenu> · ui/blocks/SettingsMenu.vue — unchanged; the button is one more child in its default slot<PrimaryButton> · ui/buttons/PrimaryButton.vue — variant="transparent" + outline already ship (.button.variant\:transparent.outline)<Icon name="information" /> · ui/icons/Information.vue, registered in IconIndex.vue — no new icon needed<HelpModal> — the modal it opens, specced in section 2<template #settingsMenu>
<SettingsMenu v-model:popped="menuTriggered">
<template #default="{ unpop }">
<!-- ...Skin Tone / Theme / Emoji Size rows unchanged... -->
<MenuDivider />
<PrimaryButton
:disabled="!account.isAuthenticated"
@click="openAllSettings(); unpop()"
iconStart="gear" variant="green" size="md" label="View all settings" />
<!-- NEW: help entry point. Same geometry as the green button
(46px pill), transparent + outline so it sits one tier
quieter. Never disabled — see behavior notes. -->
<PrimaryButton
@click="helpPopped = true; unpop()"
iconStart="information" variant="transparent" outline
size="md" label="Help & FAQs" />
</template>
</SettingsMenu>
</template>
<!-- NEW: the modal itself, a sibling of the menu (see section 2) -->
<HelpModal v-model:popped="helpPopped" />
/* ui/buttons/Button.vue — already in the app */
.button.variant\:transparent {
--bg: none;
--clr: var(--c-text-primary);
--brdr-clr: var(--c-border-primary);
}
.button.variant\:transparent:active,
.button.variant\:transparent.active {
--brdr-clr: transparent;
--bg: var(--c-surface-highlight);
}
.button.variant\:transparent.outline { border: 1px solid var(--brdr-clr); }
/* ui/buttons/PrimaryButton.vue — already in the app */
.primary-button {
border-radius: 10rem;
height: max(3.38em, 4.6rem); /* 46px at 13px */
gap: max(.66em, .8rem);
padding: 0 max(1.77em, .8rem);
background: var(--bg);
}
<SettingsMenu> · ui/blocks/SettingsMenu.vue — unchanged; the row is one more child in its default slot<Icon name="information" /> + <Icon name="arrow-caret-right" /> · both registered in IconIndex.vue — no new icons<SettingsHelpRow> — new atom: full-bleed menu-footer row (icon · label · caret)<HelpModal> — the modal it opens, specced in section 2<script setup>
import Icon from '@/components/Icon.vue';
import Text from '@/components/Text.vue';
const props = defineProps(['label']);
</script>
<template>
<button class="settings-help-row" type="button">
<Icon name="information" weight="bold" class="settings-help-row__icon" />
<Text type="body/s-heavy" class="settings-help-row__label">{{ props.label }}</Text>
<Icon name="arrow-caret-right" class="settings-help-row__caret" />
</button>
</template>
<style scoped>
.settings-help-row {
display: flex;
align-items: center;
gap: 10px;
/* full-bleed: cancel SettingsMenu's 16px surface padding so the row
and its hairline run edge to edge, and eat the surface's 16px gap
so it hugs the button above it */
width: calc(100% + 32px);
margin: -4px -16px -8px;
padding: 12px 16px;
border: none;
border-top: 1px solid var(--c-border-primary);
background: none;
cursor: pointer;
text-align: left;
&:hover { background: var(--c-button-gray-bg); }
.settings-help-row__label { flex: 1 1 auto; }
.settings-help-row__icon {
width: 18px;
height: 18px;
color: var(--c-icon-green);
flex: none;
}
.settings-help-row__caret {
width: 14px;
height: 14px;
color: var(--c-text-tertiary);
flex: none;
}
}
</style>
<template #settingsMenu>
<SettingsMenu v-model:popped="menuTriggered">
<template #default="{ unpop }">
<!-- ...Skin Tone / Theme / Emoji Size rows unchanged... -->
<MenuDivider />
<PrimaryButton
:disabled="!account.isAuthenticated"
@click="openAllSettings(); unpop()"
iconStart="gear" variant="green" size="md" label="View all settings" />
<!-- NEW: full-bleed footer row. Carries its own hairline,
so no <MenuDivider /> before it. -->
<SettingsHelpRow label="Help & FAQs"
@click="helpPopped = true; unpop()" />
</template>
</SettingsMenu>
</template>
<!-- NEW: the modal itself, a sibling of the menu (see section 2) -->
<HelpModal v-model:popped="helpPopped" />
Answers to what we get asked most.
<PopModal> · ui/components/pop/PopModal.vue — unchanged shell (teleport, escape-to-close, layering)<PrimaryButton variant="green"> · ui/buttons/PrimaryButton.vue<Icon name="arrow-caret-down" /> · registered in IconIndex.vue<Text> · ui/components/Text.vue<HelpModal> — new surface: light PopModal + FAQ list + contact footer<FaqItem> — new atom: one question, click to expand. Reusable anywhere we list FAQs<script setup>
import { ref } from 'vue';
import Icon from '@/components/Icon.vue';
import Text from '@/components/Text.vue';
const props = defineProps(['question']);
const open = ref(false);
</script>
<template>
<div class="faq__item" :class="{ 'is-open': open }">
<button class="faq__q" type="button" :aria-expanded="open" @click="open = !open">
<Text type="body/s-heavy" class="faq__q-label">{{ props.question }}</Text>
<Icon name="arrow-caret-down" class="faq__caret" />
</button>
<div class="faq__a" v-show="open"><slot /></div>
</div>
</template>
<style scoped>
.faq__item {
border-bottom: 1px solid var(--c-border-primary);
&.is-open .faq__caret { transform: rotate(180deg); }
}
.faq__q {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 13px 0;
border: none;
background: none;
text-align: left;
cursor: pointer;
}
.faq__q-label { flex: 1 1 auto; }
.faq__caret {
width: 14px;
height: 14px;
color: var(--c-text-tertiary);
flex: none;
transition: transform .18s ease;
}
.faq__a {
padding: 0 24px 14px 0;
font-family: var(--ff-inter);
font-size: 1.3rem;
line-height: 1.6;
color: var(--c-text-secondary);
a {
color: var(--c-button-text-green);
font-weight: 700;
}
}
</style>
<script setup>
import PopModal from '@/components/pop/PopModal.vue';
import PrimaryButton from '@/buttons/PrimaryButton.vue';
import Text from '@/components/Text.vue';
import FaqItem from '@/components/FaqItem.vue';
const props = defineProps({ popped: Boolean });
defineEmits(['update:popped']);
</script>
<template>
<PopModal
class="help-modal"
width="480px"
:popped="props.popped"
@unpop="$emit('update:popped', false)">
<button class="pop-modal__close" type="button" aria-label="Close"
@click="$emit('update:popped', false)">×</button>
<div class="help-modal__head">
<Text tag="h3" type="body/xl" class="pop-modal__title">Help</Text>
<Text type="body/s-med" class="pop-modal__description">Answers to what we get asked most.</Text>
</div>
<div class="faq">
<FaqItem question="Do I need an account?">
No. Search and copy work without one. A free account saves your favorites,
collections and combos so they follow you between devices.
</FaqItem>
<FaqItem question="How do I sign in? I never set a password.">
With your email or with Google. Email sends you a one-time PIN — EmojiCopy
has no passwords to set or forget.
</FaqItem>
<FaqItem question="What's free and what's Pro?">
Free gives you favorites, up to five collections and saved combos. Pro adds
unlimited lists, unlimited combos with recents, more recent emoji and no ads.
<RouterLink to="/pricing">See plans</RouterLink>
</FaqItem>
<FaqItem question="How do I copy more than one emoji at once?">
Click each emoji to stack it in the action bar at the bottom, then hit copy.
That's a combo, and free accounts can save them.
</FaqItem>
<FaqItem question="Why do my emoji look different after I paste them?">
EmojiCopy shows JoyPixels artwork. What you copy is standard Unicode, so every
app draws it in its own style.
</FaqItem>
<FaqItem question="Where's the browser extension?">
The extension and the Mac app arrive with the full launch at the end of August.
The beta is the web app.
</FaqItem>
</div>
<div class="help-modal__footer">
<PrimaryButton variant="green" size="md" label="Contact us"
href="mailto:hello@emojicopy.com" />
<Text type="body/s-reg" class="help-modal__note">We answer at hello@emojicopy.com</Text>
</div>
</PopModal>
</template>
/* The light-scheme override recipe the feedback micro-prompt uses:
components.css keeps the DARK .pop-modal shell for paywall +
onboarding; this scopes a light one to .help-modal only. */
.pop-modal.help-modal {
max-width: 480px;
align-items: stretch;
gap: 18px;
background: var(--c-surface-primary);
color: var(--c-text-primary);
border: 1px solid var(--c-border-primary);
.pop-modal__close {
background: var(--c-button-gray-bg);
box-shadow: inset 0 0 0 1px var(--c-border-primary);
color: var(--c-text-primary);
}
.pop-modal__title {
color: var(--c-text-primary);
text-align: left;
font-size: 2.2rem;
}
.pop-modal__description {
color: var(--c-text-secondary);
text-align: left;
}
}
.help-modal__head {
display: flex;
flex-direction: column;
gap: 6px;
}
.faq {
display: flex;
flex-direction: column;
border-top: 1px solid var(--c-border-primary);
}
.help-modal__footer {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
padding-top: 4px;
}
.help-modal__note {
color: var(--c-text-tertiary);
text-align: center;
}
Answers to what we get asked most.
<PopModal> · ui/components/pop/PopModal.vue — unchanged shell<PrimaryButton> + <TextButton> · ui/buttons/<Icon name="arrow-caret-right" /> · registered in IconIndex.vue<HelpModal> — same surface as A, with link rows instead of an accordion<FaqItem> — still needed, but rendered on the page, not in the modal/help, a router entry and a HelpPage.vue. The real cost of this option<script setup>
import PopModal from '@/components/pop/PopModal.vue';
import PrimaryButton from '@/buttons/PrimaryButton.vue';
import TextButton from '@/buttons/TextButton.vue';
import Icon from '@/components/Icon.vue';
import Text from '@/components/Text.vue';
const props = defineProps({ popped: Boolean });
defineEmits(['update:popped']);
/* The five surfaced here are the top of the same list HelpPage.vue
renders in full — one source, imported by both. */
import { TOP_FAQS } from '@/utils/data/faqs.js';
</script>
<template>
<PopModal
class="help-modal"
width="480px"
:popped="props.popped"
@unpop="$emit('update:popped', false)">
<button class="pop-modal__close" type="button" aria-label="Close"
@click="$emit('update:popped', false)">×</button>
<div class="help-modal__head">
<Text tag="h3" type="body/xl" class="pop-modal__title">Help</Text>
<Text type="body/s-med" class="pop-modal__description">Answers to what we get asked most.</Text>
</div>
<div class="faq-links">
<RouterLink v-for="faq in TOP_FAQS" :key="faq.id" :to="`/help#${faq.id}`">
<Text type="body/s-heavy" class="faq-links__label">{{ faq.question }}</Text>
<Icon name="arrow-caret-right" class="faq-links__arrow" />
</RouterLink>
</div>
<div class="help-modal__more">
<TextButton variant="green" size="md" label="See all FAQs" route="/help" />
</div>
<div class="help-modal__footer">
<PrimaryButton variant="green" size="md" label="Contact us"
href="mailto:hello@emojicopy.com" />
<Text type="body/s-reg" class="help-modal__note">We answer at hello@emojicopy.com</Text>
</div>
</PopModal>
</template>
/* Same light-scheme override as A */
.pop-modal.help-modal {
max-width: 480px;
align-items: stretch;
gap: 18px;
background: var(--c-surface-primary);
color: var(--c-text-primary);
border: 1px solid var(--c-border-primary);
.pop-modal__close {
background: var(--c-button-gray-bg);
box-shadow: inset 0 0 0 1px var(--c-border-primary);
color: var(--c-text-primary);
}
.pop-modal__title {
color: var(--c-text-primary);
text-align: left;
font-size: 2.2rem;
}
.pop-modal__description {
color: var(--c-text-secondary);
text-align: left;
}
}
.help-modal__head {
display: flex;
flex-direction: column;
gap: 6px;
}
/* link rows — the only CSS that differs from A */
.faq-links {
display: flex;
flex-direction: column;
border-top: 1px solid var(--c-border-primary);
a {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 0;
border-bottom: 1px solid var(--c-border-primary);
color: var(--c-text-primary);
text-decoration: none;
&:hover { color: var(--c-button-text-green); }
}
}
.faq-links__label { flex: 1 1 auto; }
.faq-links__arrow {
width: 14px;
height: 14px;
color: var(--c-text-tertiary);
flex: none;
}
.help-modal__more { display: flex; justify-content: flex-start; }
.help-modal__footer {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
padding-top: 4px;
}
.help-modal__note {
color: var(--c-text-tertiary);
text-align: center;
}
Answers to what we get asked most.
<PopModal> · ui/components/pop/PopModal.vue — unchanged shell<PrimaryButton> + <TextButton> · ui/buttons/<Icon name="arrow-caret-down" /> · registered in IconIndex.vue<HelpModal> — A's surface plus a "See all FAQs" link<FaqItem> — same atom as A, reused on the page/help, same cost as B<script setup>
import PopModal from '@/components/pop/PopModal.vue';
import PrimaryButton from '@/buttons/PrimaryButton.vue';
import TextButton from '@/buttons/TextButton.vue';
import Text from '@/components/Text.vue';
import FaqItem from '@/components/FaqItem.vue';
const props = defineProps({ popped: Boolean });
defineEmits(['update:popped']);
/* One source for both surfaces: the modal renders the first three,
HelpPage.vue renders all of them. */
import { FAQS } from '@/utils/data/faqs.js';
const topThree = FAQS.slice(0, 3);
</script>
<template>
<PopModal
class="help-modal"
width="480px"
:popped="props.popped"
@unpop="$emit('update:popped', false)">
<button class="pop-modal__close" type="button" aria-label="Close"
@click="$emit('update:popped', false)">×</button>
<div class="help-modal__head">
<Text tag="h3" type="body/xl" class="pop-modal__title">Help</Text>
<Text type="body/s-med" class="pop-modal__description">Answers to what we get asked most.</Text>
</div>
<div class="faq">
<FaqItem v-for="faq in topThree" :key="faq.id" :question="faq.question">
<span v-html="faq.answer" />
</FaqItem>
</div>
<div class="help-modal__more">
<TextButton variant="green" size="md" label="See all FAQs" route="/help" />
</div>
<div class="help-modal__footer">
<PrimaryButton variant="green" size="md" label="Contact us"
href="mailto:hello@emojicopy.com" />
<Text type="body/s-reg" class="help-modal__note">We answer at hello@emojicopy.com</Text>
</div>
</PopModal>
</template>
<script setup>
import { ref } from 'vue';
import Icon from '@/components/Icon.vue';
import Text from '@/components/Text.vue';
const props = defineProps(['question']);
const open = ref(false);
</script>
<template>
<div class="faq__item" :class="{ 'is-open': open }">
<button class="faq__q" type="button" :aria-expanded="open" @click="open = !open">
<Text type="body/s-heavy" class="faq__q-label">{{ props.question }}</Text>
<Icon name="arrow-caret-down" class="faq__caret" />
</button>
<div class="faq__a" v-show="open"><slot /></div>
</div>
</template>
<style scoped>
.faq__item {
border-bottom: 1px solid var(--c-border-primary);
&.is-open .faq__caret { transform: rotate(180deg); }
}
.faq__q {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 13px 0;
border: none;
background: none;
text-align: left;
cursor: pointer;
}
.faq__q-label { flex: 1 1 auto; }
.faq__caret {
width: 14px;
height: 14px;
color: var(--c-text-tertiary);
flex: none;
transition: transform .18s ease;
}
.faq__a {
padding: 0 24px 14px 0;
font-family: var(--ff-inter);
font-size: 1.3rem;
line-height: 1.6;
color: var(--c-text-secondary);
a {
color: var(--c-button-text-green);
font-weight: 700;
}
}
</style>
/* Same light-scheme override as A, plus the "See all" row */
.pop-modal.help-modal {
max-width: 480px;
align-items: stretch;
gap: 18px;
background: var(--c-surface-primary);
color: var(--c-text-primary);
border: 1px solid var(--c-border-primary);
.pop-modal__close {
background: var(--c-button-gray-bg);
box-shadow: inset 0 0 0 1px var(--c-border-primary);
color: var(--c-text-primary);
}
.pop-modal__title {
color: var(--c-text-primary);
text-align: left;
font-size: 2.2rem;
}
.pop-modal__description {
color: var(--c-text-secondary);
text-align: left;
}
}
.help-modal__head {
display: flex;
flex-direction: column;
gap: 6px;
}
.faq {
display: flex;
flex-direction: column;
border-top: 1px solid var(--c-border-primary);
}
.help-modal__more { display: flex; justify-content: flex-start; }
.help-modal__footer {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
padding-top: 4px;
}
.help-modal__note {
color: var(--c-text-tertiary);
text-align: center;
}
Answers to what we get asked most. Still stuck? Email hello@emojicopy.com.
<Text> · ui/components/Text.vue<FaqItem> — the same atom the modal uses, verbatim (source in the A and C drawers)HelpPage.vue + a /help router entryui/utils/data/faqs.js — one ordered list, read by both the page and the modal/* Ordered: the modal takes the first N, the page takes all.
Answers verified against PLANS-PRICING.md 2026-07-24 — anything
about plans, sign-in or launch dates changes HERE first. */
export const FAQS = [
{
id: 'account',
question: 'Do I need an account?',
answer: 'No. Search and copy work without one. A free account saves your favorites, '
+ 'collections and combos so they follow you between devices.',
},
{
id: 'sign-in',
question: 'How do I sign in? I never set a password.',
answer: 'With your email or with Google. Email sends you a one-time PIN — EmojiCopy '
+ 'has no passwords to set or forget.',
},
{
id: 'plans',
question: "What's free and what's Pro?",
answer: 'Free gives you favorites, up to five collections and saved combos. Pro adds '
+ 'unlimited lists, unlimited combos with recents, more recent emoji and no ads. '
+ '<a href="/pricing">See plans</a>',
},
{
id: 'combos',
question: 'How do I copy more than one emoji at once?',
answer: 'Click each emoji to stack it in the action bar at the bottom, then hit copy. '
+ "That's a combo, and free accounts can save them.",
},
{
id: 'artwork',
question: 'Why do my emoji look different after I paste them?',
answer: 'EmojiCopy shows JoyPixels artwork. What you copy is standard Unicode, so every '
+ 'app draws it in its own style.',
},
{
id: 'extension',
question: "Where's the browser extension?",
answer: 'The extension and the Mac app arrive with the full launch at the end of August. '
+ 'The beta is the web app.',
},
{
id: 'old-site',
question: 'Is the old EmojiCopy going away?',
answer: 'No. emojicopy.com keeps working while the beta runs.',
},
{
id: 'feedback',
question: 'How do I report a bug or ask for a feature?',
answer: 'Use the feedback button — it goes straight to the team, and beta feedback '
+ 'earns a $5 credit toward Pro.',
},
];
export const TOP_FAQS = FAQS.slice(0, 5);
<script setup>
import { onMounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import Text from '@/components/Text.vue';
import FaqItem from '@/components/FaqItem.vue';
import { FAQS } from '@/utils/data/faqs.js';
const route = useRoute();
/* Deep link: /help#combos scrolls to that question and opens it. */
onMounted(async () => {
if (!route.hash) return;
await nextTick();
const el = document.getElementById(route.hash.slice(1));
if (!el) return;
el.querySelector('.faq__q')?.click();
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
</script>
<template>
<div class="help-page">
<Text tag="h1" type="body/xxl">Help</Text>
<Text type="body/s-med" class="help-page__lede">
Answers to what we get asked most. Still stuck? Email
<a href="mailto:hello@emojicopy.com">hello@emojicopy.com</a>.
</Text>
<div class="faq">
<FaqItem v-for="faq in FAQS" :id="faq.id" :key="faq.id" :question="faq.question">
<span v-html="faq.answer" />
</FaqItem>
</div>
</div>
</template>
<style scoped>
.help-page {
width: 100%;
max-width: 620px;
margin: 0 auto;
padding: var(--sp-6);
}
.help-page__lede {
display: block;
color: var(--c-text-secondary);
margin-bottom: var(--sp-5);
a {
color: var(--c-button-text-green);
font-weight: 700;
}
}
.faq {
display: flex;
flex-direction: column;
border-top: 1px solid var(--c-border-primary);
}
</style>
{
path: '/help',
name: 'help',
component: () => import('@/views/HelpPage.vue'),
}