Unlocking Creative Potential With Custom CSS Gradients in Sitecore
Building a site and creating bold and feature rich components is something we all love to do. Sometimes I feel like the backend of Sitecore, like Content Editor, gets missed in the opportunity of what’s possible. It’s one of the reasons I wanted to see what else could be done when it comes to custom fields in Sitecore. When we build components, a lot of the time if there is a need for a graphic we offer the ability to insert an image. But what if we could offer clients something truly unique and enable them to build their own background using a nice CSS gradient.
Below you’ll see in action a custom field, made up of a number of other purely visual fields including multiple color pickers, and a range input, used to generate the CSS required to show a linear gradient. I also set it up so you could see what generated and thus know what to expect.
When the CSS is generated it is stored in essentially a Single-Line Text
field that can be accessed from a front-end component.
Curious how to do it? Let’s break it down.
Create New Custom Field in Core Database
First thing to do is to create the new custom field in the core
database. So, while in Content Editor, switch over to the core
database and then navigate to /system/Field Types
and create a new folder called Custom Types
.
Next, navigate to /sitecore/system/Field types/Simple Types/Single-Line Text
and clone that item to create a new Field Type
in that folder you created, let’s call it LinearGradient
.
Let’s fill in the fields on the right.
Assembly
- this is the dll file where your field type class will be foundClass
- the name of your class. In our case we called itLinearGradientField
.Control
- this is how the field will be referenced via our configuration file.
Setup Sitecore Patch File
While in Visual Studio, create a Sitecore patch file with the following configuration. You will need to change it so your assembly and class file matches.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<controlSources>
<source mode="on" namespace="XmCloudSXAStarter.Fields" assembly="XmCloudSXAStarter" prefix="custom" singleInstance="true" />
</controlSources>
<fieldTypes>
<fieldType name="LinearGradientField" type="XmCloudSXAStarter.Fields.LinearGradientField, XmCloudSXAStarter" />
</fieldTypes>
</sitecore>
</configuration>
Because all we’re saving for the field is essentially a string
thus we will be able to base our field on the Single-Line Text
field. The other input fields, like the color pickers and rotation selector are merely there for improved user experience.
Setup Class for Field
Let’s now create the .Next class file to render the field as we need. We’ll start with the following.
using System.Web.UI;
namespace XmCloudSXAStarter.Fields
{
public class LinearGradientField : Sitecore.Shell.Applications.ContentEditor.Text
{
protected override void DoRender(HtmlTextWriter output)
{
}
}
}
The important thing to note here is that we’re utilizing Sitecore.Shell.Applications.ContentEditor.Text
as our base class. This is what Single-Line Text
utilizes so essentially we’ll be able to use all the same functionality we just will alter the DoRender
function to change the visual output.
Breaking Down the DoRender Function
So when it comes to the actual DoRender
function it can be a bit crazy and there are certainly ways of improving it. Such as where CSS is pulled from, where JS might be pulled from, but I’ve kept it all here for simplicity.
Before I show the entire function, I’m just going to breakdown the individual bits. First let’s have a look at the HTML which is broken into a couple parts. We have our gradientdisplay
which will be the visualizer for what the linear gradient looks like.
<div id=""gradientdisplay"" style=""width:100%;height:200px;border-radius:16px;margin-bottom:16px;background:linear-gradient(0deg, #fff, #000);""></div>";
Then we have all the fields that allow us to choose the colors and angle. And if you look at
<div id=""containerColor1"">
<div id=""colorColumn1"">
<label for=""colorPicker1"">Color #1:</label>
<input type=""color"" id=""colorPicker1"" name=""color1"" value=""#ffffff"">
<br/><label for=""selectedColor1"">Selected first color:</label>
<input type=""text"" id=""selectedColor1"" name=""selectedColor1"" value=""#ffffff"" readonly>
</div>
<div id=""colorColumn2"">
<label for=""colorPicker2"">Color #2:</label>
<input type=""color"" id=""colorPicker2"" name=""color2"" value=""#000000"">
<br/>
<label for=""selectedColor2"">Selected second color:</label>
<input type=""text"" id=""selectedColor2"" name=""selectedColor2"" value=""#000000"" readonly>
</div>
<div class=""clockColumn"">
<div class=""innerClockColumn"">
<div class=""clock-container"">
<div class=""clock-arm"" id=""clockArm""></div>
</div>
<div>
<input type=""range"" min=""0"" max=""360"" value=""0"" class=""degree-input"" id=""degreeInput"" />
<p style=""text-align: center;"">Selected Degree: <span id=""degreeDisplay"">0</span>°</p>
</div>
</div>
</div>
</div>
And of course we need the field for which the resulting css that’s generated needs to be stored.
<div style=""padding-top:16px;"">
<label for=""{this.ID}"">Chosen Gradient:</label>
<input id=""{this.ID}"" class=""scContentControl"" type=""text"" value=""{Value}"" />
</div>
Next up is we have some CSS to help lay out the fields appropriately. Note that the double brackets are because all the code like the HTML, CSS and JavaScript are written within a verbatim interpolated string literal
. And thus for formatting purposes we need to use double brackets otherwise it will be expecting a token variable.
<style>
#containerColor1 {{
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}}
.innerClockColumn {{
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
}}
.clock-container {{
position: relative;
width: 60px;
height: 60px;
border: 1px solid #000;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}}
.clock-arm {{
position: absolute;
width: 50%;
height: 2px;
left: 0px;
background-color: red;
transform-origin: 100%;
transform: rotate(0deg);
}}
.degree-input {{
display: block;
margin: 20px auto;
}}
</style>
Lastly we have our JavaScript. There is one important piece to this. Due to the fact we’re saving essentially a Single-Line Text
, each time we load an item that displays a LinearGradient field, we have to determine the colors and angle all from that single text line. Thus, we need to utilize Regex to identify the three values. This gets performed immediately after the form fields are displayed. Following that we have our event listeners for the both the color pickers as well as the input range field that captures the angle.
<script>
var regex = /linear-gradient\((\d{{1,3}}deg),\s*(#[0-9a-fA-F]{{3,6}}),\s*(#[0-9a-fA-F]{{3,6}})\)/
const degreeInput = document.getElementById('degreeInput');
const clockArm = document.getElementById('clockArm');
const degreeDisplay = document.getElementById('degreeDisplay');
const colorpicker1 = document.getElementById('colorpicker1');
const colorpicker2 = document.getElementById('colorpicker2');
const selectedColor1 = document.getElementById('selectedColor1');
const selectedColor2 = document.getElementById('selectedColor2');
const gradientdisplay = document.getElementById('gradientdisplay');
const lgString = document.getElementById('{this.ID}').value;
var matches = lgString.match(regex);
if (matches) {{
var degree = matches[1];
var color1 = matches[2];
var color2 = matches[3];
colorpicker1.value = color1;
selectedColor1.value = color1;
colorpicker2.value = color2;
selectedColor2.value = color2;
degree = degree.replace('deg', '');
degreeInput.value = degree;
clockArm.style.transform = `rotate(${{degree}}deg)`;
gradientdisplay.style.background = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
}}
colorpicker1.addEventListener('input', function() {{
selectedColor1.value = colorpicker1.value;
gradientdisplay.style.background = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
document.getElementById('{this.ID}').value = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
}});
colorpicker2.addEventListener('input', function() {{
selectedColor2.value = colorpicker2.value;
gradientdisplay.style.background = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
document.getElementById('{this.ID}').value = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
}});
degreeInput.addEventListener('input', function() {{
const degree = degreeInput.value;
clockArm.style.transform = `rotate(${{degree}}deg)`;
degreeDisplay.textContent = degree;
gradientdisplay.style.background = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
document.getElementById('{this.ID}').value = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
}});
</script>
Completed DoRender Function
protected override void DoRender(HtmlTextWriter output)
{
this.ServerProperties["ID"] = this.ID;
string loader = $@"<div id=""{this.ID}-linergradientfield"">";
output.Write(loader);
string gradient = $@"
<div id=""gradientdisplay"" style=""width:100%;height:200px;border-radius:16px;margin-bottom:16px;background:linear-gradient(0deg, #fff, #000);""></div>";
output.Write(gradient);
string fields = $@"
<div id=""containerColor1"">
<div id=""colorColumn1"">
<label for=""colorPicker1"">Color #1:</label>
<input type=""color"" id=""colorPicker1"" name=""color1"" value=""#ffffff"">
<br/><label for=""selectedColor1"">Selected first color:</label>
<input type=""text"" id=""selectedColor1"" name=""selectedColor1"" value=""#ffffff"" readonly>
</div>
<div id=""colorColumn2"">
<label for=""colorPicker2"">Color #2:</label>
<input type=""color"" id=""colorPicker2"" name=""color2"" value=""#000000"">
<br/>
<label for=""selectedColor2"">Selected second color:</label>
<input type=""text"" id=""selectedColor2"" name=""selectedColor2"" value=""#000000"" readonly>
</div>
<div class=""clockColumn"">
<div class=""innerClockColumn"">
<div class=""clock-container"">
<div class=""clock-arm"" id=""clockArm""></div>
</div>
<div>
<input type=""range"" min=""0"" max=""360"" value=""0"" class=""degree-input"" id=""degreeInput"" />
<p style=""text-align: center;"">Selected Degree: <span id=""degreeDisplay"">0</span>°</p>
</div>
</div>
</div>
</div>
<style>
#containerColor1 {{
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}}
.innerClockColumn {{
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
}}
.clock-container {{
position: relative;
width: 60px;
height: 60px;
border: 1px solid #000;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}}
.clock-arm {{
position: absolute;
width: 50%;
height: 2px;
left: 0px;
background-color: red;
transform-origin: 100%;
transform: rotate(0deg);
}}
.degree-input {{
display: block;
margin: 20px auto;
}}
</style>";
output.Write(fields);
string main = $@"<div style=""padding-top:16px;"">
<label for=""{this.ID}"">Chosen Gradient:</label>
<input id=""{this.ID}"" class=""scContentControl"" type=""text"" value=""{Value}"" />
</div>
<script>
var regex = /linear-gradient\((\d{{1,3}}deg),\s*(#[0-9a-fA-F]{{3,6}}),\s*(#[0-9a-fA-F]{{3,6}})\)/
const degreeInput = document.getElementById('degreeInput');
const clockArm = document.getElementById('clockArm');
const degreeDisplay = document.getElementById('degreeDisplay');
const colorPicker1 = document.getElementById('colorPicker1');
const colorPicker2 = document.getElementById('colorPicker2');
const selectedColor1 = document.getElementById('selectedColor1');
const selectedColor2 = document.getElementById('selectedColor2');
const gradientdisplay = document.getElementById('gradientdisplay');
const lgString = document.getElementById('{this.ID}').value;
var matches = lgString?.match(regex);
if (matches) {{
var degree = matches[1];
var color1 = matches[2];
var color2 = matches[3];
colorPicker1.value = color1;
selectedColor1.value = color1;
colorPicker2.value = color2;
selectedColor2.value = color2;
degree = degree.replace('deg', '');
degreeInput.value = degree;
degreeDisplay.textContent = degree;
clockArm.style.transform = `rotate(${{degree}}deg)`;
gradientdisplay.style.background = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
}}
colorPicker1.addEventListener('input', function() {{
selectedColor1.value = colorPicker1.value;
gradientdisplay.style.background = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
document.getElementById('{this.ID}').value = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
}});
colorPicker2.addEventListener('input', function() {{
selectedColor2.value = colorPicker2.value;
gradientdisplay.style.background = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
document.getElementById('{this.ID}').value = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
}});
degreeInput.addEventListener('input', function() {{
const degree = degreeInput.value;
clockArm.style.transform = `rotate(${{degree}}deg)`;
degreeDisplay.textContent = degree;
gradientdisplay.style.background = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
document.getElementById('{this.ID}').value = 'linear-gradient(' + degreeInput.value + 'deg,' + selectedColor1.value + ',' + selectedColor2.value + ')';
}});
</script>";
output.Write(main);
output.Write("</div>");
}
One you put it all together you can then add the LinearGradient
field just like you do any other field to your component templates and you’ve now really given your content authors a powerful experience.