diff --git a/src/stable/components/News/News.css b/src/stable/components/News/News.css
new file mode 100644
index 00000000..37297243
--- /dev/null
+++ b/src/stable/components/News/News.css
@@ -0,0 +1,81 @@
+@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,400&display=swap');
+.news-card {
+ min-width: 340px;
+ width: 100%;
+ box-sizing: border-box;
+ background: #fff;
+ border-radius: 8px;
+ padding: 20px 24px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
+}
+
+.news-header {
+ display: block;
+ margin-bottom: 4px;
+}
+
+.news-title {
+ font-family: 'Montserrat', sans-serif;
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 1;
+ letter-spacing: 0;
+ margin: 0;
+ display: inline;
+ color: #222;
+ cursor: pointer;
+}
+.news-title:hover .chevron-icon,
+.news-title .chevron-icon:hover {
+ transform: translateX(4px);
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.chevron-icon {
+ stroke: #333;
+ width: 1em;
+ height: 1em;
+ vertical-align: -0.15em;
+ margin-left: 0.15em;
+ display: inline-block;
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ pointer-events: auto;
+ cursor: pointer;
+}
+
+.news-meta {
+ margin-top: 10px;
+ display: flex;
+ align-items: center;
+ gap: 14px;
+}
+
+#news-date {
+ font-family: 'Montserrat', sans-serif;
+ font-weight: 300;
+ font-style: italic;
+ font-size: 14px;
+ line-height: 1;
+ letter-spacing: 0;
+ color: #444;
+ cursor: pointer;
+}
+#news-date:hover ~ .news-title .chevron-icon {
+ transform: translateX(4px);
+}
+
+.news-tags {
+ font-family: 'Montserrat', sans-serif;
+ font-weight: 300;
+ font-style: italic;
+ font-size: 14px;
+ line-height: 1;
+ letter-spacing: 0;
+ color: #444;
+ cursor: pointer;
+}
+.news-tags:hover ~ .news-title .chevron-icon {
+ transform: translateX(4px);
+}
+
+/*# sourceMappingURL=News.css.map */
diff --git a/src/stable/components/News/News.css.map b/src/stable/components/News/News.css.map
new file mode 100644
index 00000000..459716b8
--- /dev/null
+++ b/src/stable/components/News/News.css.map
@@ -0,0 +1 @@
+{"version":3,"sourceRoot":"","sources":["News.scss"],"names":[],"mappings":"AAAQ;AAER;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAQA;;AALA;AAAA;EAEE;EACA;;;AAKJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;EACE","file":"News.css"}
\ No newline at end of file
diff --git a/src/stable/components/News/News.js b/src/stable/components/News/News.js
new file mode 100644
index 00000000..c8a400d8
--- /dev/null
+++ b/src/stable/components/News/News.js
@@ -0,0 +1,78 @@
+import styles from '!!raw-loader!./News.css';
+
+const template = document.createElement('template');
+template.innerHTML = `
+
+
+
+`;
+
+class News extends HTMLElement {
+ constructor() {
+ super();
+ const shadow = this.attachShadow({ mode: 'open' });
+ shadow.appendChild(template.content.cloneNode(true));
+ }
+
+ static get observedAttributes() {
+ return ['datetime'];
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (name === 'datetime') {
+ this.formatDate(newValue);
+ }
+ }
+
+ connectedCallback() {
+ const date = this.getAttribute('datetime');
+ this.formatDate(date);
+ }
+
+ formatDate(dateStr) {
+ // Parse the string into a Date object
+ const date = new Date(dateStr);
+
+ // Validate the date
+ if (isNaN(date.getTime())) {
+ this.shadowRoot.querySelector('#news-date').textContent = 'Invalid date';
+ return;
+ }
+
+ // Format the date — e.g., "June 1, 2024"
+ const formatted = date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+
+ // Insert the formatted date into the DOM
+ const dateElement = this.shadowRoot.querySelector('#news-date');
+ if (dateElement) {
+ dateElement.textContent = formatted;
+ }
+ }
+}
+
+export { News as default };
diff --git a/src/stable/components/News/News.scss b/src/stable/components/News/News.scss
new file mode 100644
index 00000000..744f7b34
--- /dev/null
+++ b/src/stable/components/News/News.scss
@@ -0,0 +1,86 @@
+@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,400&display=swap');
+
+.news-card {
+ min-width: 340px;
+ width: 100%;
+ box-sizing: border-box;
+ background: #fff;
+ border-radius: 8px;
+ padding: 20px 24px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
+}
+
+.news-header {
+ display: block;
+ margin-bottom: 4px;
+}
+
+.news-title {
+ font-family: 'Montserrat', sans-serif;
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 1;
+ letter-spacing: 0;
+ margin: 0;
+ display: inline;
+ color: #222;
+
+ // Chevron hover effect
+ &:hover .chevron-icon,
+ .chevron-icon:hover {
+ transform: translateX(4px);
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ cursor: pointer;
+}
+
+.chevron-icon {
+ stroke: #333;
+ width: 1em;
+ height: 1em;
+ vertical-align: -0.15em;
+ margin-left: 0.15em;
+ display: inline-block;
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ pointer-events: auto;
+ cursor: pointer;
+}
+
+.news-meta {
+ margin-top: 10px;
+ display: flex;
+ align-items: center;
+ gap: 14px;
+}
+
+#news-date {
+ font-family: 'Montserrat', sans-serif;
+ font-weight: 300;
+ font-style: italic;
+ font-size: 14px;
+ line-height: 1;
+ letter-spacing: 0;
+ color: #444;
+ cursor: pointer;
+
+ // Chevron hover effect when hovering date
+ &:hover ~ .news-title .chevron-icon {
+ transform: translateX(4px);
+ }
+}
+
+.news-tags {
+ font-family: 'Montserrat', sans-serif;
+ font-weight: 300;
+ font-style: italic;
+ font-size: 14px;
+ line-height: 1;
+ letter-spacing: 0;
+ color: #444;
+ cursor: pointer;
+
+ // Chevron hover effect when hovering tags
+ &:hover ~ .news-title .chevron-icon {
+ transform: translateX(4px);
+ }
+}
diff --git a/src/stable/components/News/cod-news.js b/src/stable/components/News/cod-news.js
new file mode 100644
index 00000000..b06814a9
--- /dev/null
+++ b/src/stable/components/News/cod-news.js
@@ -0,0 +1,2 @@
+import News from './News';
+customElements.define('cod-news', News);
diff --git a/src/stable/docs/News.mdx b/src/stable/docs/News.mdx
new file mode 100644
index 00000000..0036eb15
--- /dev/null
+++ b/src/stable/docs/News.mdx
@@ -0,0 +1,94 @@
+import {
+ Meta,
+ Title,
+ Subtitle,
+ Description,
+ Primary,
+ Controls,
+ Stories,
+ Story,
+ Source,
+} from '@storybook/blocks';
+
+import * as NewsStories from '../stories/news.stories';
+
+import '../../../.storybook/docs'; // Import all documentation components
+
+
+
+# News
+
+
+ {' '}
+ The **News** component displays a stylized news card with a title, date, and optional
+ tags slot. It formats the `datetime` attribute automatically.
+
+
+## Examples
+
+### Basic
+
+
+
+
+
+## Slots
+
+
+
+
+## HTML Attributes / JS Properties
+
+Supported HTML attributes. If the attribute is reflected in a JS property, that attribute will have a 'reflects' tag next to the name.
+
+
+
+
+## Events
+
+
+
+
+## Methods
+
+
+
+
+## Custom CSS Properties
+
+
+
+
+## CSS Parts
+
+
+
+
+## Dependencies
+
+
+
+## Accessibility
+
+The News component uses semantic HTML:
+
+- The title is wrapped in a `` for hierarchy.
+- Date text is plain text.
+- The chevron icon uses `aria-hidden="true"` and is not focusable.
diff --git a/src/stable/index-stable.js b/src/stable/index-stable.js
index 50a9c274..ded78403 100644
--- a/src/stable/index-stable.js
+++ b/src/stable/index-stable.js
@@ -2,3 +2,4 @@ import './components/GovBanner/cod-gov-banner.js';
import './components/SectionNavigation/cod-section-navigation.js';
import './components/ServiceButton/cod-service-button.js';
import './components/Tag/cod-tag.js';
+import './components/News/cod-news.js';
diff --git a/src/stable/stories/news.stories.js b/src/stable/stories/news.stories.js
new file mode 100644
index 00000000..d5e97844
--- /dev/null
+++ b/src/stable/stories/news.stories.js
@@ -0,0 +1,81 @@
+import { html } from 'lit-html';
+import { expect } from '@storybook/test';
+import { userEvent } from '@storybook/test';
+import { waitFor } from '@storybook/test';
+import '../components/News/cod-news.js';
+import '../components/Tag/cod-tag.js';
+
+export default {
+ title: 'Components/News',
+ tags: ['stable'],
+};
+
+export const Basic = {
+ render: () => html`
+
+
+
+
+ Today's Headlines: Lorem ipsum dolor sit amet.
+
+
+ Breaking News
+
+
+ `,
+ // ✅ Click handler version:
+ play: async ({ canvasElement }) => {
+ const news = canvasElement.querySelector('cod-news');
+ expect(news).toBeTruthy();
+
+ await waitFor(() => {
+ if (!news.shadowRoot) throw new Error('Shadow root not ready');
+ });
+
+ const shadow = news.shadowRoot;
+
+ const slot = shadow.querySelector('#newsTitleSlot');
+ expect(slot).toBeTruthy();
+
+ await waitFor(() => {
+ if (slot.assignedNodes().length === 0) {
+ throw new Error('Slot has no assigned nodes');
+ }
+ });
+
+ const assigned = slot.assignedNodes({ flatten: true });
+ const link = assigned.find((n) => n.nodeType === Node.ELEMENT_NODE);
+ expect(link).toBeTruthy();
+ expect(link.tagName).toBe('A');
+ expect(link.getAttribute('href')).toBe('https://example.com/article');
+ expect(link.textContent).toContain("Today's Headlines");
+
+ // ✅ Prevent default to stay on Storybook page:
+ link.addEventListener('click', (e) => e.preventDefault());
+
+ await userEvent.click(link);
+
+ const dateEl = shadow.querySelector('#news-date');
+ expect(dateEl).toBeTruthy();
+ expect(dateEl.textContent).toBe('June 9, 2025');
+
+ const tagsSlot = shadow.querySelector('#tagsSlot');
+ expect(tagsSlot).toBeTruthy();
+ await waitFor(() => {
+ if (tagsSlot.assignedNodes().length === 0) {
+ throw new Error('Tags slot empty');
+ }
+ });
+
+ const tagNode = tagsSlot
+ .assignedNodes()
+ .find((n) => n.nodeType === Node.ELEMENT_NODE);
+ expect(tagNode).toBeTruthy();
+ expect(tagNode.tagName).toBe('COD-TAG');
+ },
+};