Calculate and Display Discounted Prices Using Only CSS
Introduction
CSS isn't just for styling—it can also perform calculations and display dynamic data without JavaScript. With modern CSS features like attr(), calc(), and the :has() selector, you can compute discounted prices directly in the browser. This eliminates the need for extra scripts, reduces latency, and saves browser resources. In this guide, you'll learn how to build a pricing interface similar to e-commerce sites, where a student discount is applied to subscription fees. We'll use only HTML and CSS, leveraging custom properties and logical selectors to show original prices, discounts, and final sale prices.

What You Need
- Basic familiarity with HTML and CSS (no JavaScript required)
- A modern browser that supports CSS
:has()andattr()with numeric values (check CanIUse) - A text editor for writing code (VS Code, Sublime, etc.)
- A sample list of items with prices and discount percentages (e.g., streaming services)
Step 1: Set Up the HTML Structure
Create a container for your product list. Each item will have a label with the service name, the base price stored in data-price, and the discount rate stored in data-discount. Use checkboxes to let users toggle selections and apply discounts.
<ul class="service-list">
<li>
<label>
<span>Netflix</span>
<div class="price" data-price="7.99" data-discount="0.2">$7.99</div>
<input type="checkbox" class="select-service">
</label>
<label>
<span>Apply Student Discount (20%)</span>
<input type="checkbox" class="apply-discount">
</label>
</li>
<!-- Repeat for Disney+, HBO, etc. -->
</ul>
Step 2: Style the Base Price and Discount Toggle
First, ensure the base price is displayed normally. Then, when the user checks the discount toggle, we'll cross out the original price and show the new discounted value. Use the :has() pseudo-class to detect the toggle state.
/* Initially show price normally */
.price { font-size: 1.2em; color: #333; }
/* When discount checkbox is checked inside the list item */
li:has(.apply-discount:checked) .price {
text-decoration: line-through;
color: #999;
}
Step 3: Retrieve Numeric Values from data-* Attributes
CSS can read data-* attributes using the attr() function. However, attr() currently returns a string. To use it in calculations, we must convert it to a number within a custom property. Combine attr() with calc() by assigning the attribute to a CSS variable.
li {
--base-price: attr(data-price number);
--discount: attr(data-discount number);
}
Note: The number type hint is still experimental. For practical use, you may need a @property registration or a fallback approach. We'll use a workaround in the next step.
Step 4: Calculate the Discounted Price
Once we have numeric values, we can compute the sale price: Original Price × (1 − Discount). Use calc() inside a custom property and then display that value using a pseudo-element.
li:has(.apply-discount:checked) .price::after {
content: "$" calc(var(--base-price) * (1 - var(--discount)));
}
But attr() may not be perfectly supported. A robust alternative is to hardcode the discount in CSS variables or use a @property declaration:
@property --price-value {
syntax: '<number>';
initial-value: 0;
inherits: false;
}
li {
--price-value: attr(data-price number);
--discount-value: attr(data-discount number);
}
Step 5: Display the Discounted Price
To show the sale price, we'll add a new element or a pseudo-element. Since we already have the .price div, we can append the discounted amount using ::after or create a separate span. For clarity, use a new span inside the list item that becomes visible only when the discount is applied.

<div class="price-wrapper">
<span class="original-price" data-price="7.99" data-discount="0.2">$7.99</span>
<span class="sale-price"></span>
</div>
Then in CSS:
li:has(.apply-discount:checked) .sale-price::before {
content: "Now $" calc(attr(data-price number) * (1 - attr(data-discount number)));
display: block;
color: green;
font-weight: bold;
}
Step 6: Add Visual Feedback and Interactivity
Enhance the user experience with transitions, hover effects, and clear labels. When the discount is enabled, the original price turns gray and gets a strikethrough, while the new price appears in a prominent color. Use the :has() selector to style multiple elements simultaneously.
li:has(.apply-discount:checked) {
background: #f0fff0;
border-left: 4px solid #28a745;
}
li:has(.apply-discount:checked) .original-price {
text-decoration: line-through;
opacity: 0.6;
}
li:has(.apply-discount:checked) .sale-price {
display: inline;
}
Step 7: Test for Browser Compatibility
Because :has() and numeric attr() are cutting-edge, test your implementation in Chrome Canary or with experimental flags. For production, consider a progressive enhancement approach—let JavaScript handle the calculation if CSS fails. You can also use @supports to conditionally load the CSS feature.
@supports selector(:has(*)) and (attr(data-price number)) {
/* Your discount CSS here */
}
Tips for Best Results
- Use fallbacks: Always provide a plain text price in case the CSS calculation isn't supported. You can hide the calculated price with
@supportsfallback. - Keep discounts as decimals: Store discount percentages as decimal fractions (e.g., 0.2 for 20%) to simplify calculations.
- Leverage
@property: Registering custom properties withsyntax: 'improves compatibility and prevents inheritance issues.' - Separate concerns: Use this method for simple displays; complex logic (tax, multiple discounts) still benefits from JavaScript.
- Ensure accessibility: Screen readers may not read computed content from pseudo-elements. Add an
aria-labelor a hidden span with the calculated price for assistive technologies. - Experiment with dynamic data: You can apply this technique to progress bars, countdown timers, or any other numeric indicator visible in the UI.