Blog

July 28, 2026 · 8 min read

How to Build a Background, Highlight, and Text Color Picker for an ASP.NET Web Forms Master Page

A single shared master page meant every listing on the platform looked identical, and any request for custom branding meant hand-editing CSS for one client at a time. Here is how I built a three-part color picker — background, highlight, and text — that lets each listing owner style their own page live, safely.

Why a Shared Master Page Doesn't Work for Multi-Tenant Listing Styling

A property listing platform typically runs every listing page through one shared master page — one Site.master, one CSS file, one look for every listing on the site. That works fine until listing owners start asking for their own branding: their own background color, their own accent color on buttons and borders, their own text color to match their existing marketing.

The naive fix is to give each listing owner a custom CSS override, hand-edited by a developer. That does not scale — every new request is a support ticket, every change is a deploy, and the master page slowly fills up with special-case CSS classes for one-off clients. The actual fix is to make styling a data problem, not a code problem: store three color values per listing, and have the master page render them dynamically for whichever listing is currently being viewed.

Step 1 — Model the Three Color Values

Every listing gets exactly three customizable values: background, highlight (used for borders, buttons, and accents), and text. Keep the model deliberately small — three values are enough to make a listing feel branded without turning this into a full theme editor.

CREATE TABLE ListingStyle (
    ListingId       INT PRIMARY KEY,
    BackgroundColor VARCHAR(7) NOT NULL DEFAULT '#FFFFFF',
    HighlightColor  VARCHAR(7) NOT NULL DEFAULT '#0078D4',
    TextColor       VARCHAR(7) NOT NULL DEFAULT '#000000',
    UpdatedAt       DATETIME NOT NULL DEFAULT GETUTCDATE()
);

Storing colors as fixed-length hex strings (#RRGGBB) rather than named colors or RGB tuples keeps validation simple — a single regex covers the entire column, both on the way in and on the way out.

Step 2 — Build the Color Picker Control

The native HTML <input type="color"> element gives you a full OS-level color picker with zero JavaScript dependencies and works in every modern browser. Wrap three of them, one per value, in a user control so the same picker can be reused anywhere a listing owner edits their style:

<!-- ListingStylePicker.ascx -->
<div class="style-picker">
    <label for="<%= colorBackground.ClientID %>">Background</label>
    <input type="color" id="colorBackground" runat="server" value="#ffffff" oninput="updatePreview()" />

    <label for="<%= colorHighlight.ClientID %>">Highlight</label>
    <input type="color" id="colorHighlight" runat="server" value="#0078d4" oninput="updatePreview()" />

    <label for="<%= colorText.ClientID %>">Text</label>
    <input type="color" id="colorText" runat="server" value="#000000" oninput="updatePreview()" />

    <asp:Button ID="btnSaveStyle" runat="server" Text="Save Style" OnClick="btnSaveStyle_Click" />
    <asp:Label ID="lblError" runat="server" CssClass="error-text" />
</div>

<div id="listingPreview" class="listing-preview">
    <h3>Sample Listing Title</h3>
    <p>This is how your listing will appear to visitors.</p>
</div>

Marking the inputs runat="server" means the code-behind can read their submitted values directly on postback via colorBackground.Value, without wiring up a separate hidden field or manual form parsing.

Step 3 — Live Preview Before Saving

Listing owners should see the result before committing to it. CSS custom properties make this a few lines of JavaScript — update the properties on input, and the preview box repaints instantly with no server round-trip:

function updatePreview() {
    var bg = document.getElementById('<%= colorBackground.ClientID %>').value;
    var hl = document.getElementById('<%= colorHighlight.ClientID %>').value;
    var tx = document.getElementById('<%= colorText.ClientID %>').value;

    var preview = document.getElementById('listingPreview');
    preview.style.setProperty('--listing-bg', bg);
    preview.style.setProperty('--listing-highlight', hl);
    preview.style.setProperty('--listing-text', tx);
}
.listing-preview {
    background-color: var(--listing-bg, #ffffff);
    color: var(--listing-text, #000000);
    border: 2px solid var(--listing-highlight, #0078d4);
    padding: 16px;
    border-radius: 8px;
}

The fallback values in var(--listing-bg, #ffffff) matter — if the custom property is ever unset, the preview still renders with a sane default instead of falling back to browser defaults or transparent backgrounds.

Step 4 — Validate and Persist on the Server

The <input type="color"> element only lets a user select a valid hex color through the browser's picker UI — but nothing stops a direct HTTP POST with an arbitrary string in that field. Client-side constraints are a UX convenience, not a security boundary. Validate the hex format again on the server before it touches the database:

private static readonly Regex HexColorPattern = new Regex(@"^#[0-9A-Fa-f]{6}$", RegexOptions.Compiled);

protected void btnSaveStyle_Click(object sender, EventArgs e)
{
    string background = colorBackground.Value;
    string highlight   = colorHighlight.Value;
    string text        = colorText.Value;

    if (!HexColorPattern.IsMatch(background) ||
        !HexColorPattern.IsMatch(highlight) ||
        !HexColorPattern.IsMatch(text))
    {
        lblError.Text = "Invalid color value submitted.";
        return;
    }

    int listingId = Convert.ToInt32(Request.QueryString["listingId"]);

    using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["ListingDb"].ConnectionString))
    using (var cmd = new SqlCommand(@"
        MERGE ListingStyle AS target
        USING (SELECT @ListingId AS ListingId) AS source
        ON target.ListingId = source.ListingId
        WHEN MATCHED THEN
            UPDATE SET BackgroundColor = @Background, HighlightColor = @Highlight, TextColor = @Text, UpdatedAt = GETUTCDATE()
        WHEN NOT MATCHED THEN
            INSERT (ListingId, BackgroundColor, HighlightColor, TextColor)
            VALUES (@ListingId, @Background, @Highlight, @Text);", conn))
    {
        cmd.Parameters.AddWithValue("@ListingId", listingId);
        cmd.Parameters.AddWithValue("@Background", background);
        cmd.Parameters.AddWithValue("@Highlight", highlight);
        cmd.Parameters.AddWithValue("@Text", text);

        conn.Open();
        cmd.ExecuteNonQuery();
    }
}

The MERGE statement handles both the first save (insert) and every subsequent edit (update) with one round-trip, which matches how this actually gets used — a listing owner might set their style once at signup or come back and tweak it months later.

Step 5 — Inject the Saved Colors Into the Master Page at Render Time

This is the piece that makes the whole approach work: the master page is shared, but the style block it emits is not. On every request, the master page's code-behind looks up which listing is being viewed, reads that listing's saved colors, and writes a small <style> block into the page head before anything renders:

// Site.master.cs
protected void Page_Load(object sender, EventArgs e)
{
    int listingId;
    if (int.TryParse(Request.QueryString["listingId"], out listingId))
    {
        var style = ListingStyleRepository.GetByListingId(listingId);

        var styleBlock = new HtmlGenericControl("style");
        styleBlock.InnerHtml =
            $":root {{ --listing-bg: {style.BackgroundColor}; --listing-highlight: {style.HighlightColor}; --listing-text: {style.TextColor}; }}";

        Page.Header.Controls.Add(styleBlock);
    }
}

Every listing page pulls in the same master page and the same base CSS file. What changes per request is three custom property values, computed once per page load and scoped to that request only — no per-client CSS files, no build step, no deploy required to onboard a new listing's branding.

Step 6 — Defaults and Defense-in-Depth on Read

Validating on save is not the same as trusting the value forever. A direct database edit, a migration script, or a future admin tool that writes to this table might not go through the same validation path. Because this value is about to be written directly into a <style> block as raw markup, re-validate it on the way out, not just on the way in:

public static ListingStyle GetByListingId(int listingId)
{
    // ... query ListingStyle by listingId ...

    if (!IsValidHex(background) || !IsValidHex(highlight) || !IsValidHex(text))
        return ListingStyle.Default; // never inject unvalidated data into markup, regardless of its source

    return new ListingStyle
    {
        BackgroundColor = background,
        HighlightColor  = highlight,
        TextColor       = text
    };
}

If a listing has never set a custom style, or the stored value somehow fails validation, falling back to ListingStyle.Default means the page still renders correctly with the platform's standard colors — a missing or bad style value degrades gracefully instead of breaking the page or leaving an injection vector open.

Putting It All Together — The Structure

The complete pattern has four layers, each with one responsibility:

  • Color picker control — captures exactly three values with a live, no-server-round-trip preview
  • Server-side validation — a single hex-format regex enforced both when a value is saved and again when it's read back out
  • Per-listing storage — one row per listing, with defaults applied whenever a value is missing or invalid
  • Master page injection — the shared template reads the current listing's style at request time and emits a scoped custom-property block, so one master page renders a different look for every listing without a CSS file per client

Adding a fourth customizable value later — a border radius, a font choice — means extending the same model, the same picker, and the same injection point. Nothing about the master page itself needs to change.

Implementation Checklist

  • Colors stored as fixed-length #RRGGBB hex strings, one row per listing
  • Native <input type="color"> controls, marked runat="server" for direct code-behind access
  • Live preview driven by CSS custom properties, updated on input with no server round-trip
  • Hex format validated server-side on save — client-side constraints are UX only, never trusted as the security boundary
  • Hex format validated again on read, before injecting into the master page's <style> block
  • Sane default style applied whenever a value is missing or fails validation
  • Master page reads the current listing ID per request and injects a scoped style block — no per-client CSS files, no deploy needed for a new listing's branding

For more on the listing platform this styling system sits on top of, see the posts on cleaning up video and XML data on a property listing platform and fixing a RapidAPI quota issue on the same platform. If you're maintaining a legacy ASP.NET Web Forms system and want per-client customization added without a full rewrite, see the .NET backend development services page or get in touch.