diff --git a/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts b/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts
index 895814f133..275d965747 100644
--- a/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts
+++ b/apps/web/app/api/v1/client/[environmentId]/environment/lib/survey.ts
@@ -61,6 +61,7 @@ export const getSurveysForEnvironmentState = reactCache(
displayLimit: true,
displayOption: true,
hiddenFields: true,
+ isBackButtonHidden: true,
triggers: {
select: {
actionClass: {
diff --git a/apps/web/app/lib/templates.ts b/apps/web/app/lib/templates.ts
index d4ae069ea2..f8042c6ad5 100644
--- a/apps/web/app/lib/templates.ts
+++ b/apps/web/app/lib/templates.ts
@@ -7064,5 +7064,6 @@ export const previewSurvey = (projectName: string, t: TFnType) => {
triggers: [],
showLanguageSwitch: false,
followUps: [],
+ isBackButtonHidden: false,
} as TSurvey;
};
diff --git a/apps/web/modules/survey/editor/components/response-options-card.tsx b/apps/web/modules/survey/editor/components/response-options-card.tsx
index 57fb9e4bce..08b29056c8 100644
--- a/apps/web/modules/survey/editor/components/response-options-card.tsx
+++ b/apps/web/modules/survey/editor/components/response-options-card.tsx
@@ -205,6 +205,10 @@ export const ResponseOptionsCard = ({
}
};
+ const handleHideBackButtonToggle = () => {
+ setLocalSurvey({ ...localSurvey, isBackButtonHidden: !localSurvey.isBackButtonHidden });
+ };
+
useEffect(() => {
if (!!localSurvey.surveyClosedMessage) {
setSurveyClosedMessage({
@@ -515,6 +519,13 @@ export const ResponseOptionsCard = ({
>
)}
+
diff --git a/apps/web/modules/survey/lib/survey.ts b/apps/web/modules/survey/lib/survey.ts
index ba35bc36bf..af624428db 100644
--- a/apps/web/modules/survey/lib/survey.ts
+++ b/apps/web/modules/survey/lib/survey.ts
@@ -41,6 +41,7 @@ export const selectSurvey = {
pin: true,
resultShareKey: true,
showLanguageSwitch: true,
+ isBackButtonHidden: true,
languages: {
select: {
default: true,
diff --git a/apps/web/modules/survey/templates/lib/minimal-survey.ts b/apps/web/modules/survey/templates/lib/minimal-survey.ts
index 91d9cd0ebf..968a517b6e 100644
--- a/apps/web/modules/survey/templates/lib/minimal-survey.ts
+++ b/apps/web/modules/survey/templates/lib/minimal-survey.ts
@@ -41,4 +41,5 @@ export const getMinimalSurvey = (t: TFnType): TSurvey => ({
isSingleResponsePerEmailEnabled: false,
variables: [],
followUps: [],
+ isBackButtonHidden: false,
});
diff --git a/docs/images/xm-and-surveys/surveys/general-features/hide-back-button/hide-back-button.webp b/docs/images/xm-and-surveys/surveys/general-features/hide-back-button/hide-back-button.webp
new file mode 100644
index 0000000000..88c1a8bca3
Binary files /dev/null and b/docs/images/xm-and-surveys/surveys/general-features/hide-back-button/hide-back-button.webp differ
diff --git a/docs/mint.json b/docs/mint.json
index 057ec28620..9187d59c7d 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -47,7 +47,8 @@
"xm-and-surveys/surveys/general-features/shareable-dashboards",
"xm-and-surveys/surveys/general-features/schedule-start-end-dates",
"xm-and-surveys/surveys/general-features/metadata",
- "xm-and-surveys/surveys/general-features/variables"
+ "xm-and-surveys/surveys/general-features/variables",
+ "xm-and-surveys/surveys/general-features/hide-back-button"
]
},
{
diff --git a/docs/xm-and-surveys/surveys/general-features/hide-back-button.mdx b/docs/xm-and-surveys/surveys/general-features/hide-back-button.mdx
new file mode 100644
index 0000000000..dcdb492f8c
--- /dev/null
+++ b/docs/xm-and-surveys/surveys/general-features/hide-back-button.mdx
@@ -0,0 +1,11 @@
+---
+title: Hide Back Button
+description: Learn how to hide the back button in surveys.
+icon: arrow-left
+---
+
+Surveys display a back button by default. If you want to prevent respondents from returning to previous questions, you'll need to disable this feature explicitly.
+
+To disable the back button, navigate to the survey settings and select the Response options tab.
+
+
diff --git a/packages/database/migration/20250226080646_set_response_updated_at_default/migration.sql b/packages/database/migration/20250226080646_set_response_updated_at_default/migration.sql
new file mode 100644
index 0000000000..e2d04b4ec2
--- /dev/null
+++ b/packages/database/migration/20250226080646_set_response_updated_at_default/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Response" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
diff --git a/packages/database/migration/20250226080718_add_is_back_button_hidden_to_survey/migration.sql b/packages/database/migration/20250226080718_add_is_back_button_hidden_to_survey/migration.sql
new file mode 100644
index 0000000000..bdf64cfc4d
--- /dev/null
+++ b/packages/database/migration/20250226080718_add_is_back_button_hidden_to_survey/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Survey" ADD COLUMN "isBackButtonHidden" BOOLEAN NOT NULL DEFAULT false;
diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma
index 904274d903..d9781e2809 100644
--- a/packages/database/schema.prisma
+++ b/packages/database/schema.prisma
@@ -410,6 +410,7 @@ model Survey {
verifyEmail Json? // deprecated
isVerifyEmailEnabled Boolean @default(false)
isSingleResponsePerEmailEnabled Boolean @default(false)
+ isBackButtonHidden Boolean @default(false)
pin String?
resultShareKey String? @unique
displayPercentage Decimal?
diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts
index 6aa137c92f..d9da25a217 100644
--- a/packages/lib/survey/service.ts
+++ b/packages/lib/survey/service.ts
@@ -69,6 +69,7 @@ export const selectSurvey = {
autoComplete: true,
isVerifyEmailEnabled: true,
isSingleResponsePerEmailEnabled: true,
+ isBackButtonHidden: true,
redirectUrl: true,
projectOverwrites: true,
styling: true,
diff --git a/packages/lib/survey/tests/__mock__/survey.mock.ts b/packages/lib/survey/tests/__mock__/survey.mock.ts
index 0031ebdced..cca45f9d37 100644
--- a/packages/lib/survey/tests/__mock__/survey.mock.ts
+++ b/packages/lib/survey/tests/__mock__/survey.mock.ts
@@ -185,6 +185,7 @@ const baseSurveyProperties = {
displayLimit: 3,
welcomeCard: mockWelcomeCard,
questions: [mockQuestion],
+ isBackButtonHidden: false,
endings: [
{
id: "umyknohldc7w26ocjdhaa62c",
diff --git a/packages/surveys/src/components/general/question-conditional.tsx b/packages/surveys/src/components/general/question-conditional.tsx
index 4c1fec2821..b67be6f3c7 100644
--- a/packages/surveys/src/components/general/question-conditional.tsx
+++ b/packages/surveys/src/components/general/question-conditional.tsx
@@ -41,6 +41,7 @@ interface QuestionConditionalProps {
surveyId: string;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function QuestionConditional({
@@ -60,6 +61,7 @@ export function QuestionConditional({
onFileUpload,
autoFocusEnabled,
currentQuestionId,
+ isBackButtonHidden,
}: QuestionConditionalProps) {
const getResponseValueForRankingQuestion = (
value: string[],
@@ -93,6 +95,7 @@ export function QuestionConditional({
setTtc={setTtc}
autoFocusEnabled={autoFocusEnabled}
currentQuestionId={currentQuestionId}
+ isBackButtonHidden={isBackButtonHidden}
/>
) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ? (
) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ? (
) : question.type === TSurveyQuestionTypeEnum.NPS ? (
) : question.type === TSurveyQuestionTypeEnum.CTA ? (
) : question.type === TSurveyQuestionTypeEnum.Rating ? (
) : question.type === TSurveyQuestionTypeEnum.Consent ? (
) : question.type === TSurveyQuestionTypeEnum.Date ? (
) : question.type === TSurveyQuestionTypeEnum.PictureSelection ? (
) : question.type === TSurveyQuestionTypeEnum.FileUpload ? (
) : question.type === TSurveyQuestionTypeEnum.Cal ? (
) : question.type === TSurveyQuestionTypeEnum.Matrix ? (
) : question.type === TSurveyQuestionTypeEnum.Address ? (
) : question.type === TSurveyQuestionTypeEnum.Ranking ? (
) : question.type === TSurveyQuestionTypeEnum.ContactInfo ? (
) : null;
}
diff --git a/packages/surveys/src/components/general/survey.tsx b/packages/surveys/src/components/general/survey.tsx
index 1b4982d9bf..2ecda6ba2c 100644
--- a/packages/surveys/src/components/general/survey.tsx
+++ b/packages/surveys/src/components/general/survey.tsx
@@ -399,6 +399,7 @@ export function Survey({
languageCode={selectedLanguage}
autoFocusEnabled={autoFocusEnabled}
currentQuestionId={questionId}
+ isBackButtonHidden={localSurvey.isBackButtonHidden}
/>
)
);
diff --git a/packages/surveys/src/components/questions/address-question.tsx b/packages/surveys/src/components/questions/address-question.tsx
index bd8c51aa0a..388e3f39f3 100644
--- a/packages/surveys/src/components/questions/address-question.tsx
+++ b/packages/surveys/src/components/questions/address-question.tsx
@@ -25,6 +25,7 @@ interface AddressQuestionProps {
setTtc: (ttc: TResponseTtc) => void;
currentQuestionId: TSurveyQuestionId;
autoFocusEnabled: boolean;
+ isBackButtonHidden: boolean;
}
export function AddressQuestion({
@@ -40,6 +41,7 @@ export function AddressQuestion({
setTtc,
currentQuestionId,
autoFocusEnabled,
+ isBackButtonHidden,
}: AddressQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -179,7 +181,7 @@ export function AddressQuestion({
isLastQuestion={isLastQuestion}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function CalQuestion({
@@ -38,6 +39,7 @@ export function CalQuestion({
ttc,
setTtc,
currentQuestionId,
+ isBackButtonHidden,
}: CalQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -95,7 +97,7 @@ export function CalQuestion({
/>
)}
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
{
diff --git a/packages/surveys/src/components/questions/consent-question.tsx b/packages/surveys/src/components/questions/consent-question.tsx
index 4576978ae0..e5b1a5a822 100644
--- a/packages/surveys/src/components/questions/consent-question.tsx
+++ b/packages/surveys/src/components/questions/consent-question.tsx
@@ -23,6 +23,7 @@ interface ConsentQuestionProps {
setTtc: (ttc: TResponseTtc) => void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function ConsentQuestion({
@@ -38,6 +39,7 @@ export function ConsentQuestion({
setTtc,
currentQuestionId,
autoFocusEnabled,
+ isBackButtonHidden,
}: ConsentQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -126,7 +128,7 @@ export function ConsentQuestion({
isLastQuestion={isLastQuestion}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
currentQuestionId: TSurveyQuestionId;
autoFocusEnabled: boolean;
+ isBackButtonHidden: boolean;
}
export function ContactInfoQuestion({
@@ -40,6 +41,7 @@ export function ContactInfoQuestion({
setTtc,
currentQuestionId,
autoFocusEnabled,
+ isBackButtonHidden,
}: ContactInfoQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -181,7 +183,7 @@ export function ContactInfoQuestion({
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
isLastQuestion={isLastQuestion}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function CTAQuestion({
@@ -37,6 +38,7 @@ export function CTAQuestion({
setTtc,
autoFocusEnabled,
currentQuestionId,
+ isBackButtonHidden,
}: CTAQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -92,7 +94,7 @@ export function CTAQuestion({
)}
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
function CalendarIcon() {
@@ -91,6 +92,7 @@ export function DateQuestion({
setTtc,
ttc,
currentQuestionId,
+ isBackButtonHidden,
}: DateQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const [errorMessage, setErrorMessage] = useState("");
@@ -272,7 +274,7 @@ export function DateQuestion({
isLastQuestion={isLastQuestion}
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function FileUploadQuestion({
@@ -44,6 +45,7 @@ export function FileUploadQuestion({
ttc,
setTtc,
currentQuestionId,
+ isBackButtonHidden,
}: FileUploadQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -110,7 +112,7 @@ export function FileUploadQuestion({
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
isLastQuestion={isLastQuestion}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function MatrixQuestion({
@@ -38,6 +39,7 @@ export function MatrixQuestion({
ttc,
setTtc,
currentQuestionId,
+ isBackButtonHidden,
}: MatrixQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -210,7 +212,7 @@ export function MatrixQuestion({
isLastQuestion={isLastQuestion}
tabIndex={isCurrent ? 0 : -1}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function MultipleChoiceMultiQuestion({
@@ -39,6 +40,7 @@ export function MultipleChoiceMultiQuestion({
setTtc,
autoFocusEnabled,
currentQuestionId,
+ isBackButtonHidden,
}: MultipleChoiceMultiProps) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -293,7 +295,7 @@ export function MultipleChoiceMultiQuestion({
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
isLastQuestion={isLastQuestion}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function MultipleChoiceSingleQuestion({
@@ -39,6 +40,7 @@ export function MultipleChoiceSingleQuestion({
setTtc,
autoFocusEnabled,
currentQuestionId,
+ isBackButtonHidden,
}: MultipleChoiceSingleProps) {
const [startTime, setStartTime] = useState(performance.now());
const [otherSelected, setOtherSelected] = useState(false);
@@ -250,7 +252,7 @@ export function MultipleChoiceSingleQuestion({
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
isLastQuestion={isLastQuestion}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function NPSQuestion({
@@ -38,6 +39,7 @@ export function NPSQuestion({
ttc,
setTtc,
currentQuestionId,
+ isBackButtonHidden,
}: NPSQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const [hoveredNumber, setHoveredNumber] = useState(-1);
@@ -153,14 +155,16 @@ export function NPSQuestion({
- {!question.required && (
+ {question.required ? (
+ <>>
+ ) : (
)}
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function OpenTextQuestion({
@@ -40,6 +41,7 @@ export function OpenTextQuestion({
setTtc,
autoFocusEnabled,
currentQuestionId,
+ isBackButtonHidden,
}: OpenTextQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const [currentLength, setCurrentLength] = useState(value.length || 0);
@@ -161,7 +163,7 @@ export function OpenTextQuestion({
isLastQuestion={isLastQuestion}
onClick={() => {}}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function PictureSelectionQuestion({
@@ -39,6 +40,7 @@ export function PictureSelectionQuestion({
ttc,
setTtc,
currentQuestionId,
+ isBackButtonHidden,
}: PictureSelectionProps) {
const [startTime, setStartTime] = useState(performance.now());
const isMediaAvailable = question.imageUrl || question.videoUrl;
@@ -209,7 +211,7 @@ export function PictureSelectionQuestion({
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
isLastQuestion={isLastQuestion}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function RankingQuestion({
@@ -44,6 +45,7 @@ export function RankingQuestion({
setTtc,
autoFocusEnabled,
currentQuestionId,
+ isBackButtonHidden,
}: RankingQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const isCurrent = question.id === currentQuestionId;
@@ -272,7 +274,7 @@ export function RankingQuestion({
buttonLabel={getLocalizedValue(question.buttonLabel, languageCode)}
isLastQuestion={isLastQuestion}
/>
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
void;
autoFocusEnabled: boolean;
currentQuestionId: TSurveyQuestionId;
+ isBackButtonHidden: boolean;
}
export function RatingQuestion({
@@ -51,6 +52,7 @@ export function RatingQuestion({
ttc,
setTtc,
currentQuestionId,
+ isBackButtonHidden,
}: RatingQuestionProps) {
const [hoveredNumber, setHoveredNumber] = useState(0);
const [startTime, setStartTime] = useState(performance.now());
@@ -259,7 +261,9 @@ export function RatingQuestion({
- {!question.required && (
+ {question.required ? (
+ <>>
+ ) : (
)}
- {!isFirstQuestion && (
+ {!isFirstQuestion && !isBackButtonHidden && (
null);
diff --git a/packages/types/surveys/types.ts b/packages/types/surveys/types.ts
index e4bbc3c553..a541555266 100644
--- a/packages/types/surveys/types.ts
+++ b/packages/types/surveys/types.ts
@@ -868,13 +868,14 @@ export const ZSurvey = z
singleUse: ZSurveySingleUse.nullable(),
isVerifyEmailEnabled: z.boolean(),
isSingleResponsePerEmailEnabled: z.boolean(),
+ isBackButtonHidden: z.boolean(),
pin: z.string().min(4, { message: "PIN must be a four digit number" }).nullish(),
resultShareKey: z.string().nullable(),
displayPercentage: z.number().min(0.01).max(100).nullable(),
languages: z.array(ZSurveyLanguage),
})
.superRefine((survey, ctx) => {
- const { questions, languages, welcomeCard, endings } = survey;
+ const { questions, languages, welcomeCard, endings, isBackButtonHidden } = survey;
let multiLangIssue: z.IssueData | null;
@@ -943,7 +944,9 @@ export const ZSurvey = z
];
const fieldsToValidate =
- questionIndex === 0 ? initialFieldsToValidate : [...initialFieldsToValidate, "backButtonLabel"];
+ questionIndex === 0 || isBackButtonHidden
+ ? initialFieldsToValidate
+ : [...initialFieldsToValidate, "backButtonLabel"];
for (const field of fieldsToValidate) {
// Skip label validation for consent questions as its called checkbox label