Example: Solar Savings Estimator
This example builds a solar savings calculator that uses a radio group, a slider, and a chart driven by a hidden data sheet.
Download the example spreadsheet to follow along or upload directly to NExS.
What We’re Building
A homeowner’s solar estimator that:
- Takes a monthly electric bill as a typed input
- Lets the user pick how sunny their location is with a radio group
- Sets the system size with a slider
- Calculates system cost, yearly savings, payback period, and 25-year savings
- Charts net savings year by year so the break-even point is easy to see
The workbook also shows a useful pattern: the visible sheet stays simple, while the assumptions and chart data live on separate sheets that users never see.
Step 1: Create the Excel Spreadsheet
The workbook has three worksheets plus NExS.app.
Calculator (the visible view)
| B | C | D | E | F | |
|---|---|---|---|---|---|
| 2 | Solar Savings Estimator | ||||
| 3 | See what solar panels could save you. | ||||
| 4 | |||||
| 5 | Monthly electric bill | $180 | |||
| 6 | |||||
| 7 | Sunshine where you live | 0 | Sunny | ||
| 8 | 1 | Average | |||
| 9 | 0 | Cloudy | |||
| 10 | |||||
| 11 | System size (kW) | (slider) | 6.0 | ||
| 12 | |||||
| 13 | |||||
| 14 | System cost | =Assumptions!B18 | |||
| 15 | Savings per year | =Assumptions!B17 | |||
| 16 | Payback (years) | =IF(F15>0,F14/F15,“”) | |||
| 17 | Savings over 25 years | =25*F15-F14 | |||
| 18 | |||||
| 19–31 | (chart area) |
Notes on the layout:
- B2:F2 and B3:F3 are merged for the title and subtitle.
- C7:C9 hold the radio group. Exactly one cell is
1, and the rest are0. - C11:E11 is merged to give the slider room. The slider writes its value into F11.
- B19:F31 is left empty. The chart is drawn over these cells.
Assumptions (hidden from users)
All the constants and the intermediate math live here, so the Calculator sheet only shows inputs and results.
| A | B | |
|---|---|---|
| 1 | Assumptions | |
| 3 | Electricity rate ($/kWh) | 0.18 |
| 4 | Installed cost ($ per watt) | 2.40 |
| 5 | Fixed install cost | 2000 |
| 6 | System efficiency | 80% |
| 8 | Peak sun hours per day | |
| 9 | Sunny | 5.5 |
| 10 | Average | 4.5 |
| 11 | Cloudy | 3.5 |
| 13 | Calculations | |
| 14 | Sun hours used | =IF(Calculator!C7=1,B9,IF(Calculator!C9=1,B11,B10)) |
| 15 | Yearly production (kWh) | =Calculator!F11*B14*365*B6 |
| 16 | Yearly usage (kWh) | =Calculator!F5/B3*12 |
| 17 | Yearly savings | =MIN(B15,B16)*B3 |
| 18 | System cost | =B5+Calculator!F11*1000*B4 |
B14 turns the radio group into a single number: it uses the Sunny or Cloudy hours when that option is selected, and falls back to Average otherwise. B17 caps savings at the household’s actual usage, so oversizing the system doesn’t inflate the result.
Data (chart source)
| A | B | |
|---|---|---|
| 1 | Savings year by year [Net savings ($)] | |
| 2 | Year | Net savings |
| 3 | 1 | =A3*Calculator!$F$15-Calculator!$F$14 |
| 4 | 2 | =A4*Calculator!$F$15-Calculator!$F$14 |
| … | … | … |
| 27 | 25 | =A27*Calculator!$F$15-Calculator!$F$14 |
Fill the formula in B3 down to B27. Each row is the cumulative savings minus the up-front cost, so the bars start negative and cross zero in the payback year.
Formatting
- F5, F14, F15, F17: Currency, 0 decimals (
$#,##0) - F11, F16: Number, 1 decimal (
0.0) - C7:C9: Custom format
"●";;"○", which shows a filled dot for1and an empty dot for0, so the radio cells still look right in Excel - Assumptions B3:B4: Currency, 2 decimals. B6: Percentage
- Data B3:B27: Currency, 0 decimals
- B2, B17, and the result values: Bold
Step 2: Create the NExS.app Worksheet
Create a new worksheet named NExS.app:
| A | B | C | D | |
|---|---|---|---|---|
| 1 | # NExS app configuration | |||
| 2 | app | Solar Savings | ||
| 3 | view | Calculator!B2:F31 | ||
| 4 | name | Main | ||
| 5 | input | F5 | ||
| 6 | radio | C7:C9 | ||
| 7 | slider | |||
| 8 | cell | C11 | ||
| 9 | linkedCell | F11 | ||
| 10 | min | 2 | ||
| 11 | max | 12 | ||
| 12 | step | 0.5 | ||
| 13 | chart | B19:F31 | ||
| 14 | source | Data!A1:B27 | ||
| 15 | type | column | ||
| 16 | legend | none | ||
| 17 | end |
What each part does:
view: OnlyCalculator!B2:F31is shown. The Assumptions and Data sheets still calculate, but users never see them.input F5: The monthly bill is a typed input.radio C7:C9: The three sunshine cells become one radio group. Selecting one sets it to1and the others to0. See Radio Groups.slider: Drawn in C11 and linked to F11, it ranges from 2 to 12 kW in 0.5 kW steps. F11 doesn’t need its owninputline, because the slider’slinkedCellmakes it editable. See Sliders.chart: A column chart over B19:F31, fed from the Data sheet. With one data series a legend adds nothing, so it’s turned off. See Charts.#rows are comments, andendmarks the end of the configuration.
Tip: The NExS Excel Add-in generated this
NExS.appsheet. You can build it by hand as shown above, or let the add-in write it for you.
Step 3: Upload and Test
- Save as
.xlsx - Upload to platform.nexs.com
- Drag the slider and watch the chart’s break-even bar move
- Switch between Sunny and Cloudy. A cloudier location produces less power, so payback takes longer
Step 4: Embed on Your Website
Script Embed (Recommended)
<!-- Replace this UUID with your own app's ID if building from scratch -->
<script class="nexs"
src="https://static.nexs.com/js/nexs_embed.js"
data-nexs-app-url="https://platform.nexs.com/app/17eadd5c-e49c-49a4-ba11-6db81add35b2">
</script>
Custom UI with Declarative API
Use the same workbook to drive your own interface. Every control is bound with the nexs__input class (see the Declarative API Reference):
- Monthly bill: a text input bound to F5. A text input receives the cell’s raw value (
180), so a shortnexsappupdatedlistener replaces it with the formatted text ($180) whenever the app updates, unless the user is typing in it. - System size: a range input bound to F11, with the same min, max, and step as the slider in
NExS.app. The kW label next to it is anexs__output_formattedspan on F11, so it always shows the value the app is using. The range input only sends its value when the user releases it, so a small tooltip above the thumb follows theinputevent to show the value while dragging. - Sunshine: three radio buttons with
value="1", each bound to its own cell (C7, C8, C9). The workbook’s radio group spans three cells, and a declarative radio only sets its own cell to1, so a short listener sets the other two cells to0.
Try It
This custom page is running live against the published Solar Savings app. The NExS app itself is hidden, and only the custom controls are shown. To see the original spreadsheet view, open the Solar Savings app in a new window.
Solar Savings Estimator
Savings per year:
Payback: years
The Code
<!DOCTYPE html>
<html lang="en">
<head>
<title>Solar Savings Estimator</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 440px;
margin: 40px auto;
padding: 20px;
}
h1 {
color: #333;
text-align: center;
}
.card {
background: #f8f9fa;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.field {
margin-bottom: 20px;
}
.field-label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #555;
}
.field input[type="text"] {
width: 100%;
padding: 12px;
font-size: 18px;
border: 2px solid #ddd;
border-radius: 8px;
box-sizing: border-box;
}
.field input[type="range"] {
width: 100%;
margin: 0;
accent-color: #f5a623;
}
/* Tooltip that follows the slider thumb while dragging */
.slider-wrap {
position: relative;
}
.slider-tip {
position: absolute;
bottom: calc(100% + 8px);
/* --pct is 0 to 1; 16px is roughly the thumb width */
left: calc(var(--pct, 0.4) * (100% - 16px) + 8px);
transform: translateX(-50%);
padding: 4px 8px;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
color: white;
background: #333;
border-radius: 4px;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
}
.slider-tip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #333;
}
.slider-wrap:hover .slider-tip,
.slider-wrap:focus-within .slider-tip {
opacity: 1;
}
.sun-options {
display: flex;
gap: 10px;
}
/* Radio buttons styled as buttons: the input is hidden, the label is the button */
.sun-option {
flex: 1;
position: relative;
padding: 12px;
font-size: 16px;
text-align: center;
border: 2px solid #ddd;
border-radius: 8px;
background: white;
cursor: pointer;
}
.sun-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.sun-option:has(input:checked) {
background: #f5a623;
color: white;
border-color: #f5a623;
}
.sun-option:has(input:focus-visible) {
outline: 2px solid #333;
outline-offset: 2px;
}
.results {
background: #f5a623;
color: white;
border-radius: 8px;
padding: 20px;
text-align: center;
}
.results .label {
font-size: 14px;
opacity: 0.9;
}
.results .amount {
font-size: 36px;
font-weight: bold;
margin: 5px 0;
}
.results .detail {
font-size: 14px;
opacity: 0.9;
line-height: 1.6;
}
</style>
</head>
<body>
<h1>Solar Savings Estimator</h1>
<div class="card">
<div class="field">
<label class="field-label" for="bill">Monthly Electric Bill</label>
<input type="text"
id="bill"
class="nexs__input"
data-nexs-cell-addr="F5"
placeholder="$180">
</div>
<div class="field">
<span class="field-label">Sunshine Where You Live</span>
<div class="sun-options">
<label class="sun-option">
<input type="radio" name="sun" class="nexs__input"
data-nexs-cell-addr="C7" value="1"> Sunny
</label>
<label class="sun-option">
<input type="radio" name="sun" class="nexs__input"
data-nexs-cell-addr="C8" value="1" checked> Average
</label>
<label class="sun-option">
<input type="radio" name="sun" class="nexs__input"
data-nexs-cell-addr="C9" value="1"> Cloudy
</label>
</div>
</div>
<div class="field">
<label class="field-label" for="size">
System Size:
<span class="nexs__output_formatted" data-nexs-cell-addr="F11">6.0</span> kW
</label>
<div class="slider-wrap">
<input type="range"
id="size"
class="nexs__input"
data-nexs-cell-addr="F11"
min="2" max="12" step="0.5" value="6">
<output class="slider-tip" for="size">6.0 kW</output>
</div>
</div>
<div class="results">
<div class="label">Savings Over 25 Years</div>
<div class="amount">
<span class="nexs__output_formatted" data-nexs-cell-addr="F17"></span>
</div>
<div class="detail">
System cost: <span class="nexs__output_formatted" data-nexs-cell-addr="F14"></span><br>
Savings per year: <span class="nexs__output_formatted" data-nexs-cell-addr="F15"></span><br>
Payback: <span class="nexs__output_formatted" data-nexs-cell-addr="F16"></span> years
</div>
</div>
</div>
<!-- NExS app (handles calculations) -->
<!-- Replace this UUID with your own app's ID if building from scratch -->
<script class="nexs"
src="https://static.nexs.com/js/nexs_embed.js"
data-nexs-app-url="https://platform.nexs.com/app/17eadd5c-e49c-49a4-ba11-6db81add35b2">
</script>
<script>
// Each radio sets its own cell to 1. Clear the other two so
// exactly one sunshine cell is 1, as the workbook expects.
const sunRadios = document.querySelectorAll('input[name="sun"]');
sunRadios.forEach(radio => {
radio.addEventListener('change', () => {
const others = [...sunRadios].filter(r => r !== radio);
NEXS.setInputs(others.flatMap(r => [r.dataset.nexsCellAddr, 0]));
});
});
// The range input only updates F11 when released. Show the value
// in a tooltip above the thumb while the user drags.
const slider = document.getElementById('size');
const tip = document.querySelector('.slider-tip');
function updateTip() {
tip.textContent = Number(slider.value).toFixed(1) + ' kW';
tip.style.setProperty('--pct', (slider.value - slider.min) / (slider.max - slider.min));
}
slider.addEventListener('input', updateTip);
updateTip();
// Text inputs receive the cell's raw value (180). After each update,
// show the formatted text instead ($180), except while the user is typing.
document.addEventListener('nexsappupdated', () => {
document.querySelectorAll('input[type="text"].nexs__input').forEach(input => {
if (input !== document.activeElement) {
input.value = NEXS.cell(input.dataset.nexsCellAddr).text.trim();
}
});
});
</script>
</body>
</html>
To hide the spreadsheet view and show only your custom controls, as the demo above does, wrap the NExS script in <div style="display:none;">. See Hiding the NExS App.
Enhancements
Make the Electricity Rate an Input
Electricity prices vary a lot by region. Move the rate onto the Calculator sheet so users can enter their own:
| B | F | |
|---|---|---|
| 12 | Electricity rate ($/kWh) | 0.18 |
Then point Assumptions!B3 at it with =Calculator!F12, and add it to the view:
| A | B | C | |
|---|---|---|---|
| input | F5, F12 |
Show a Payback Status Light
Add an LED in E16 that turns green when the system pays for itself within 10 years:
Formula in E16:
=IF(F16<=10,"green","red")
NExS.app:
| A | B | C | D | |
|---|---|---|---|---|
| led | ||||
| cell | E16 |
What You Learned
- Keeping assumptions and intermediate math on a separate sheet that isn’t part of the view
- Turning a set of cells into a radio group, and reading the selection with
IF - Using a custom number format so radio cells display as dots in Excel
- Linking a slider to an input cell with
min,max, andstep - Drawing a chart over a cell range from a hidden data sheet
- Binding a custom UI with
nexs__inputtext, range, and radio inputs, and keeping a multi-cell radio group in sync withNEXS.setInputs
Next Steps
- Tip Calculator Example — Start with the basics of inputs and outputs
- Sales Dashboard Example — Build a multi-view dashboard with several charts
