This is possible using JSLink/CSR client-side validation. That is, if you are on a version of SharePoint that still has JSLink/CSR available.
From my understanding, SP Online modern experience does not use the JSLink/CSR system from older versions. I am unsure if SP Online classic experience still has that (can someone who knows confirm in a comment?). SP 2013 / 2016 on-prem definitely has JSLink/CSR available to tap into (and I believe 2019 as well? Again, not sure on that one...)
Here's some example code (tested on 2013):
var MyNamespace = MyNamespace || {}; MyNamespace.PreviousFieldValue = null; MyNamespace.MyFieldValidator = function myFieldValidator () { MyNamespace.MyFieldValidator.prototype.Validate = function (fieldValue) { var isError = false; var errorMessage = ''; // only validate if the field has a value if (fieldValue) { // assume that we do need to check if it is a past date var shouldCheckIfPastDate = true; var enteredDate = new Date(fieldValue); // if there was a previous value, check to see if it has changed if (MyNamespace.PreviousFieldValue !== null) { var previousDate = new Date(MyNamespace.PreviousFieldValue); // if the date has not changed, don't check if it's a past date // Date object can't do equality comparison natively, so we have to use getTime() if (enteredDate.getTime() === previousDate.getTime()) { shouldCheckIfPastDate = false; } } if (shouldCheckIfPastDate) { // get todays date and zero out the time so we are only comparing the date var today = new Date(); today.setHours(0, 0, 0, 0); // Date object can handle greater than/less than comparison natively if (enteredDate < today) { isError = true; errorMessage = 'You cannot enter a past date.'; } } } return new SPClientForms.ClientValidation.ValidationResult(isError, errorMessage); } } MyNamespace.MyFieldFieldOverride = { edit: function edit (ctx) { // if it's not blank, store the current value of the field to check against later if (ctx.CurrentFieldValue) { MyNamespace.PreviousFieldValue = ctx.CurrentFieldValue; } // get the form context and create a new validator set var formCtx = SPClientTemplates.Utility.GetFormContextForCurrentField(ctx); var fieldValidators = new SPClientForms.ClientValidation.ValidatorSet(); // if the field has been marked as required, register the default required field validator if (formCtx.fieldSchema.Required) { fieldValidators.RegisterValidator(new SPClientForms.ClientValidation.RequiredValidator()); } // register our custom validator fieldValidators.RegisterValidator(new MyNamespace.MyFieldValidator()); // add the error callback and register the validator set with the form context formCtx.registerValidationErrorCallback(formCtx.fieldName, MyNamespace.MyFieldFieldOverride.onError); formCtx.registerClientValidator(formCtx.fieldName, fieldValidators); // render the default form control for a date column var returnHtml = SPFieldDateTime_Edit(ctx); // add a spot for our custom error message returnHtml += "<span id='MyFieldCustomError' class='ms-formvalidation ms-csrformvalidation'></span>"; return returnHtml; }, onError: function onError (error) { document.getElementById('MyFieldCustomError').innerHTML = "<span role='alert'>" + error.errorMessage + "</span>"; }, render: function render () { SPClientTemplates.TemplateManager.RegisterTemplateOverrides({ Templates: { Fields: { YourDateField: { 'NewForm': MyNamespace.MyFieldFieldOverride.edit, 'EditForm': MyNamespace.MyFieldFieldOverride.edit } } } }); } } // handle MDS-enabled or MDS-disabled situations RegisterModuleInit(SPClientRenderer.ReplaceUrlTokens('~site/path/to/MyFieldValidator.js'), MyNamespace.MyFieldFieldOverride.render); MyNamespace.MyFieldFieldOverride.render();