NeoSand / theme.py
kbray's picture
Fri, 17 Apr 2026 13:05:34 -0400
ef0b8db
"""
Sage & Sand β€” Neumorphic Gradio Theme
For AUTOMATIC1111 Stable Diffusion WebUI
Palette:
#CCD5AE sage green (accent, highlights)
#E9EDC9 pale sage (secondary accent)
#FEFAE0 ivory cream (light surfaces)
#FAEDCD warm parchment (mid surfaces)
#D4A373 caramel tan (page background β€” all neumorphic shadows derived here)
Neumorphic shadow values (derived from #D4A373):
Light highlight : #E8C49A (+20% brightness)
Dark recess : #A87845 (-20% brightness)
Usage in AUTOMATIC1111:
1. Place theme.py and app.py in your A1111 root folder.
2. In webui.py, locate the gr.Blocks(...) call and add:
from theme import SageAndSandNeumorphic, NEUMORPHIC_CSS
with gr.Blocks(theme=SageAndSandNeumorphic(), css=NEUMORPHIC_CSS) as demo:
3. Restart the WebUI.
Publishing to Hugging Face Hub (standalone theme):
from theme import SageAndSandNeumorphic
theme = SageAndSandNeumorphic()
theme.push_to_hub(
repo_name="NeoSand",
org_name="kbray",
hf_token="hf_...",
)
"""
import gradio as gr
from gradio.themes.base import Base
from gradio.themes.utils import colors, fonts, sizes
# ── Palette constants ────────────────────────────────────────────────────────
BG = "#D4A373" # page background / neumorphic base
LIGHT_SHDW = "#E8C49A" # raised highlight shadow
DARK_SHDW = "#A87845" # raised depth shadow
PANEL = "#DAAA7A" # panel β€” slightly darker than BG for recessed feel
INPUT_BG = "#D9A870" # inputs β€” slightly darker, appear pressed in
ACCENT = "#CCD5AE" # sage green
ACCENT2 = "#E9EDC9" # pale sage
CREAM = "#FEFAE0" # text-on-dark surfaces / bright highlights
PARCHMENT = "#FAEDCD" # lighter raised surfaces
TEXT_DARK = "#3a3a2a" # body text on light
TEXT_MID = "#5a4a30" # body text on tan
TEXT_MUTED = "#7a6248" # muted / label text on tan
# ── Neumorphic CSS (injected via gr.Blocks(css=NEUMORPHIC_CSS)) ──────────────
# Gradio's token system does not expose every element's box-shadow, so we
# supplement the theme tokens with targeted CSS overrides.
NEUMORPHIC_CSS = f"""
/* ── Reset & base ─────────────────────────────────────────────────────── */
gradio-app {{
background: {BG} !important;
}}
/* ── Block / panel containers β€” raised ────────────────────────────────── */
.gradio-container .block,
.gradio-container .form,
.gradio-container .gap {{
background: {BG} !important;
border: none !important;
border-radius: 14px !important;
box-shadow: -6px -6px 12px {LIGHT_SHDW},
6px 6px 12px {DARK_SHDW} !important;
}}
/* ── Inputs / textareas β€” recessed (inset shadow) ──────────────────────── */
.gradio-container input[type="text"],
.gradio-container input[type="number"],
.gradio-container input[type="search"],
.gradio-container textarea,
.gradio-container select {{
background: {INPUT_BG} !important;
border: none !important;
border-radius: 10px !important;
box-shadow: inset -3px -3px 7px {LIGHT_SHDW},
inset 3px 3px 7px {DARK_SHDW} !important;
color: {TEXT_DARK} !important;
transition: box-shadow 0.2s ease !important;
}}
.gradio-container input[type="text"]:focus,
.gradio-container input[type="number"]:focus,
.gradio-container textarea:focus,
.gradio-container select:focus {{
box-shadow: inset -2px -2px 5px {LIGHT_SHDW},
inset 2px 2px 5px {DARK_SHDW},
0 0 0 2px rgba(204,213,174,0.55) !important;
outline: none !important;
}}
/* ── Sliders β€” track recessed, thumb raised ────────────────────────────── */
.gradio-container input[type="range"] {{
-webkit-appearance: none;
appearance: none;
background: transparent !important;
border: none !important;
box-shadow: none !important;
}}
.gradio-container input[type="range"]::-webkit-slider-runnable-track {{
height: 6px;
border-radius: 3px;
background: {INPUT_BG};
box-shadow: inset -2px -2px 4px {LIGHT_SHDW},
inset 2px 2px 4px {DARK_SHDW};
border: none;
}}
.gradio-container input[type="range"]::-webkit-slider-thumb {{
-webkit-appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: {BG};
margin-top: -6px;
box-shadow: -3px -3px 6px {LIGHT_SHDW},
3px 3px 6px {DARK_SHDW};
border: none;
cursor: pointer;
transition: box-shadow 0.15s ease;
}}
.gradio-container input[type="range"]::-webkit-slider-thumb:hover {{
box-shadow: -4px -4px 8px {LIGHT_SHDW},
4px 4px 8px {DARK_SHDW},
0 0 0 3px rgba(204,213,174,0.45);
}}
.gradio-container input[type="range"]::-moz-range-track {{
height: 6px;
border-radius: 3px;
background: {INPUT_BG};
box-shadow: inset -2px -2px 4px {LIGHT_SHDW},
inset 2px 2px 4px {DARK_SHDW};
border: none;
}}
.gradio-container input[type="range"]::-moz-range-thumb {{
width: 18px;
height: 18px;
border-radius: 50%;
background: {BG};
box-shadow: -3px -3px 6px {LIGHT_SHDW},
3px 3px 6px {DARK_SHDW};
border: none;
cursor: pointer;
}}
/* ── Checkboxes ─────────────────────────────────────────────────────────── */
.gradio-container input[type="checkbox"] {{
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 5px;
background: {BG};
box-shadow: inset -2px -2px 5px {LIGHT_SHDW},
inset 2px 2px 5px {DARK_SHDW};
border: none;
cursor: pointer;
transition: all 0.15s ease;
}}
.gradio-container input[type="checkbox"]:checked {{
background: {ACCENT};
box-shadow: inset -2px -2px 5px rgba(255,255,255,0.3),
inset 2px 2px 5px rgba(0,0,0,0.15);
}}
/* ── Buttons β€” primary (raised, pressed on active) ─────────────────────── */
.gradio-container .btn,
.gradio-container button.primary,
.gradio-container button[variant="primary"] {{
background: {BG} !important;
border: none !important;
border-radius: 10px !important;
box-shadow: -5px -5px 10px {LIGHT_SHDW},
5px 5px 10px {DARK_SHDW} !important;
color: {TEXT_DARK} !important;
font-weight: 500 !important;
transition: box-shadow 0.15s ease, transform 0.1s ease !important;
}}
.gradio-container button.primary,
.gradio-container button[variant="primary"] {{
background: linear-gradient(145deg, {ACCENT}, #b8c49a) !important;
color: {TEXT_DARK} !important;
}}
.gradio-container .btn:hover,
.gradio-container button:hover {{
box-shadow: -7px -7px 14px {LIGHT_SHDW},
7px 7px 14px {DARK_SHDW} !important;
}}
.gradio-container .btn:active,
.gradio-container button:active {{
box-shadow: inset -3px -3px 7px {LIGHT_SHDW},
inset 3px 3px 7px {DARK_SHDW} !important;
transform: scale(0.985) !important;
}}
/* ── Stop / cancel button ──────────────────────────────────────────────── */
.gradio-container button[variant="stop"],
.gradio-container button.stop {{
background: linear-gradient(145deg, #e8c9a0, #d4b080) !important;
}}
/* ── Tabs ──────────────────────────────────────────────────────────────── */
.gradio-container .tabs > .tab-nav {{
border-bottom: none !important;
gap: 8px;
}}
.gradio-container .tabs > .tab-nav > button {{
background: {BG} !important;
border: none !important;
border-radius: 10px !important;
box-shadow: -4px -4px 8px {LIGHT_SHDW},
4px 4px 8px {DARK_SHDW} !important;
color: {TEXT_MID} !important;
padding: 8px 18px !important;
transition: all 0.15s ease !important;
}}
.gradio-container .tabs > .tab-nav > button.selected {{
background: {BG} !important;
box-shadow: inset -3px -3px 7px {LIGHT_SHDW},
inset 3px 3px 7px {DARK_SHDW} !important;
color: {TEXT_DARK} !important;
font-weight: 500 !important;
}}
/* ── Accordion ─────────────────────────────────────────────────────────── */
.gradio-container .accordion {{
border: none !important;
border-radius: 12px !important;
box-shadow: -5px -5px 10px {LIGHT_SHDW},
5px 5px 10px {DARK_SHDW} !important;
overflow: hidden;
}}
.gradio-container .accordion > .label-wrap {{
background: {BG} !important;
}}
/* ── Image output panel ────────────────────────────────────────────────── */
.gradio-container .image-container,
.gradio-container .output-image {{
border: none !important;
border-radius: 14px !important;
box-shadow: inset -4px -4px 10px {LIGHT_SHDW},
inset 4px 4px 10px {DARK_SHDW} !important;
overflow: hidden;
}}
/* ── Labels & typography ───────────────────────────────────────────────── */
.gradio-container label span,
.gradio-container .block-label,
.gradio-container .label-wrap span {{
color: {TEXT_MID} !important;
font-weight: 500 !important;
font-size: 13px !important;
letter-spacing: 0.02em !important;
}}
.gradio-container .prose h1,
.gradio-container .prose h2,
.gradio-container .prose h3 {{
color: {TEXT_DARK} !important;
}}
.gradio-container .prose p,
.gradio-container .prose li {{
color: {TEXT_MID} !important;
}}
/* ── Scrollbars ────────────────────────────────────────────────────────── */
.gradio-container ::-webkit-scrollbar {{
width: 6px;
height: 6px;
}}
.gradio-container ::-webkit-scrollbar-track {{
background: {INPUT_BG};
border-radius: 3px;
box-shadow: inset 0 0 4px {DARK_SHDW};
}}
.gradio-container ::-webkit-scrollbar-thumb {{
background: {ACCENT};
border-radius: 3px;
}}
"""
# ── Gradio colour ramp (required by Base theme) ──────────────────────────────
sage_sand = colors.Color(
name="sage_sand",
c50="#f7f5ec",
c100="#FEFAE0",
c200="#FAEDCD",
c300="#E9EDC9",
c400="#CCD5AE",
c500="#b8c49a",
c600="#a0ad80",
c700="#D4A373",
c800="#b8895a",
c900="#8a6140",
c950="#5c3d24",
)
class SageAndSandNeumorphic(Base):
"""
Sage & Sand Neumorphic β€” a soft-extruded Gradio theme.
Pair with NEUMORPHIC_CSS for the full effect:
import gradio as gr
from theme import SageAndSandNeumorphic, NEUMORPHIC_CSS
with gr.Blocks(theme=SageAndSandNeumorphic(), css=NEUMORPHIC_CSS) as demo:
...
"""
def __init__(
self,
primary_hue: colors.Color = sage_sand,
secondary_hue: colors.Color = sage_sand,
neutral_hue: colors.Color = sage_sand,
spacing_size: sizes.Size = sizes.spacing_md,
radius_size: sizes.Size = sizes.radius_lg,
text_size: sizes.Size = sizes.text_md,
font=(
fonts.GoogleFont("DM Sans"),
"ui-sans-serif",
"system-ui",
"sans-serif",
),
font_mono=(
fonts.GoogleFont("JetBrains Mono"),
"ui-monospace",
"monospace",
),
):
super().__init__(
primary_hue=primary_hue,
secondary_hue=secondary_hue,
neutral_hue=neutral_hue,
spacing_size=spacing_size,
radius_size=radius_size,
text_size=text_size,
font=font,
font_mono=font_mono,
)
super().set(
# ── Page background ─────────────────────────────────────────────
body_background_fill=BG,
body_background_fill_dark="#8a6140",
# ── Blocks (base token β€” CSS overrides the visual shadow) ────────
block_background_fill=BG,
block_background_fill_dark="#a07040",
block_border_width="0px",
block_border_color="transparent",
block_shadow=f"-6px -6px 12px {LIGHT_SHDW}, 6px 6px 12px {DARK_SHDW}",
block_shadow_dark="none",
# Panel
panel_background_fill=BG,
panel_background_fill_dark="#9a7040",
panel_border_width="0px",
# ── Block labels ─────────────────────────────────────────────────
block_label_background_fill="transparent",
block_label_background_fill_dark="transparent",
block_label_text_color=TEXT_MID,
block_label_text_color_dark=CREAM,
block_label_text_size="*text_sm",
block_label_text_weight="500",
block_title_text_color=TEXT_DARK,
block_title_text_color_dark=CREAM,
block_title_text_weight="600",
# ── Inputs ───────────────────────────────────────────────────────
input_background_fill=INPUT_BG,
input_background_fill_dark="#9a7040",
input_background_fill_focus=INPUT_BG,
input_border_width="0px",
input_border_color="transparent",
input_shadow=f"inset -3px -3px 7px {LIGHT_SHDW}, inset 3px 3px 7px {DARK_SHDW}",
input_shadow_focus=f"inset -2px -2px 5px {LIGHT_SHDW}, inset 2px 2px 5px {DARK_SHDW}, 0 0 0 2px rgba(204,213,174,0.55)",
input_placeholder_color=TEXT_MUTED,
input_placeholder_color_dark="#c8a878",
# ── Slider ───────────────────────────────────────────────────────
slider_color=ACCENT,
slider_color_dark=ACCENT2,
# ── Checkboxes ───────────────────────────────────────────────────
checkbox_background_color=BG,
checkbox_background_color_dark="#a07040",
checkbox_background_color_selected=ACCENT,
checkbox_background_color_selected_dark="#a0ad80",
checkbox_border_width="0px",
checkbox_border_color="transparent",
checkbox_label_background_fill=BG,
checkbox_label_background_fill_dark="#9a7040",
checkbox_label_background_fill_hover=BG,
checkbox_label_background_fill_selected=ACCENT,
checkbox_label_background_fill_selected_dark="#a0ad80",
# ── Buttons ──────────────────────────────────────────────────────
button_primary_background_fill=f"linear-gradient(145deg, {ACCENT}, #b8c49a)",
button_primary_background_fill_dark=f"linear-gradient(145deg, #a0ad80, #8a9668)",
button_primary_background_fill_hover=f"linear-gradient(145deg, #b8c49a, {ACCENT})",
button_primary_text_color=TEXT_DARK,
button_primary_text_color_dark=CREAM,
button_primary_border_color="transparent",
button_primary_shadow=f"-5px -5px 10px {LIGHT_SHDW}, 5px 5px 10px {DARK_SHDW}",
button_primary_shadow_hover=f"-7px -7px 14px {LIGHT_SHDW}, 7px 7px 14px {DARK_SHDW}",
button_primary_shadow_active=f"inset -3px -3px 7px {LIGHT_SHDW}, inset 3px 3px 7px {DARK_SHDW}",
button_transition="box-shadow 0.15s ease, transform 0.1s ease",
button_secondary_background_fill=BG,
button_secondary_background_fill_dark="#9a7040",
button_secondary_background_fill_hover=BG,
button_secondary_text_color=TEXT_MID,
button_secondary_text_color_dark=CREAM,
button_secondary_border_color="transparent",
button_cancel_background_fill=f"linear-gradient(145deg, #e8c9a0, #d4b080)",
button_cancel_background_fill_dark=f"linear-gradient(145deg, #8a6140, #7a5130)",
button_cancel_text_color=TEXT_DARK,
button_cancel_text_color_dark=CREAM,
button_large_padding="10px 22px",
button_small_padding="5px 12px",
# ── Tables ───────────────────────────────────────────────────────
table_even_background_fill=INPUT_BG,
table_even_background_fill_dark="#9a7040",
table_odd_background_fill=BG,
table_odd_background_fill_dark="#a07040",
table_border_color="transparent",
table_row_focus=ACCENT2,
table_row_focus_dark="#b8895a",
# ── Typography ───────────────────────────────────────────────────
body_text_color=TEXT_DARK,
body_text_color_dark=CREAM,
body_text_color_subdued=TEXT_MUTED,
body_text_color_subdued_dark="#d4c8a8",
link_text_color="#7a8a5a",
link_text_color_dark=ACCENT,
link_text_color_hover="#5a6a3a",
link_text_color_hover_dark=ACCENT2,
# ── Code ─────────────────────────────────────────────────────────
code_background_fill=INPUT_BG,
code_background_fill_dark="#8a6140",
# ── Error / status ────────────────────────────────────────────────
error_background_fill="#e8c8a8",
error_background_fill_dark="#7a4820",
error_border_color="transparent",
error_text_color="#7a3820",
error_text_color_dark="#f5d0b0",
)
# ── Convenience instance ─────────────────────────────────────────────────────
theme = SageAndSandNeumorphic()
# ── Quick preview ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
with gr.Blocks(
theme=SageAndSandNeumorphic(),
css=NEUMORPHIC_CSS,
title="Sage & Sand Neumorphic β€” Preview",
) as demo:
gr.Markdown("## Sage & Sand Neumorphic")
gr.Markdown("A soft-extruded earthy theme for Stable Diffusion WebUI.")
with gr.Row():
with gr.Column():
prompt = gr.Textbox(label="Prompt", lines=3, placeholder="A serene landscape…")
neg_prompt = gr.Textbox(label="Negative prompt", lines=2, placeholder="blurry, watermark…")
with gr.Row():
steps = gr.Slider(1, 150, value=20, step=1, label="Steps")
cfg = gr.Slider(1, 30, value=7, step=0.5, label="CFG Scale")
with gr.Row():
sampler = gr.Dropdown(["Euler a", "DPM++ 2M", "DDIM"], value="Euler a", label="Sampler")
faces = gr.Checkbox(label="Restore faces")
with gr.Row():
gr.Button("Generate", variant="primary")
gr.Button("Interrupt", variant="stop")
with gr.Column():
gr.Image(label="Output", height=320)
with gr.Tabs():
with gr.Tab("Generation"):
with gr.Row():
gr.Slider(512, 2048, value=512, step=64, label="Width")
gr.Slider(512, 2048, value=512, step=64, label="Height")
gr.Slider(1, 8, value=1, step=1, label="Batch size")
with gr.Tab("ControlNet"):
gr.Textbox(label="ControlNet model", placeholder="control_v11p_sd15_openpose")
gr.Slider(0, 2, value=1, step=0.05, label="Control weight")
with gr.Tab("Extras"):
gr.Textbox(label="Notes", lines=4)
demo.launch()