JOUR 73361

Coding the News

Learn how America's top news organizations escape rigid publishing systems to design beautiful data-driven stories on deadline.

Ben Welsh, Adjunct Assistant Professor

Spring 2026

Mondays 6–9 p.m.

Lab 436

Week 4

Interactive Inputs

How to engage your readers with reactive components

Feb. 23, 2026

Part 1: Introduction to reactivity

This week we'll venture further into what you can do with Svelte by learning how to create reactive components.

Reactivity is the arcane term that developers use to describe features that respond instantly to new data or user input. It's what allows news developers to create engaging applications that wouldn't be possible in print or a one-way broadcast.

Interactive features can take many different forms. They can be quizzes, calculators, polls, games, maps, dashboards, rankings and anything else that allows the page to automatically update in response to user actions.

A famous example is "How Y’all, Youse and You Guys Talk," a dialect quiz published in 2013 by Josh Katz and Wilson Andrews of The New York Times.

How Y'all, Youse and You Guys Talk
https://www.nytimes.com/interactive/2014/upshot/dialect-quiz-map.html
The New York Times dialect quiz showing a heat map of the United States based on reader answers about regional vocabulary

It asks readers a series of questions about the words they choose — do you say "sneakers" or "tennis shoes"? — and then generates a personalized heat map that pinpoints where they are likely to have grown up. It rocketed across the web, ranking as the The Times' most popular page of the year, and earning Katz, who authored it as an intern, a full-time job.

"I’m pretty blown away by the response to the whole thing."

— Josh Katz

Today we're going to learn the fundamentals you need to build interactive components like the Times quiz. Each part will introduce a concept you need to know.

Create your repository

Before we begin, open your browser and navigate to our template repository at github.com/palewire/cuny-jour-static-site-template.

cuny-jour-static-site-template
https://github.com/palewire/cuny-jour-static-site-template
GitHub template repository page showing the Use this template button

Just like our previous classes, click the green "Use this template" button near the top right of the page. This week, let's name our repository my-first-interactive-components. Make sure "Public" is selected, then click "Create repository."

Clone the repository and its code onto your computer using the techniques we covered in week one. Open it in Visual Studio Code. Start a terminal and run npm install to fetch the template's dependencies. These steps should start to feel like second nature.

Part 2: Introduction to state

In programming, "state" is the name for any piece of information stored by the application that can change over time. A toggle button has state: it's either on or off. A news quiz's state updates as readers answer each question. Every time you buy something online, you gradually update the state of your shopping cart.

In Svelte, we manage state using another one of its magic keywords. Fittingly, it's called $state(). It tells Svelte to pay special attention to a JavaScript variable and update the page whenever it changes. All you have to do is wrap a default value in $state(), and Svelte takes care of the rest.

let myReactiveVariable = $state(initialValue);

Let's ease into using state by building a component that shows or hides content whenever you click a button. This kind of feature often appears on news websites to give readers control over whether they want to expand a story to read more.

Creating a ReadMore component

We'll illustrate this by building a component that shows a "Read more" button. When clicked, it will reveal hidden content. Click again and the content will disappear.

Create a new file at src/lib/components/ directory we used last week. Call it ReadMore.svelte. Start with a simple script block at the top.

<script>
  let { title, children } = $props();
  let isExpanded = $state(false);
</script>

The first line uses the $props() function you learned last week. The title variable will be the headline at the top of our component. The children variable will represent whatever text should be hidden or revealed in the component we're building.

The new trick is the $state() function on the second line. It will be stored as a variable called isExpanded and the default, starting value is false. This means that when the component first loads, isExpanded will be false, and the content will be hidden.

Now let's add the markup that will display the button and content.

<script>
  let { title, children } = $props();
  let isExpanded = $state(false);
</script>

<div class="read-more">
  <button onclick={() => isExpanded = !isExpanded}>
    {isExpanded ? '▼' : '▶'} {title}
  </button>
  {#if isExpanded}
    <div class="content">
      {@render children()}
    </div>
  {/if}
</div>

The onclick attribute is an event handler. It tells Svelte what to do when the user clicks the button. In this case, we're using a function that flips the value of isExpanded. If it's false, it becomes true. If it's true, it becomes false. This is a common pattern called toggling.

The {isExpanded ? '▼' : '▶'} syntax is called a ternary operator. It's a shorthand way of saying "if isExpanded is true, show a down arrow; otherwise, show a right arrow." This gives the user a visual cue about whether the content is expanded or collapsed.

You can see that our title variable is being used right after the arrow. When we use the ReadMore component, whatever we pass as the title property will appear here.

The {#if isExpanded} block is Svelte's conditional rendering syntax. Everything inside this block only appears on the page when isExpanded is true. When the user clicks the button and isExpanded becomes true, Svelte automatically adds this content to the page. When it becomes false, Svelte removes it.

Finally, let's add some styling. Copy and paste this into the bottom of the file. Don't worry about the particulars of the CSS. It's boilerplate to make the component look decent.

<script>
  let { title, children } = $props();
  let isExpanded = $state(false);
</script>

<div class="read-more">
  <button onclick={() => isExpanded = !isExpanded}>
    {isExpanded ? '▼' : '▶'} {title}
  </button>
  {#if isExpanded}
    <div class="content">
      {@render children()}
    </div>
  {/if}
</div>

<style>
  .read-more {
    margin: 1.5rem 0;
    border: 1px solid var(--color-light-gray);
  }

  button {
    width: 100%;
    padding: 1rem;
    text-align: left;
    background: var(--color-light-gray);
    border: none;
    font-weight: bold;
    cursor: pointer;
  }

  .content {
    padding: 1rem;
    border-top: 1px solid var(--color-light-gray);
  }
</style>

Using it on your page

Save your ReadMore.svelte file and open src/routes/+page.svelte. Add an import at the top of the script block:

import ReadMore from '$lib/components/ReadMore.svelte';

Now add the component somewhere inside the ArticleBody. We'll pass in the same title and content that Katz and Andrews used in their dialect quiz:

<ReadMore title="About This Quiz">
  <p>
    The data for the quiz and maps shown here come from over
    350,000 survey responses collected from August to October 2013
    by Josh Katz, a graphics editor for the New York Times who developed
    this quiz and has since written “Speaking American,” a visual
    exploration of American regional dialects.
  </p>

  <p>
    Most of the questions used in this quiz are based on those in
    the Harvard Dialect Survey, a linguistics project begun in 2002 by
    Bert Vaux and Scott Golder. The original questions and results for
    that survey can be found on Dr. Vaux’s current website.
  </p>

  <p>
    The colors on the large heat map correspond to the probability
    that a randomly selected person in that location would respond
    to a randomly selected survey question the same way that you did.
    The three smaller maps show which answer most contributed to those
    cities being named the most (or least) similar to you.
  </p>
</ReadMore>

Save the file. As we've done in previous weeks, open a terminal and execute npm run dev to start the development server.

When you check your browser at localhost:5173. You should see a button with our title and a right arrow. Click it, and the methodology text appears. Click again, and it disappears.

Become a force for good. Join our next class.
https://localhost:5173
A ReadMore component showing the title 'About This Quiz' with a right arrow. When clicked, the arrow points down and a paragraph of text appears below.

Try adding a few more ReadMore components with methodologies from more recent Katz stories.

<ReadMore title="About the data">
  <p>
    Our analysis is based on data shared by researchers at the health research group KFF,
    calculated using insurance premiums published on healthcare.gov and state insurance
    marketplaces for the 2026 plan year.
  </p>

  <p>
    Premiums are calculated based on the price of the “benchmark” silver plan averaged across the
    entire nation for an individual 60-year-old. The national averages assume every state uses the
    most common 3:1 age rating calculation. Tax credits and thresholds are calculated using I.R.S.
    income thresholds for each year and the federal poverty level for the contiguous United
    States. Numbers for Alaska and Hawaii differ. Dollars are not inflation-adjusted.
  </p>
</ReadMore>

<ReadMore title="About the data">
  <p>
    For this project, we reached out to dozens of historians and political scientists, including
    some participants of C-SPAN’s Presidential Historians Survey. We asked them to provide us with
    relevant precedent to specific Trump actions, if there were any, and to describe how those
    precedents were and were not similar to what Mr. Trump has done.
  </p>
</ReadMore>

Notice how each ReadMore component maintains its own state. Clicking one doesn't affect the others.

Become a force for good. Join our next class.
https://localhost:5173
Three ReadMore components stacked on top of each other, each with its own title and content. Two are toggled open to show the content. One is toggled closed to hide the content.

This is because each component instance has its own isExpanded variable. This independence is another powerful feature of component-based design.

Part 3: Introduction to derived values

Managing state is only the beginning of the story. The reactive variables you define can serve as the foundation for other values, triggering ripple effects across the page as the user manipulates an input.

That's where the $derived() function comes in. As its name suggests, it is based on the current state of other variables. When those variables change, the derived value automatically recalculates.

let myDerivedValue = $derived(someState + anotherState);

Let's learn how to use $derived() by building a subscription cost calculator. You'll check off which streaming services you subscribe to and the component will tally up your monthly bill.

Creating a StreamingCost component

Create a new file in the src/lib/components/ directory. Call it StreamingCost.svelte. Start with a simple script block at the top:

<script>
  let netflix = $state(false);
</script>

Add the markup that will display a clickable service button.

<script>
  let netflix = $state(false);
</script>

<div class="calculator">
  <h3>How much do your streaming subscriptions cost?</h3>

  <button class="service" onclick={() => netflix = !netflix}>
    <span class="indicator">{netflix ? '✓' : '○'}</span>
    <span class="name">Netflix</span>
    <span class="price">$17.99/mo</span>
  </button>
</div>

This pattern should feel familiar from Part 2. The onclick handler toggles netflix between true and false. The ternary {netflix ? '✓' : '○'} shows a check mark when selected or a circle when not.

Add some styling. Copy and paste this into the bottom of the file.

<script>
  let netflix = $state(false);
</script>

<div class="calculator">
   <h3>How much do your streaming subscriptions cost?</h3>

  <button class="service" onclick={() => netflix = !netflix}>
    <span class="indicator">{netflix ? '✓' : '○'}</span>
    <span class="name">Netflix</span>
    <span class="price">$17.99/mo</span>
  </button>
</div>

<style>
  .calculator {
    padding: 1.5rem;
    background: var(--color-light-gray);
    margin: 2rem 0;
    max-width: 400px;
  }

  h3 {
    margin: 0 0 1rem !important;
  }

  .service {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem;
    border: none;
    border-bottom: 1px solid #ddd;
    background: transparent;
    cursor: pointer;
    width: 100%;
    text-align: left;
  }

  .indicator {
    font-size: 1.25rem;
  }

  .name {
    flex: 1;
    font-weight: bold;
  }

  .price {
    color: #666;
  }
</style>

Save your StreamingCost.svelte file and open src/routes/+page.svelte. Add an import at the top of the script block:

import StreamingCost from '$lib/components/StreamingCost.svelte';

Place it in your page:

<StreamingCost />

Check your browser. You should see Netflix with a checkbox and price. Click it on and off.

Become a force for good. Join our next class.
https://localhost:5173
A StreamingCost component showing a button for Netflix with a checkbox and price. When clicked, the checkbox toggles between checked and unchecked.

The checkbox works, but nothing else happens yet. Let's add in the calculation.

Using $derived() for on-the-fly calculations

Now comes the magic. We want to show the total monthly cost. Let's add a $derived() value that calculates it:

<script>
  let netflix = $state(false);
  let total = $derived(netflix ? 17.99 : 0);
</script>

The $derived() function creates a value that recalculates automatically. The expression netflix ? 17.99 : 0 is another ternary operator. If netflix is true, it uses 17.99, the current standard price. Otherwise, it uses zero.

Add a summary section to display the total:

<div class="calculator">
  <h3>How much do your streaming subscriptions cost?</h3>

  <button class="service" onclick={() => netflix = !netflix}>
    <span class="indicator">{netflix ? '✓' : '○'}</span>
    <span class="name">Netflix</span>
    <span class="price">$17.99/mo</span>
  </button>

  <div class="summary">
    <span class="summary-label">Monthly Total:</span>
    <span class="summary-amount">${total.toFixed(2)}</span>
  </div>
</div>

We add toFixed(2) to the total to make sure it rounds the number to two decimal places, fixing a strange quirk of JavaScript that can result in far too many decimal places being printed.

Add styles for the summary:

.summary {
  display: flex;
  justify-content: space-between;
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 2px solid var(--color-dark-gray);
}

.summary-label {
  font-size: 1.125rem;
  }

.summary-amount {
  font-size: 1.125rem;
  font-weight: bold;
  color: var(--color-accent);
}

Save and test. Check Netflix on, and the total shows $17.99. Uncheck it, and you're back to $0.00.

Become a force for good. Join our next class.
https://localhost:5173
A StreamingCost component showing a button for Netflix with a checkbox and price. Below it is a summary section that shows the monthly total. When the Netflix checkbox is checked, the total shows $17.99. When unchecked, it shows $0.00.

This is $derived() in action. You change the input, and every value that depends on it recalculates automatically.

Adding more services

One service isn't much of a calculator. Let's add the rest of the major streaming platforms. First, we need to store the prices somewhere. A JavaScript object is perfect for this. Let's add the prices of each service to the top of our script block, and a state variable for each one:

  const prices = {
    netflix: 17.99,
    disney: 18.99,
    max: 18.49,
    hulu: 18.99,
    apple: 9.99,
    peacock: 10.99
  };

  let netflix = $state(false);
  let disney = $state(false);
  let max = $state(false);
  let hulu = $state(false);
  let apple = $state(false);
  let peacock = $state(false);

Below that, let's update the total variable to add up all the services instead of just Netflix:

let total = $derived(
  (netflix ? prices.netflix : 0) +
  (disney ? prices.disney : 0) +
  (max ? prices.max : 0) +
  (hulu ? prices.hulu : 0) +
  (apple ? prices.apple : 0) +
  (peacock ? prices.peacock : 0)
);

We now have six boolean state variables, one for each service. The total derived value adds up the price of each checked service.

Now update the markup with all six services. You should draw the cost labels from the prices object instead of hardcoding them in the markup.

<div class="calculator">
  <h3>How much do your streaming subscriptions cost?</h3>

  <button class="service" onclick={() => netflix = !netflix}>
    <span class="indicator">{netflix ? '✓' : '○'}</span>
    <span class="name">Netflix</span>
    <span class="price">${prices.netflix}/mo</span>
  </button>

  <button class="service" onclick={() => disney = !disney}>
    <span class="indicator">{disney ? '✓' : '○'}</span>
    <span class="name">Disney+</span>
    <span class="price">${prices.disney}/mo</span>
  </button>

  <button class="service" onclick={() => max = !max}>
    <span class="indicator">{max ? '✓' : '○'}</span>
    <span class="name">Max</span>
    <span class="price">${prices.max}/mo</span>
  </button>

  <button class="service" onclick={() => hulu = !hulu}>
    <span class="indicator">{hulu ? '✓' : '○'}</span>
    <span class="name">Hulu</span>
    <span class="price">${prices.hulu}/mo</span>
  </button>

  <button class="service" onclick={() => apple = !apple}>
    <span class="indicator">{apple ? '✓' : '○'}</span>
    <span class="name">Apple TV+</span>
    <span class="price">${prices.apple}/mo</span>
  </button>

  <button class="service" onclick={() => peacock = !peacock}>
    <span class="indicator">{peacock ? '✓' : '○'}</span>
    <span class="name">Peacock</span>
    <span class="price">${prices.peacock}/mo</span>
  </button>

  <div class="summary">
    <span class="summary-label">Monthly Total</span>
    <span class="summary-amount">${total.toFixed(2)}</span>
  </div>
</div>

Save and check your browser. Click different combinations of services. See the monthly total climb as you add services, or shrink as you unsubscribe.

Become a force for good. Join our next class.
https://localhost:5173
A StreamingCost component showing buttons for Netflix, Disney+, Max, Hulu, Apple TV+ and Peacock. Each has a checkbox and price. Below is a summary section that shows the monthly total. As different services are checked and unchecked, the total updates to reflect the new sum.

Part 4: Introduction to binding

So far, we've handled simple interactions where the user clicks a button and the page changes. That's a nice start, but some inputs are more fluid than that. Think of a text box, which gradually updates as the user types in more text.

Svelte handles these situations with bind:value. It creates a live connection between an input and a $state variable. When the user types, drags or touches, everything stays in sync automatically.

Here's a simple example of a bound text input that will automatically print its contents on the page.

<script>
  let boundVariable = $state('');
</script>

<input type="text" bind:value={boundVariable}>
<p>Hello, {boundVariable}!</p>

Notice how the bind:value attribute on the input connects the text box to the bound variable. It's a simple trick with powerful implications.

Creating a TipCalculator component

Let's explore the power of bound variables by building a tip calculator. It will let users enter a bill amount and see the tip calculated instantly.

Create a new file in the src/lib/components/ directory. Call it TipCalculator.svelte. Start with a simple script block at top:

<script>
  let billAmount = $state(0);
  let tip = $derived(billAmount * 0.20);
</script>

The first line creates a state variable called billAmount with a default value of 0. The second line uses $derived() to calculate 20% of the bill using a fixed value we've entered into the code. Whenever billAmount changes, the tip will automatically recalculate at that rate.

Now let's add the markup that will display the input and result, much like your subscription survey. The key difference: You'll fit bind:value on a text input.

<script>
  let billAmount = $state(0);
  let tip = $derived(billAmount * 0.20);
</script>

<div class="calculator">
  <h3>Tip Calculator</h3>

  <div class="input-group">
    <label for="bill">Bill Amount</label>
    <div class="input-wrapper">
      <span class="currency">$</span>
      <input
        type="number"
        id="bill"
        bind:value={billAmount}
        min="0"
        step="0.01"
      >
    </div>
  </div>

  <div class="result">
    <span class="label">20% Tip</span>
    <span class="value">${tip.toFixed(2)}</span>
  </div>
</div>

Add some simple styles.

<style>
  .calculator {
    padding: 1.5rem;
    background: var(--color-light-gray);
    margin: 2rem 0;
    max-width: 400px;
  }

  h3 {
    margin: 0 0 1.5rem !important;
    font-weight: bold !important;
    font-size: 1.75rem !important;
    text-transform: uppercase;
  }

  .input-group {
    margin-bottom: 1.5rem;
  }

  label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: bold;
  }

  .input-wrapper {
    display: flex;
    align-items: center;
    border: 2px solid #ddd;
  }

  .currency {
    padding: 0.75rem;
    background: #eee;
    font-weight: bold;
  }

  input[type="number"] {
    flex: 1;
    padding: 0.75rem;
    border: none;
    font-size: 1.25rem;
    width: 100%;
  }

  .result {
    padding: 1rem;
    background: var(--color-accent);
    color: white;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .result .value {
    font-size: 1.5rem;
    font-weight: bold;
  }
</style>

Save your TipCalculator.svelte file and open src/routes/+page.svelte. Add an import at the top of the script block:

import TipCalculator from '$lib/components/TipCalculator.svelte';

Now place the component somewhere inside the ArticleBody, as we did with our other components.

<TipCalculator />

Check your browser. Type a bill amount and watch the tip calculate instantly.

Become a force for good. Join our next class.
https://localhost:5173
A TipCalculator component showing a bill amount input and a 20% tip calculation. When the user types a bill amount, the tip updates instantly.

Adding a custom tip percentage

Now let's complicate things and add a way to customize the percentage. In the script tag at the top of the component, add a new $state variable with default tip percentage. Then replace our tip variable with a $derived variable that calculates the amount based on the two inputs.

<script>
  let billAmount = $state(0);
  let tipPercent = $state(20);
  let tip = $derived(billAmount * (tipPercent / 100));
</script>

Down in the markup, add a range slider bound to the tipPercent variable that lets users adjust the value. Then update the final box to print the tipPercent in our bottom line.

<div class="calculator">
  <h3>Tip Calculator</h3>

  <div class="input-group">
    <label for="percent">Tip {tipPercent}%</label>
    <input
        type="range"
        id="percent"
        bind:value={tipPercent}
        min="10"
        max="30"
        step="1"
    >
  </div>

  <div class="input-group">
    <label for="bill">Bill Amount</label>
    <div class="input-wrapper">
      <span class="currency">$</span>
      <input
        type="number"
        id="bill"
        bind:value={billAmount}
        min="0"
        step="0.01"
      >
    </div>
  </div>

  <div class="result">
    <span class="label">{tipPercent}% Tip</span>
    <span class="value">${tip.toFixed(2)}</span>
  </div>
</div>

Save, reload and twiddle around a little to try it out.

Become a force for good. Join our next class.
https://localhost:5173
A TipCalculator component with a slider added.

Adding the total

Let's show the total bill including tip. Add another derived value to your script that calculates it.

<script>
  let billAmount = $state(0);
  let tipPercent = $state(20);
  let tip = $derived(billAmount * (tipPercent / 100));
  let total = $derived(billAmount + tip);
</script>

Add another result display. Wrap both results in a container:

<div class="result">
  <span class="label">{tipPercent}% Tip:</span>
  <span class="value">${tip.toFixed(2)}</span>
</div>
<div class="result">
  <span class="label">Total</span>
  <span class="value">${total.toFixed(2)}</span>
</div>

Boom. There it is.

Become a force for good. Join our next class.
https://localhost:5173
A TipCalculator component with a total added.

Splitting the bill

For the final feature, let's add the ability to split the bill among multiple people.

Add another state variable for the input and derived value with the new calculation.

<script>
  let billAmount = $state(0);
  let tipPercent = $state(20);
  let numPeople = $state(1);
  let tip = $derived(billAmount * (tipPercent / 100));
  let total = $derived(billAmount + tip);
  let perPerson = $derived(total / numPeople);
</script>

Add a third input for the number of people, after the tip slider and before the bill amount.

  <div class="input-group">
    <label for="percent">Tip {tipPercent}%</label>
    <input
        type="range"
        id="percent"
        bind:value={tipPercent}
        min="10"
        max="30"
        step="1"
    >
  </div>

  <div class="input-group">
    <label for="people">Split between {numPeople} {numPeople === 1 ? 'person' : 'people'}</label>
    <input type="number" id="people" bind:value={numPeople} min="1">
  </div>

  <div class="input-group">
    <label for="bill">Bill Amount</label>
    <div class="input-wrapper">
      <span class="currency">$</span>
      <input
        type="number"
        id="bill"
        bind:value={billAmount}
        min="0"
        step="0.01"
      >
    </div>
  </div>

Notice we use min="1" on the input to ensure we never go below 1 person. The label dynamically shows "person" or "people" using a ternary operator.

Add the per-person result. The {#if numPeople > 1} only shows this line when splitting among multiple people.

  <div class="result">
    <span class="label">{tipPercent}% Tip</span>
    <span class="value">${tip.toFixed(2)}</span>
  </div>
  <div class="result">
    <span class="label">Total</span>
    <span class="value">${total.toFixed(2)}</span>
  </div>
  {#if numPeople > 1}
  <div class="result">
      <span class="label">Per Person</span>
      <span class="value">${perPerson.toFixed(2)}</span>
  </div>
  {/if}

Save and check your browser. You should now be able to enter a bill amount, adjust the tip percentage and split the bill among multiple people.

Become a force for good. Join our next class.
https://localhost:5173
A TipCalculator component showing a bill amount input, a tip percentage slider, and a split between input. Below are result displays showing the tip amount, total, and per-person cost.

Congratulations, you've used $state, $derived and bind:value together to create an interactive calculator.

Part 5: Introduction to subcomponents

Let's return to our streaming cost calculator from Part 3. Look at the markup — it has six nearly identical button blocks, each using the native onclick handler to the toggle the checkout and update the page. It works fine, but there's a lot of repetition, and now that we have the bind syntax in our toolbox, we can do better.

Creating a ServiceButton component

The pattern we used for each service is:

<button class="service" onclick={() => netflix = !netflix}>
  <span class="indicator">{netflix ? '✓' : '○'}</span>
  <span class="name">Netflix</span>
  <span class="price">${prices.netflix}/mo</span>
</button>

Let's extract this into its own component. Create a new file called ServiceButton.svelte in the src/lib/components/ directory.

There's a new keyword here: $bindable(). This tells Svelte that a property can be bound from the parent component. The selected prop has a default value of false, but when the parent uses bind:selected, changes flow in both directions.

<script>
  let { name, price, selected = $bindable(false) } = $props();
</script>

<button class="service" onclick={() => selected = !selected}>
  <span class="indicator">{selected ? '✓' : '○'}</span>
  <span class="name">{name}</span>
  <span class="price">${price}/mo</span>
</button>

<style>
  .service {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem;
    border: none;
    border-bottom: 1px solid #ddd;
    background: transparent;
    cursor: pointer;
    width: 100%;
    text-align: left;
  }

  .indicator {
    font-size: 1.25rem;
  }

  .name {
    flex: 1;
    font-weight: bold;
  }

  .price {
    color: #666;
  }
</style>

Now let's update StreamingCost.svelte to use the new component. First, add an import at the top of the script block.

import ServiceButton from './ServiceButton.svelte';

Now replace all six button blocks with our new component. Instead of bind:value, we use bind:selected to match our property name.

<div class="calculator">

  <h3>How much do your streaming subscriptions cost?</h3>

  <ServiceButton name="Netflix" price={prices.netflix} bind:selected={netflix} />
  <ServiceButton name="Disney+" price={prices.disney} bind:selected={disney} />
  <ServiceButton name="Max" price={prices.max} bind:selected={max} />
  <ServiceButton name="Hulu" price={prices.hulu} bind:selected={hulu} />
  <ServiceButton name="Apple TV+" price={prices.apple} bind:selected={apple} />
  <ServiceButton name="Peacock" price={prices.peacock} bind:selected={peacock} />

  <div class="summary">
    <span class="summary-label">Monthly Total</span>
    <span class="summary-amount">${total.toFixed(2)}</span>
  </div>

</div>

The calculator works exactly as before, but now each service button is a clean, reusable component.

This pattern is fundamental to streamlining interactive applications. The parent stays in control of the data, while children handle their display logic.

Homework

Task 1: Build an interactive news component

Find a story published in the past year by a news organization you admire that would benefit from an interactive component. Then build it.

Your component should use at least two of the three reactive concepts we covered today: $state, $derived and bind:value. You should build it inside a fresh copy of our class template.

Some examples of what this might look like:

  • A story about rising grocery prices could have a calculator where readers enter their weekly grocery budget and see how inflation has changed what they're paying compared to a year ago.
  • A story about rent stabilization could have a tool where readers enter details about their building and see if they'd be affected.

Task 2: Prepare to present

Be ready to share you interactive component with the class and explain how you built it. You should be prepared to:

  • Show the story that inspired it and explain what editorial problem the component solves.
  • Trace the reactive flow: what state does it track, what user actions change it, and what values derive from it.
  • Discuss how you used AI in the process and what you had to adjust by hand.

Task 3: Make a pull request

Our guest this week, Casey Miller, shared how she submitted her work for review using GitHub's pull requests system. You should work through the "First Pull Request" tutorial and submit an addition to the moneyinpolitics.wtf dictionary.