[Your Form] -> Settings -> Form Styler. * - Click "Add New" to create a new styling feed. * - Give your profile a name (e.g., "Dark Theme" or "Contact Page Style"). * * 2) Configure Styles * - Open the "Styling" section in the feed settings. * - Global Tokens: Use sections like Typography, Colors, and Spacing to set general form styles. * - Type Overrides: Select a field type from the dropdown, then click "Type overrides" in the navigation to edit. * - Field Overrides: Click a specific field in the field list on the left to apply styles only to that field. * * 3) Activate and Apply * - Mark the feed as "Default" if you want it applied to all instances of this form. * - Use the "Apply in admin preview" checkbox to see your styles while building the form. * - To apply a specific (non-default) feed via URL for testing, append `?blfs_feed=[FEED_ID]` to your page URL. * * 4) Advanced Management * - Export/Import: Copy the JSON payload to move styles between forms or sites. * - Reset: Use the "Reset defaults" button to clear all configurations and start fresh. * * Developer Notes * - CSS Injection: Styles are injected into the page head via `` tags, scoped to the specific form and feed. * - Scoping: CSS rules use dual selectors (`.blfs-scope-[ID]` and `#gform_wrapper_[ID]`) for maximum compatibility. * - Payload: The authoritative data source is the base64-encoded JSON stored in the `style_b64` feed meta. */ if ( ! defined( 'ABSPATH' ) ) { exit; } add_action( 'gform_loaded', function () { if ( ! class_exists( 'GFForms' ) || ! method_exists( 'GFForms', 'include_addon_framework' ) ) { return; } GFForms::include_addon_framework(); if ( class_exists( 'GF_BrightLeaf_Form_Styler_AddOn' ) ) { return; } /** * Class GF_BrightLeaf_Form_Styler_AddOn * Extends the GFFeedAddOn class to provide custom form-styling functionality for Gravity Forms. * * This add-on enables the addition of conditional styling configurations, feed management for form styling, * and admin preview capabilities. It integrates seamlessly with Gravity Forms to allow the configuration * and application of styles at both global and field-level granularity. */ class GF_BrightLeaf_Form_Styler_AddOn extends GFFeedAddOn { // phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore,PHPCompatibility.FunctionDeclarations.NewClosure.ThisFoundOutsideClass /** * The version of the add-on. * * @var string */ protected $_version = '1.5.0'; /** * The minimum required version of Gravity Forms. * * @var string */ protected $_min_gravityforms_version = '2.6'; /** * The slug for the add-on. * * @var string */ protected $_slug = 'bl-gf-form-styler'; /** * The path to the file containing the add-on. * * @var string */ protected $_path = __FILE__; /** * The full path to the file containing the add-on. * * @var string */ protected $_full_path = __FILE__; /** * The title of the add-on. * * @var string */ protected $_title = 'BrightLeaf Form Styler'; /** * The short title of the add-on. * * @var string */ protected $_short_title = 'Form Styler'; // phpcs:enable PSR2.Classes.PropertyDeclaration.Underscore /** * The singleton instance of the class. * * @var self|null */ private static $instance = null; /** * Keep track of feeds already injected on this page load to prevent duplicate
How this works: Global tokens become CSS variables on a feed-specific scope class. Type overrides apply to all fields of a given type. Field overrides apply only to a specific field and win over type overrides.

Sections

Types

Pick a type, then open "Type overrides".

Fields

Form ID: Feed ID:


Generated CSS (preview)

Preview only — CSS is injected at runtime via a <style> tag and is not stored in a GF settings field.

JSON payload (advanced)

This is the authoritative saved payload. The UI edits this value. Invalid JSON will be rejected on save.
get_feeds( $form_id ); if ( is_array( $feeds ) ) { foreach ( $feeds as $f ) { $other_id = absint( rgar( $f, 'id' ) ); if ( absint( $feed_id ) === $other_id ) { continue; } $meta = rgar( $f, 'meta' ); if ( rgar( $meta, 'is_default' ) ) { $meta['is_default'] = 0; if ( method_exists( 'GFAPI', 'update_feed_meta' ) ) { /* @noinspection PhpUndefinedMethodInspection */ GFAPI::update_feed_meta( $other_id, $meta ); } else { $this->update_feed_meta( $other_id, $meta ); } } } } } return parent::save_feed_settings( $feed_id, $form_id, $settings ); } /** * Get default style payload. * * @return array Default tokens and override structures. */ protected function default_style_payload() { // All token values are empty strings by default. // Only tokens the user explicitly fills in will emit CSS rules. // This ensures a fresh feed has zero impact on form appearance. return [ 'version' => 1, 'tokens' => [ 'typography' => [ 'base_font_size' => '', 'label_font_size' => '', 'input_font_size' => '', ], 'colors' => [ 'text' => '', 'label' => '', 'choice_label' => '', 'description' => '', 'input_bg' => '', 'input_border' => '', 'focus' => '', 'error' => '', 'button_bg' => '', 'button_text' => '', ], 'spacing' => [ 'field_margin_bottom' => '', 'input_padding' => '', 'section_padding' => '', ], 'borders' => [ 'radius' => '', 'border_width' => '', ], 'buttons' => [ 'radius' => '', 'padding' => '', ], 'states' => [ 'focus_ring' => '', 'error_border' => '', ], ], 'type_overrides' => (object) [], 'field_overrides' => (object) [], ]; } /** * Check for feed override in request and apply it to form object. * * @param array $form The form object. * * @return array Modified form object. */ public function maybe_set_feed_override_from_request( $form ) { if ( empty( $form ) || empty( $form['id'] ) ) { return $form; } if ( rgget( 'blfs_feed' ) ) { $form['_blfs_feed_override'] = absint( rgget( 'blfs_feed' ) ); } return $form; } /** * Identify applicable feed for a form. * * Priority: Request override > Default feed > First active feed. * * @param array $form The form object. * * @return array|null The selected feed or null. */ protected function pick_applicable_feed_for_form( $form ) { $form_id = (int) rgar( $form, 'id' ); if ( ! $form_id ) { return null; } $override = (int) rgar( $form, '_blfs_feed_override' ); if ( $override ) { $feed = GFAPI::get_feed( $override ); if ( is_array( $feed ) && (int) rgar( $feed, 'form_id' ) === $form_id && rgar( $feed, 'addon_slug' ) === $this->_slug ) { return $feed; } } $feeds = $this->get_feeds( $form_id ); if ( empty( $feeds ) || ! is_array( $feeds ) ) { return null; } foreach ( $feeds as $f ) { if ( ! rgar( $f, 'is_active' ) ) { continue; } if ( rgar( rgar( $f, 'meta' ), 'is_default' ) ) { return $f; } } foreach ( $feeds as $f ) { if ( rgar( $f, 'is_active' ) ) { return $f; } } return null; } /** * Inject scope class into form tag. * * @param string $form_tag The opening
tag HTML. * @param array $form The form object. * * @return string Modified form tag HTML. */ public function filter_form_tag_add_scope_class( $form_tag, $form ) { $feed = $this->pick_applicable_feed_for_form( $form ); if ( ! is_array( $feed ) ) { return $form_tag; } $feed_id = (int) rgar( $feed, 'id' ); if ( ! $feed_id ) { return $form_tag; } $scope_class = 'blfs-scope-' . $feed_id; if ( strpos( $form_tag, 'class=' ) !== false ) { $form_tag = preg_replace( '/class=(["\'])(.*?)\1/', 'class=$1$2 ' . esc_attr( $scope_class ) . '$1', $form_tag, 1 ); } else { $form_tag = str_replace( 'pick_applicable_feed_for_form( $form ); if ( ! is_array( $feed ) ) { return $form_string; } $feed_id = (int) rgar( $feed, 'id' ); if ( ! $feed_id ) { return $form_string; } if ( in_array( $feed_id, self::$injected_feeds, true ) ) { return $form_string; } self::$injected_feeds[] = $feed_id; $meta = rgar( $feed, 'meta' ); // Allow GF's own preview page through even without the checkbox, // but block other admin contexts unless the checkbox is ticked. if ( is_admin() && ! rgar( $meta, 'apply_admin_preview' ) && ! $this->is_gf_preview_context() ) { return $form_string; } $payload_b64 = rgar( $meta, 'style_b64' ); $json = ''; if ( ! empty( $payload_b64 ) && is_string( $payload_b64 ) ) { $decoded = base64_decode( $payload_b64, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode if ( false !== $decoded ) { $json = $decoded; } } if ( empty( $json ) ) { $json = wp_json_encode( $this->default_style_payload() ); } $css = $this->generate_css_from_json( $form_id, $feed_id, $json ); if ( empty( $css ) ) { return $form_string; } $js = ''; if ( rgar( $meta, 'debug_mode' ) ) { GFCommon::log_debug( __METHOD__ . "(): Injecting BLFS CSS. form_id=$form_id, feed_id=$feed_id, is_admin=" . ( is_admin() ? '1' : '0' ) ); $is_admin_js = is_admin() ? 'true' : 'false'; $js = ""; } $style_tag = "\n\n$js\n"; return $style_tag . $form_string; // ↑ Function ends here. Do NOT add any code after this closing brace. } /** * Detect GF's built-in form preview page. */ protected function is_gf_preview_context() { if ( rgget( 'gf_page' ) && 'preview' === rgget( 'gf_page' ) ) { return true; } $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; if ( strpos( $uri, 'gf_page=preview' ) !== false ) { return true; } if ( rgget( 'page' ) && strpos( rgget( 'page' ), 'gf_' ) === 0 && strpos( rgget( 'page' ), 'preview' ) !== false ) { return true; } return false; } /* -------------------------------------------------------------------- CSS generation (PHP — server-side render) Scope: ".blfs-scope-{feedId}, #gform_wrapper_{formId}" The dual selector gives front-end coverage via the class AND a fallback via the GF wrapper ID for edge cases where the class is not injected (e.g., cached markup, third-party renderers). -------------------------------------------------------------------- */ /** * Helper to generate CSS for a set of overrides. * * @param string $selector The base CSS selector for this set of overrides. * @param array $ov The override configuration. * * @return string The generated CSS. */ private function generate_override_css( $selector, $ov ) { if ( ! is_array( $ov ) ) { return ''; } $label_sel = "$selector .gfield_label"; $input_sel = "$selector input:not([type=\"checkbox\"]):not([type=\"radio\"]), $selector textarea, $selector select"; $choice_label_sel = "$selector .gchoice label"; $label_color = isset( $ov['label_color'] ) ? trim( (string) $ov['label_color'] ) : ''; $label_size = isset( $ov['label_font_size'] ) ? trim( (string) $ov['label_font_size'] ) : ''; $input_bg = isset( $ov['input_bg'] ) ? trim( (string) $ov['input_bg'] ) : ''; $input_text = isset( $ov['input_text'] ) ? trim( (string) $ov['input_text'] ) : ''; $input_border = isset( $ov['input_border'] ) ? trim( (string) $ov['input_border'] ) : ''; $input_radius = isset( $ov['input_radius'] ) ? trim( (string) $ov['input_radius'] ) : ''; $css = ''; if ( $label_color || $label_size ) { $css .= "\n$label_sel {"; if ( $label_color ) { $css .= " color: $label_color !important;"; } if ( $label_size ) { $css .= " font-size: $label_size !important;"; } $css .= " }\n"; } if ( $input_bg || $input_text || $input_border || $input_radius ) { $css .= "\n$input_sel {"; if ( $input_bg ) { $css .= " background: $input_bg !important;"; } if ( $input_text ) { $css .= " color: $input_text !important;"; } if ( $input_border ) { $css .= " border-color: $input_border !important;"; } if ( $input_radius ) { $css .= " border-radius: $input_radius !important;"; } $css .= " }\n"; } if ( $input_text ) { $css .= "\n$choice_label_sel { color: $input_text !important; }\n"; } return $css; } /** * Generate CSS from JSON payload. * * @param int $form_id The form ID. * @param int $feed_id The feed ID. * @param string $json The JSON style payload. * * @return string Generated CSS rules. */ protected function generate_css_from_json( $form_id, $feed_id, $json ) { $data = json_decode( $json, true ); if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) { return ''; } $scopes = [ ".blfs-scope-$feed_id", "#gform_wrapper_$form_id" ]; $scope_str = implode( ', ', $scopes ); $expand = function ( $sub, $attach = false ) use ( $scopes ) { $out = []; foreach ( $scopes as $s ) { $out[] = $attach ? "$s$sub" : "$s $sub"; } return implode( ",\n", $out ); }; $tokens = isset( $data['tokens'] ) && is_array( $data['tokens'] ) ? $data['tokens'] : []; $type_overrides = isset( $data['type_overrides'] ) && is_array( $data['type_overrides'] ) ? $data['type_overrides'] : []; $field_overrides = isset( $data['field_overrides'] ) && is_array( $data['field_overrides'] ) ? $data['field_overrides'] : []; $get = function ( $path, $default_value = '' ) use ( $tokens ) { $parts = explode( '.', $path ); $cur = $tokens; foreach ( $parts as $p ) { if ( ! is_array( $cur ) || ! array_key_exists( $p, $cur ) ) { return $default_value; } $cur = $cur[ $p ]; } return is_scalar( $cur ) ? (string) $cur : $default_value; }; // No fallback defaults — empty string means "not set", and the var_lines loop // skips empty values, so only what the user has explicitly entered gets emitted. $vars = [ '--blfs-base-font-size' => $get( 'typography.base_font_size', '' ), '--blfs-label-font-size' => $get( 'typography.label_font_size', '' ), '--blfs-input-font-size' => $get( 'typography.input_font_size', '' ), '--blfs-text' => $get( 'colors.text', '' ), '--blfs-label' => $get( 'colors.label', '' ), '--blfs-choice-label' => $get( 'colors.choice_label', '' ), '--blfs-description' => $get( 'colors.description', '' ), '--blfs-input-bg' => $get( 'colors.input_bg', '' ), '--blfs-input-border' => $get( 'colors.input_border', '' ), '--blfs-focus' => $get( 'colors.focus', '' ), '--blfs-error' => $get( 'colors.error', '' ), '--blfs-button-bg' => $get( 'colors.button_bg', '' ), '--blfs-button-text' => $get( 'colors.button_text', '' ), '--blfs-field-mb' => $get( 'spacing.field_margin_bottom', '' ), '--blfs-input-padding' => $get( 'spacing.input_padding', '' ), '--blfs-radius' => $get( 'borders.radius', '' ), '--blfs-border-width' => $get( 'borders.border_width', '' ), '--blfs-button-radius' => $get( 'buttons.radius', '' ), '--blfs-button-padding' => $get( 'buttons.padding', '' ), '--blfs-focus-ring' => $get( 'states.focus_ring', '' ), '--blfs-error-border' => $get( 'states.error_border', '' ), ]; $var_lines = []; foreach ( $vars as $k => $v ) { $v = trim( (string) $v ); if ( '' === $v ) { continue; } // Basic sanitization to prevent breaking out of CSS declaration. $v = str_replace( [ ';', '{', '}', '\\' ], '', $v ); $var_lines[] = " $k: $v;"; } // Only emit a :root-level custom-property block if there is at least one set value. if ( ! empty( $var_lines ) ) { $css = "$scope_str {\n" . implode( "\n", $var_lines ) . "\n}\n\n"; } else { $css = ''; } // Helper: build a selector string for common input fields. $input_types = [ 'text', 'email', 'number', 'tel', 'url', 'password' ]; $input_sel_list = []; foreach ( $input_types as $t ) { $input_sel_list[] = $expand( "input[type=\"$t\"]" ); } $input_sel_list[] = $expand( 'textarea' ); $input_sel_list[] = $expand( 'select' ); $input_sel_list = implode( ",\n", $input_sel_list ); $focus_sel_list = []; foreach ( $input_types as $t ) { $focus_sel_list[] = $expand( "input[type=\"$t\"]:focus" ); } $focus_sel_list[] = $expand( 'textarea:focus' ); $focus_sel_list[] = $expand( 'select:focus' ); $focus_sel_list = implode( ",\n", $focus_sel_list ); $button_sel_list = $expand( '.gform_button' ) . ",\n" . $expand( 'input[type="submit"].gform_button' ); $error_input_sel_list = $expand( '.gfield_error input' ) . ",\n" . $expand( '.gfield_error textarea' ) . ",\n" . $expand( '.gfield_error select' ); // ---- Wrapper: font-size + text color ---- $wrapper_props = []; if ( $get( 'typography.base_font_size', '' ) !== '' ) { $wrapper_props[] = ' font-size: var(--blfs-base-font-size) !important;'; } if ( $get( 'colors.text', '' ) !== '' ) { $wrapper_props[] = ' color: var(--blfs-text) !important;'; } if ( ! empty( $wrapper_props ) ) { $css .= $expand( '.gform_wrapper', true ) . ",\n" . $scope_str . " {\n" . implode( "\n", $wrapper_props ) . "\n}\n\n"; } // ---- Field margin ---- if ( $get( 'spacing.field_margin_bottom', '' ) !== '' ) { $css .= $expand( '.gfield' ) . " {\n margin-bottom: var(--blfs-field-mb) !important;\n}\n\n"; } // ---- Label ---- $label_props = []; if ( $get( 'colors.label', '' ) !== '' ) { $label_props[] = ' color: var(--blfs-label) !important;'; } if ( $get( 'typography.label_font_size', '' ) !== '' ) { $label_props[] = ' font-size: var(--blfs-label-font-size) !important;'; } if ( ! empty( $label_props ) ) { $css .= $expand( '.gfield_label' ) . " {\n" . implode( "\n", $label_props ) . "\n}\n\n"; } // ---- Choice labels (radio/checkbox option text) — separate from field label ---- if ( $get( 'colors.choice_label', '' ) !== '' ) { $css .= $expand( '.gchoice label' ) . " { color: var(--blfs-choice-label); }\n\n"; } // ---- Description ---- if ( $get( 'colors.description', '' ) !== '' ) { $css .= $expand( '.gfield_description' ) . " { color: var(--blfs-description); }\n\n"; } // ---- Inputs (only emit properties that are actually set) ---- $input_props = []; if ( $get( 'colors.input_bg', '' ) !== '' ) { $input_props[] = ' background: var(--blfs-input-bg);'; } if ( $get( 'colors.input_border', '' ) !== '' ) { $input_props[] = ' border-color: var(--blfs-input-border);'; } if ( $get( 'borders.border_width', '' ) !== '' ) { $input_props[] = ' border-width: var(--blfs-border-width);'; } if ( $get( 'borders.radius', '' ) !== '' ) { $input_props[] = ' border-radius: var(--blfs-radius);'; } if ( $get( 'spacing.input_padding', '' ) !== '' ) { $input_props[] = ' padding: var(--blfs-input-padding);'; } if ( $get( 'typography.input_font_size', '' ) !== '' ) { $input_props[] = ' font-size: var(--blfs-input-font-size);'; } if ( ! empty( $input_props ) ) { $css .= "$input_sel_list {\n" . implode( "\n", $input_props ) . "\n}\n\n"; } // ---- Focus state ---- $focus_props = []; if ( $get( 'colors.focus', '' ) !== '' ) { $focus_props[] = ' border-color: var(--blfs-focus);'; } if ( $get( 'states.focus_ring', '' ) !== '' ) { $focus_props[] = ' box-shadow: var(--blfs-focus-ring);'; } if ( ! empty( $focus_props ) ) { $css .= "$focus_sel_list {\n outline: none;\n" . implode( "\n", $focus_props ) . "\n}\n\n"; } // ---- Button ---- $button_props = []; if ( $get( 'colors.button_bg', '' ) !== '' ) { $button_props[] = ' background: var(--blfs-button-bg);'; $button_props[] = ' border: 1px solid var(--blfs-button-bg);'; } if ( $get( 'colors.button_text', '' ) !== '' ) { $button_props[] = ' color: var(--blfs-button-text);'; } if ( $get( 'buttons.radius', '' ) !== '' ) { $button_props[] = ' border-radius: var(--blfs-button-radius);'; } if ( $get( 'buttons.padding', '' ) !== '' ) { $button_props[] = ' padding: var(--blfs-button-padding);'; } if ( ! empty( $button_props ) ) { $css .= "$button_sel_list {\n" . implode( "\n", $button_props ) . "\n}\n\n"; } // ---- Error state ---- if ( $get( 'states.error_border', '' ) !== '' ) { $css .= "$error_input_sel_list {\n border-color: var(--blfs-error-border);\n}\n\n"; } // Type overrides foreach ( $type_overrides as $type => $ov ) { $type = preg_replace( '/[^a-z0-9_\-]/i', '', (string) $type ); if ( '' === $type ) { continue; } $css .= $this->generate_override_css( $expand( ".gfield--type-$type" ), $ov ); } // Field overrides (highest specificity — these win over type overrides) foreach ( $field_overrides as $field_id => $ov ) { $field_id = preg_replace( '/[^0-9.]/', '', (string) $field_id ); if ( '' === $field_id ) { continue; } $css .= $this->generate_override_css( $expand( "#field_{$form_id}_$field_id" ), $ov ); } return $css; } } GFAddOn::register( 'GF_BrightLeaf_Form_Styler_AddOn' ); }, 5 );