diff --git a/docs/src/helpers/menus.js b/docs/src/helpers/menus.js
index fbb54555b..2a8644997 100644
--- a/docs/src/helpers/menus.js
+++ b/docs/src/helpers/menus.js
@@ -11,7 +11,7 @@ const componentPages = components.map(page => ({
export const topbarMenu = [
{
_id: 'framework',
- name: 'Tech Guide',
+ name: 'Docs',
url: '/introduction',
},
{
@@ -19,7 +19,6 @@ export const topbarMenu = [
name: 'UI Primitives',
url: '/usage',
},
-
{
_id: 'api',
name: 'API Reference',
diff --git a/docs/src/layouts/Homepage.css b/docs/src/layouts/Homepage.css
index 00c53df9d..f7a724693 100644
--- a/docs/src/layouts/Homepage.css
+++ b/docs/src/layouts/Homepage.css
@@ -71,6 +71,7 @@
background-image: linear-gradient(#000, transparent 300px), radial-gradient(100vw 1000px at 50% 50%, rgba(0, 0, 0, 0.8) 0, rgba(0, 0, 0, 0.7) 500px, rgba(0, 0, 0, 0) 800px);
background-image: linear-gradient(#000, transparent 300px), radial-gradient(100vw 1000px at 50% 50%, rgba(0, 0, 0, 0.8) 0, oklch(0.37 0.09 227.35) 500px, rgba(0, 0, 0, 0) 800px);
background-image: linear-gradient(#000, transparent 300px), radial-gradient(50% 50% at 50% 50%, oklch(0 0 0) 0%, transparent 800px);
+ background-image: linear-gradient(#000, transparent 300px), radial-gradient(100vw 1000px at 50% 50%, rgb(0 0 0 / 0%) 0, rgba(0, 0, 0, 0.7) 500px, rgba(0, 0, 0, 0) 800px);
pointer-events: none;
}
diff --git a/docs/src/pages/api/helpers/index.mdx b/docs/src/pages/api/helpers/index.mdx
index 8abdd8277..4a7d8c80f 100755
--- a/docs/src/pages/api/helpers/index.mdx
+++ b/docs/src/pages/api/helpers/index.mdx
@@ -4,6 +4,7 @@ title: Template Helpers
icon: book-open
description: Comprehensive index of all template helpers in Semantic UI
---
+import { UICards, UICard } from '@semantic-ui/core';
@@ -27,17 +28,61 @@ Template helpers can be used directly from any template
```sui
-{{formatDate myDate 'h:mm A'}}
+{formatDate myDate 'h:mm A'}
```
## Helper Categories
-* **[Array Helpers](/api/helpers/arrays)** - working with arrays and lists.
-* **[Comparison Helpers](/api/helpers/comparison)** - comparing values and making decisions.
-* **[CSS Helpers](/api/helpers/css)** - working with CSS classes and styles.
-* **[Date Helpers](/api/helpers/dates)** - formatting and manipulating dates.
-* **[Debug Helpers](/api/helpers/debug)** - debugging and logging within templates.
-* **[Logical Helpers](/api/helpers/logical)** - conditional checks and logical operations.
-* **[Numeric Helpers](/api/helpers/numeric)** - working with numbers.
-* **[Object Helpers](/api/helpers/objects)** - manipulating and transforming objects.
-* **[Reactivity Helpers](/api/helpers/reactivity)** - working with reactive computations.
-* **[String Helpers](/api/helpers/strings)** - string manipulation and formatting.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/src/pages/api/index.mdx b/docs/src/pages/api/index.mdx
index d6151f9d7..24b688814 100755
--- a/docs/src/pages/api/index.mdx
+++ b/docs/src/pages/api/index.mdx
@@ -4,3 +4,48 @@ icon: cpu
title: API Reference
description: An API reference documenting available packages
---
+import { UICards, UICard } from '@semantic-ui/core';
+
+## Reference Sections
+
+The API reference guides are designed to provide exhaustive documentation for the underlying `@semantic-ui` packages.
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/src/pages/introduction.mdx b/docs/src/pages/introduction.mdx
index 23ea3e1ff..694fa1396 100755
--- a/docs/src/pages/introduction.mdx
+++ b/docs/src/pages/introduction.mdx
@@ -4,19 +4,62 @@ icon: open book
title: Introduction
description: Learn About Semantic UI
---
+import { UICards, UICard } from '@semantic-ui/core';
import { Tabs, TabItem } from '@astrojs/starlight/components';
import PlaygroundExample from '@components/PlaygroundExample/PlaygroundExample.astro';
-## What is Semantic UI?
+The two main ways to use Semantic UI
+* **Creating UI Components** - Semantic provides all the tools to build custom UI components whether your goal is to build an inhouse design system, build a website, or contribute UI components to the open source community.
+* **Use UI Components** - Semantic provides an out of the box set of ui components similar to the original version or other UI frameworks
-Semantic UI is a UI framework built for authoring websites using [web components](/web-components) with signals based reactivity, an expressive shadow DOM templating syntax, robust theming using css layers and variables, and an integrated underlying UI component framework that supports SSR by default.
+> Semantic UI's component framework is designed to provide the missing toolkit to make web components easy to author and ship at scale, with features like reactivity, templating, tools for events and keybindings, and a robust css framework for styling.
-### Key Features
-Semantic includes the following core libraries:
-* An underlying [UI component framework](/components) that includes things like popups, dropdowns, modals, and form controls.
-* A robust [reactive templating language](/templates) that compiles to web components.
-* A signals based [reactivity library](/reactivity).
-* A modern 3kb jQuery-like replacement called [Query](/query) to handle DOM APIs
-* A higly-performant [rendering engine](/rendering) powered by [Lit](https://lit.dev) under the hood.
+## Creating UI Components
+Semantic provides several core libraries to make it easy to ship custom UI as web components.
+
+
+
+
+
+
+
+
+
+
+## Using UI Components
+
+If you prefer to use Semantic UI purely as a UI framework and not author your own custom UI, you can simply use web components directly wherever you write your code from `@semantic-ui/core`
+
+
diff --git a/docs/src/pages/ui/[...slug].astro b/docs/src/pages/ui/[...slug].astro
index 6d15f4e50..6224e9248 100644
--- a/docs/src/pages/ui/[...slug].astro
+++ b/docs/src/pages/ui/[...slug].astro
@@ -1,6 +1,6 @@
---
import { getCollection } from 'astro:content';
-import { each, toTitleCase, inArray, toTitleCase } from '@semantic-ui/utils';
+import { each, toTitleCase, inArray } from '@semantic-ui/utils';
import Definition from '@layouts/Definition.astro';
import SpecDefinition from '@components/SpecDefinition.astro';
import * as SpecExports from '@semantic-ui/specs';
@@ -32,6 +32,7 @@ const definitionDisplayed = inArray(displayedTab, ['singular', 'definition', 'pl
const { SpecReader, ...Specs } = SpecExports;
const spec = Specs[data.specName];
const componentSpec = new SpecReader(spec, { plural });
+console.log(componentSpec, plural);
const componentName = componentSpec.getComponentName();
const definition = componentSpec.getDefinition();
diff --git a/packages/specs/src/index.js b/packages/specs/src/index.js
index a9bbe8893..397a407fa 100644
--- a/packages/specs/src/index.js
+++ b/packages/specs/src/index.js
@@ -5,12 +5,13 @@ export { ContainerSpec, ContainerComponentSpec } from './specs/container/index.j
export { RailSpec, RailComponentSpec } from './specs/rail/index.js';
/* Primitives */
-export { ButtonSpec, ButtonComponentSpec } from './specs/button/index.js';
+export { ButtonSpec, ButtonPluralComponentSpec, ButtonComponentSpec } from './specs/button/index.js';
export { IconSpec, IconComponentSpec } from './specs/icon/index.js';
export { MenuSpec, MenuComponentSpec, MenuItemSpec, MenuItemComponentSpec } from './specs/menu/index.js';
export { LabelSpec, LabelComponentSpec } from './specs/label/index.js';
export { InputSpec, InputComponentSpec } from './specs/input/index.js';
export { SegmentSpec, SegmentComponentSpec } from './specs/segment/index.js';
+export { CardSpec, CardPluralComponentSpec, CardComponentSpec } from './specs/card/index.js';
/* Components */
export { ModalSpec, ModalComponentSpec } from './specs/modal/index.js';
diff --git a/packages/specs/src/spec-reader.js b/packages/specs/src/spec-reader.js
index a5ff416b1..6dcfa10b2 100644
--- a/packages/specs/src/spec-reader.js
+++ b/packages/specs/src/spec-reader.js
@@ -58,6 +58,7 @@ export class SpecReader {
types: [],
states: [],
variations: [],
+ settings: [],
};
// user can specify only portions of definition appears of a certain usage level
@@ -70,7 +71,7 @@ export class SpecReader {
const spec = this.spec;
// standard example
- const defaultContent = spec?.examples?.defaultContent;
+ const defaultContent = (plural) ? spec?.examples?.defaultPluralContent : spec?.examples?.defaultContent;
const defaultModifiers = values(spec?.examples?.defaultAttributes || {}).join(' ');
definition.types.push({
title: spec.name,
@@ -426,7 +427,164 @@ export class SpecReader {
for the web component. It is a subset of the component spec that can be searched quickly
and has a reduced filesize.
*/
- getWebComponentSpec(spec = this.spec) {
+ getWebComponentSpec(spec = this.spec, { plural = this.plural } = {}) {
+
+ if(spec == this.spec && this.componentSpec) {
+ return this.componentSpec;
+ }
+
+ let componentSpec = {
+ tagName: spec.tagName,
+ content: [],
+ contentAttributes: [],
+
+ types: [],
+ variations: [],
+ states: [],
+ events: [],
+ settings: [],
+
+ properties: [],
+ attributes: [],
+ optionAttributes: {},
+ propertyTypes: {},
+ allowedValues: {},
+ attributeClasses: [],
+ defaultValues: {},
+ inheritedPluralVariations: [],
+ };
+
+ const addSettingsFromPart = (section) => {
+ let specPart = spec[section] || [];
+
+ // plural spec uses a separate array to list which values are permitted to be shared
+ if(plural) {
+ const permittedListNames = {
+ types: 'pluralSharedTypes',
+ variations: 'pluralSharedVariations',
+ states: 'pluralSharedStates',
+ content: 'pluralSharedContent',
+ settings: 'pluralSharedSettings',
+ events: 'pluralSharedEvents',
+ };
+ const permittedListName = get(permittedListNames, section);
+ const permittedValues = get(spec, permittedListName) || [];
+ if(permittedListName) {
+ specPart = specPart.filter((spec) => {
+ const propertyName = this.getPropertyName(spec);
+ return inArray(propertyName, permittedValues);
+ });
+ }
+
+ // the section name will not corresponse to the section name passed in
+ section = section.replace('pluralOnly', '').toLowerCase();
+
+ }
+ each(specPart, (spec) => {
+ const propertyName = this.getPropertyName(spec);
+
+ // it is a requirement to have a property name defined
+ if(!propertyName) {
+ return;
+ }
+
+ // add to list of this grouping, i.e types: ['emphasis']
+ if(componentSpec[section]) {
+ componentSpec[section].push(propertyName);
+ }
+
+ // find allowed option values for this attribute i.e. emphasis: ['primary', 'secondary']
+ const allowedValues = this.getAllowedValues(spec);
+ if(allowedValues) {
+ componentSpec.allowedValues[propertyName] = allowedValues;
+ }
+
+ // find native type of this property i.e. String
+ const propertyType = this.getPropertyType(spec, section, allowedValues);
+ if (propertyType) {
+ componentSpec.propertyTypes[propertyName] = propertyType;
+ }
+
+ // find attribute name if it its not a function
+ const attributeName = this.getAttributeName(spec, propertyType);
+ if(attributeName) {
+ componentSpec.attributes.push(attributeName);
+ }
+ else {
+ componentSpec.properties.push(propertyName);
+ }
+
+ // find default values
+ const defaultValue = this.getDefaultValue(spec, propertyType, section);
+ if (defaultValue !== undefined) {
+ componentSpec.defaultValues[propertyName] = defaultValue;
+ }
+
+ /* Special Cases */
+
+ // "content" can be attribute or slot
+ if (section === 'content') {
+ if(spec.attribute) {
+ componentSpec.contentAttributes.push(spec.attribute);
+ }
+ else if(spec.slot) {
+ componentSpec.slots.push(spec.slot);
+ }
+ }
+
+ // attributes can opt in to having its attribute as a ui class name
+ // i.e. ['attached', 'left-attached'] includes 'attached' the attribute as a class
+ if (attributeName && spec.includeAttributeClass) {
+ componentSpec.attributeClasses.push(propertyName);
+ }
+
+ });
+ };
+
+ // Only process necessary parts of the spec
+ const singularParts = [
+ 'content',
+ 'types',
+ 'states',
+ 'variations',
+ 'settings',
+ 'events',
+ ];
+ each(singularParts, addSettingsFromPart);
+
+ if(plural) {
+ const pluralOnlyParts = [
+ 'pluralOnlyContent',
+ 'pluralOnlyTypes',
+ 'pluralOnlyStates',
+ 'pluralOnlySettings',
+ 'pluralOnlyVariations',
+ 'pluralOnlyEvents',
+ ];
+ each(pluralOnlyParts, addSettingsFromPart);
+ }
+
+ // avoid having to reverse array at runtime
+ let options = mapObject(componentSpec.allowedValues, (values, key) => {
+ return values = values.filter(value => isString(value));
+ });
+ componentSpec.optionAttributes = reverseKeys(options);
+
+ // store some details for plurality if present
+ componentSpec.inheritedPluralVariations = spec.pluralSharedVariations || [];
+
+ this.componentSpec = componentSpec;
+
+ return componentSpec;
+ }
+
+
+
+ /* This is a format that is consumed by defineComponent to determine valid attributes
+ for the web component. It is a subset of the component spec that can be searched quickly
+ and has a reduced filesize.
+ */
+ getPluralWebComponentSpec(spec = this.spec) {
if(spec == this.spec && this.componentSpec) {
return this.componentSpec;
@@ -488,7 +646,7 @@ export class SpecReader {
componentSpec.properties.push(propertyName);
}
- // get default value
+ // find default values
const defaultValue = this.getDefaultValue(spec, propertyType, section);
if (defaultValue !== undefined) {
componentSpec.defaultValues[propertyName] = defaultValue;
@@ -496,7 +654,7 @@ export class SpecReader {
/* Special Cases */
- // content can option to either using slots or attributes
+ // "content" can be attribute or slot
if (section === 'content') {
if(spec.attribute) {
componentSpec.contentAttributes.push(spec.attribute);
@@ -506,8 +664,8 @@ export class SpecReader {
}
}
- // components can opt in to having its attribute as a class name
- // i.e. .attached + .left-attached
+ // attributes can opt in to having its attribute as a ui class name
+ // i.e. ['attached', 'left-attached'] includes 'attached' the attribute as a class
if (attributeName && spec.includeAttributeClass) {
componentSpec.attributeClasses.push(propertyName);
}
@@ -520,6 +678,7 @@ export class SpecReader {
addSettingsFromPart('types');
addSettingsFromPart('states');
addSettingsFromPart('variations');
+ addSettingsFromPart('pluralVariations');
addSettingsFromPart('settings');
addSettingsFromPart('events');
diff --git a/packages/specs/src/specs/button/button.json b/packages/specs/src/specs/button/button.json
index 31b389216..a0ea2dbcd 100644
--- a/packages/specs/src/specs/button/button.json
+++ b/packages/specs/src/specs/button/button.json
@@ -4,6 +4,9 @@
"description": "A button indicates a possible user action",
"tagName": "ui-button",
"exportName": "UIButton",
+ "examples": {
+ "defaultPluralContent": "\n One\n Two\n Three\n"
+ },
"content": [
{
"name": "Icon",
diff --git a/packages/specs/src/specs/button/index.js b/packages/specs/src/specs/button/index.js
index 2ae35e239..524b28c03 100644
--- a/packages/specs/src/specs/button/index.js
+++ b/packages/specs/src/specs/button/index.js
@@ -2,11 +2,12 @@ import { SpecReader } from '@semantic-ui/specs';
import ButtonSpec from './button.json';
-const reader = new SpecReader();
-const ButtonComponentSpec = reader.getWebComponentSpec(ButtonSpec);
+const ButtonComponentSpec = new SpecReader(ButtonSpec).getWebComponentSpec();
+const ButtonPluralComponentSpec = new SpecReader(ButtonSpec, { plural: true }).getWebComponentSpec();
export default ButtonSpec;
export {
ButtonSpec,
- ButtonComponentSpec
+ ButtonComponentSpec,
+ ButtonPluralComponentSpec
};
diff --git a/packages/specs/src/specs/card/card.json b/packages/specs/src/specs/card/card.json
new file mode 100644
index 000000000..31b53ef51
--- /dev/null
+++ b/packages/specs/src/specs/card/card.json
@@ -0,0 +1,165 @@
+{
+ "uiType": "element",
+ "name": "Card",
+ "description": "A button indicates a possible user action",
+ "tagName": "ui-card",
+ "exportName": "UICard",
+ "content": [
+ {
+ "name": "Icon",
+ "includeAttributeClass": true,
+ "attribute": "icon",
+ "couplesWith": ["ui-icon"],
+ "description": "include an icon",
+ "exampleCode": "Pause"
+ },
+ {
+ "name": "Header",
+ "attribute": "header",
+ "description": "include an icon",
+ "exampleCode": "Get started with your new card."
+ },
+ {
+ "name": "Description",
+ "attribute": "description",
+ "description": "include a description",
+ "exampleCode": ""
+ },
+ {
+ "name": "Subheader",
+ "attribute": "subheader",
+ "description": "include an subheader",
+ "exampleCode": "Get started with your new card."
+ },
+ {
+ "name": "Image",
+ "attribute": "image",
+ "couplesWith": ["ui-image"],
+ "description": "include an image",
+ "exampleCode": "Automation"
+ }
+ ],
+ "types": [
+ {
+ "name": "Selection",
+ "attribute": "selection",
+ "description": "allow for selection between choices",
+ "usageLevel": 1
+ }
+ ],
+ "states": [
+ {
+ "name": "Hover",
+ "attribute": "hover",
+ "description": "be hovered"
+ },
+ {
+ "name": "Focus",
+ "attribute": "focused",
+ "description": "be focused by the keyboard"
+ },
+ {
+ "name": "Disabled",
+ "attribute": "disabled",
+ "includeAttributeClass": true,
+ "description": "have interactions disabled",
+ "options": [
+ {
+ "name": "Disabled",
+ "value": "disabled",
+ "description": "disable interactions"
+ },
+ {
+ "name": "Clickable Disabled",
+ "value": "clickable-disabled",
+ "description": "allow interactions but appear disabled"
+ }
+ ]
+ }
+ ],
+ "variations": [
+ {
+ "name": "Fluid",
+ "attribute": "fluid",
+ "usageLevel": 1,
+ "description": "take the width of its container"
+ },
+ {
+ "name": "Link",
+ "attribute": "link",
+ "usageLevel": 1,
+ "description": "can be formatted as if the card can be clicked"
+ },
+ {
+ "name": "Horizontal",
+ "attribute": "horizontal",
+ "usageLevel": 1,
+ "description": "can have content horizontally oriented"
+ }
+ ],
+ "settings": [
+ {
+ "name": "Href",
+ "type": "string",
+ "attribute": "href",
+ "description": "link to a url"
+ }
+ ],
+ "supportsPlural": true,
+ "pluralName": "Cards",
+ "pluralTagName": "ui-cards",
+ "pluralExportName": "UICards",
+ "pluralDescription": "Cards can exist together as a group",
+ "pluralContent": [],
+ "pluralSharedTypes": ["link"],
+ "pluralOnlyTypes": [],
+ "pluralOnlyVariations": [
+ {
+ "name": "doubling",
+ "attribute": "doubling",
+ "description": "A group of cards can double its column width for mobile",
+ "usageLevel": 3
+ },
+ {
+ "name": "stackable",
+ "attribute": "stackable",
+ "description": "A group of cards can automatically stack rows to a single columns on mobile devices",
+ "usageLevel": 3
+ },
+ {
+ "name": "Count",
+ "attribute": "count",
+ "description": "A group of cards can set how many cards should exist in a row",
+ "usageLevel": 3,
+ "options": [
+ {
+ "name": "Two",
+ "value": "two",
+ "description": "A card group can have two items per row"
+ },
+ {
+ "name": "Three",
+ "value": "three",
+ "description": "A card group can have three items per row"
+ },
+ {
+ "name": "Four",
+ "value": "four",
+ "description": "A card group can have four items per row"
+ },
+ {
+ "name": "Five",
+ "value": "five",
+ "description": "A card group can have five items per row"
+ },
+ {
+ "name": "Six",
+ "value": "six",
+ "description": "A card group can have six items per row"
+ }
+ ]
+ }
+ ],
+ "pluralSharedVariations": [
+ ]
+}
diff --git a/packages/specs/src/specs/card/index.js b/packages/specs/src/specs/card/index.js
new file mode 100644
index 000000000..3e3c176b3
--- /dev/null
+++ b/packages/specs/src/specs/card/index.js
@@ -0,0 +1,13 @@
+import { SpecReader } from '@semantic-ui/specs';
+
+import CardSpec from './card.json';
+
+const CardComponentSpec = new SpecReader(CardSpec).getWebComponentSpec();
+const CardPluralComponentSpec = new SpecReader(CardSpec, { plural: true }).getWebComponentSpec();
+
+export default CardSpec;
+export {
+ CardSpec,
+ CardComponentSpec,
+ CardPluralComponentSpec,
+};
diff --git a/packages/specs/src/specs/icon/icon.json b/packages/specs/src/specs/icon/icon.json
index a41309609..d491fac7e 100644
--- a/packages/specs/src/specs/icon/icon.json
+++ b/packages/specs/src/specs/icon/icon.json
@@ -462,7 +462,8 @@
"pluralTagName": "ui-icons",
"pluralDescription": "Icons can exist together as a group",
"pluralVariations": [
- "colored"
+ "colored",
+ "size"
],
"examples": {
"defaultAttributes": {
diff --git a/src/components/button/plural/buttons.js b/src/components/button/plural/buttons.js
index 9d4bbd6c7..322aa0fdb 100644
--- a/src/components/button/plural/buttons.js
+++ b/src/components/button/plural/buttons.js
@@ -1,5 +1,5 @@
import { defineComponent } from '@semantic-ui/component';
-import { ButtonComponentSpec } from '@semantic-ui/specs';
+import { ButtonPluralComponentSpec } from '@semantic-ui/specs';
import ButtonShadowCSS from '../css/button-shadow.css?raw';
import ButtonPageCSS from '../css/button-page.css?raw';
@@ -11,7 +11,7 @@ export const UIButtons = defineComponent({
plural: true,
singularTag: 'ui-button',
delegateFocus: true,
- componentSpec: ButtonComponentSpec,
+ componentSpec: ButtonPluralComponentSpec,
template: ButtonsTemplate,
css: ButtonShadowCSS,
pageCSS: ButtonPageCSS,
diff --git a/src/components/card/card.html b/src/components/card/card.html
new file mode 100644
index 000000000..d9240cead
--- /dev/null
+++ b/src/components/card/card.html
@@ -0,0 +1,65 @@
+{#if href}
+
+ {> content}
+
+{else}
+
+ {> content}
+
+{/if}
+
+{#snippet content}
+ {#if horizontal}
+ {>icon}
+
+ {>header}
+ {>image}
+ {>description}
+
+ {else}
+
+ {>header}
+ {>image}
+ {>description}
+
+ {/if}
+ {> slot}
+{/snippet}
+
+{#snippet icon}
+ {#if icon}
+
+
+
+ {/if}
+{/snippet}
+
+{#snippet header}
+
+{/snippet}
+
+{#snippet image}
+
+ {image}
+ {> slot image}
+
+{/snippet}
+
+{#snippet description}
+
+ {description}
+ {> slot description}
+
+{/snippet}
+
+{#snippet meta}
+
+ {#each value in meta}
+ {value}
+ {/each}
+ {> slot meta}
+
+{/snippet}
diff --git a/src/components/card/card.js b/src/components/card/card.js
new file mode 100644
index 000000000..5bbf37ec8
--- /dev/null
+++ b/src/components/card/card.js
@@ -0,0 +1,18 @@
+import { defineComponent } from '@semantic-ui/component';
+import { CardComponentSpec } from '@semantic-ui/specs';
+
+import CSS from './css/card-shadow.css?raw';
+import Template from './card.html?raw';
+
+const createComponent = ({$}) => ({
+});
+
+const UICard = defineComponent({
+ tagName: 'ui-card',
+ componentSpec: CardComponentSpec,
+ template: Template,
+ css: CSS,
+ createComponent,
+});
+
+export { UICard };
diff --git a/src/components/card/css/card-shadow.css b/src/components/card/css/card-shadow.css
new file mode 100644
index 000000000..ee49e16df
--- /dev/null
+++ b/src/components/card/css/card-shadow.css
@@ -0,0 +1,105 @@
+/* src/components/card/css/shadow/content/card.css */
+@layer component.card.content.card {
+ :host {
+ margin: var(--vertically-spaced);
+ }
+ .card {
+ display: block;
+ cursor: pointer;
+ text-decoration: none;
+ padding: var(--padding);
+ border-radius: var(--border-radius);
+ border: var(--internal-border);
+ transition: var(--transition);
+ box-shadow: var(--floating-shadow);
+ }
+ .card ::slotted(ui-icon),
+ .card ::slotted(.icon),
+ .card .icon {
+ font-size: var(--massive);
+ }
+ .card ::slotted(.header),
+ .card .header {
+ color: var(--primary-text-color);
+ font-size: var(--large);
+ font-weight: var(--bold);
+ transition: var(--transition);
+ }
+ .card ::slotted(.description),
+ .card .description {
+ color: var(--standard-60);
+ font-size: var(--medium);
+ transition: var(--transition);
+ }
+ .card ::slotted(.meta),
+ .card .meta {
+ margin-top: var(--compact-spacing);
+ padding-top: var(--compact-spacing);
+ border-top: var(--border);
+ display: flex;
+ gap: var(--compact-spacing);
+ }
+ .card .meta > * {
+ color: var(--standard-90);
+ font-weight: bold;
+ }
+ .link.card,
+ a.card {
+ cursor: pointer;
+ color: inherit;
+ }
+ .link.card:hover,
+ a.card:hover {
+ border: 1px solid var(--primary-text-color);
+ }
+ .four.cards ::slotted(*) {
+ flex-basis: 25%;
+ }
+ .horizontal {
+ display: flex;
+ flex-direction: row;
+ gap: 1rem;
+ }
+}
+
+/* src/components/card/css/shadow/plural/cards.css */
+@layer component.card.plural {
+ .cards {
+ display: grid;
+ grid-auto-rows: 1fr;
+ gap: 1rem;
+ width: 100%;
+ }
+ ::slotted(ui-card) {
+ display: contents;
+ }
+ .cards.one {
+ grid-template-columns: 1fr;
+ }
+ .cards.two {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ .cards.three {
+ grid-template-columns: repeat(3, 1fr);
+ }
+ .cards.four {
+ grid-template-columns: repeat(4, 1fr);
+ }
+ .cards.five {
+ grid-template-columns: repeat(5, 1fr);
+ }
+ .cards.six {
+ grid-template-columns: repeat(6, 1fr);
+ }
+ .cards.seven {
+ grid-template-columns: repeat(7, 1fr);
+ }
+ .cards.eight {
+ grid-template-columns: repeat(8, 1fr);
+ }
+}
+
+/* src/components/card/css/shadow/variations/fluid.css */
+@layer component.card.variations.fluid;
+
+/* src/components/card/css/shadow/card.css */
diff --git a/src/components/card/css/shadow/card.css b/src/components/card/css/shadow/card.css
new file mode 100644
index 000000000..d52a51f35
--- /dev/null
+++ b/src/components/card/css/shadow/card.css
@@ -0,0 +1,8 @@
+/* Content */
+@import url('./content/card.css') layer(component.card.content.card);
+
+/* Group */
+@import url('./plural/cards.css') layer(component.card.plural);
+
+/* Variations */
+@import url('./variations/fluid.css') layer(component.card.variations.fluid);
diff --git a/src/components/card/css/shadow/content/card.css b/src/components/card/css/shadow/content/card.css
new file mode 100644
index 000000000..fabd722d1
--- /dev/null
+++ b/src/components/card/css/shadow/content/card.css
@@ -0,0 +1,74 @@
+:host {
+ margin: var(--vertically-spaced);
+}
+
+.card {
+
+ display: block;
+ cursor: pointer;
+ text-decoration: none;
+ padding: var(--padding);
+ border-radius: var(--border-radius);
+ border: var(--internal-border);
+ transition: var(--transition);
+ box-shadow: var(--floating-shadow);
+
+ ::slotted(ui-icon),
+ ::slotted(.icon),
+ .icon {
+ font-size: var(--massive);
+ }
+
+ ::slotted(.header),
+ .header {
+ color: var(--primary-text-color);
+ font-size: var(--large);
+ font-weight: var(--bold);
+ transition: var(--transition);
+ }
+
+ ::slotted(.description),
+ .description {
+ color: var(--standard-60);
+ font-size: var(--medium);
+ transition: var(--transition);
+ }
+
+ ::slotted(.meta),
+ .meta {
+ margin-top: var(--compact-spacing);
+ padding-top: var(--compact-spacing);
+ border-top: var(--border);
+ display: flex;
+ gap: var(--compact-spacing);
+
+ & > * {
+ color: var(--standard-90);
+ font-weight: bold;
+ }
+ }
+
+
+}
+
+.link.card,
+a.card {
+ cursor: pointer;
+ color: inherit;
+
+ &:hover {
+ border: 1px solid var(--primary-text-color);
+ }
+}
+
+
+.four.cards ::slotted(*) {
+ flex-basis: 25%;
+}
+
+
+.horizontal {
+ display: flex;
+ flex-direction: row;
+ gap: 1rem;
+}
diff --git a/src/components/card/css/shadow/plural/cards.css b/src/components/card/css/shadow/plural/cards.css
new file mode 100644
index 000000000..7d070e416
--- /dev/null
+++ b/src/components/card/css/shadow/plural/cards.css
@@ -0,0 +1,49 @@
+/*******************************
+ Groups
+*******************************/
+
+.cards {
+ display: grid;
+ grid-auto-rows: 1fr;
+ gap: 1rem;
+ width: 100%;
+
+}
+
+::slotted(ui-card) {
+ display: contents;
+}
+
+.cards {
+ &.one {
+ grid-template-columns: 1fr;
+ }
+
+ &.two {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ &.three {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ &.four {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ &.five {
+ grid-template-columns: repeat(5, 1fr);
+ }
+
+ &.six {
+ grid-template-columns: repeat(6, 1fr);
+ }
+
+ &.seven {
+ grid-template-columns: repeat(7, 1fr);
+ }
+
+ &.eight {
+ grid-template-columns: repeat(8, 1fr);
+ }
+}
diff --git a/src/components/card/css/shadow/variations/fluid.css b/src/components/card/css/shadow/variations/fluid.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/components/card/index.js b/src/components/card/index.js
new file mode 100644
index 000000000..93351a954
--- /dev/null
+++ b/src/components/card/index.js
@@ -0,0 +1,2 @@
+export { UICard } from './card.js';
+export { UICards } from './plural/cards.js';
diff --git a/src/components/card/plural/cards.html b/src/components/card/plural/cards.html
new file mode 100644
index 000000000..41c856e6e
--- /dev/null
+++ b/src/components/card/plural/cards.html
@@ -0,0 +1,3 @@
+
+ {{>slot}}
+
diff --git a/src/components/card/plural/cards.js b/src/components/card/plural/cards.js
new file mode 100644
index 000000000..8caf9a5cb
--- /dev/null
+++ b/src/components/card/plural/cards.js
@@ -0,0 +1,16 @@
+import { defineComponent } from '@semantic-ui/component';
+import { CardPluralComponentSpec } from '@semantic-ui/specs';
+
+import CardShadowCSS from '../css/card-shadow.css?raw';
+
+import CardsTemplate from './cards.html?raw';
+
+export const UICards = defineComponent({
+ tagName: 'ui-cards',
+ singularTag: 'ui-card',
+ plural: true,
+ delegateFocus: true,
+ componentSpec: CardPluralComponentSpec,
+ template: CardsTemplate,
+ css: CardShadowCSS,
+});
diff --git a/src/semantic-ui.js b/src/semantic-ui.js
index afbc7b8ed..e31405a88 100755
--- a/src/semantic-ui.js
+++ b/src/semantic-ui.js
@@ -4,6 +4,7 @@ export { UIRail } from './components/rail/index.js';
/* Primitives */
export { UIButton, UIButtons } from './components/button/index.js';
+export { UICard, UICards } from './components/card/index.js';
export { UIIcon } from './components/icon/index.js';
export { UIMenu, MenuItem } from './components/menu/index.js';
export { UIInput } from './components/input/index.js';
diff --git a/src/themes/base/base.css b/src/themes/base/base.css
index 50f70064e..ac9bd93ce 100755
--- a/src/themes/base/base.css
+++ b/src/themes/base/base.css
@@ -18,6 +18,7 @@
@import url("./menu/menu-theme.css") layer(baseTheme.component.menu);
@import url("./input/input-theme.css") layer(baseTheme.component.input);
@import url("./label/label-theme.css") layer(baseTheme.component.label);
+@import url("./card/card-theme.css") layer(baseTheme.component.card);
/* Components */
@import url("./modal/modal-theme.css") layer(baseTheme.component.modal);
diff --git a/src/themes/base/card/card-theme.css b/src/themes/base/card/card-theme.css
new file mode 100644
index 000000000..932ed3fd9
--- /dev/null
+++ b/src/themes/base/card/card-theme.css
@@ -0,0 +1,14 @@
+/*******************************
+ Button
+*******************************/
+
+/* Content */
+@import url('./content/card-variables.css') layer(content.card);
+
+/* Types */
+@import url('./types/selection-variables.css') layer(type.selection);
+
+/* Variations */
+@import url('./variations/fluid-variables.css') layer(variations.evenlySpaced);
+
+
diff --git a/src/themes/base/card/content/card-variables.css b/src/themes/base/card/content/card-variables.css
new file mode 100644
index 000000000..ea1fe1bb9
--- /dev/null
+++ b/src/themes/base/card/content/card-variables.css
@@ -0,0 +1,3 @@
+:root {
+ --card-border: var(--border);
+}
diff --git a/src/themes/base/card/types/selection-variables.css b/src/themes/base/card/types/selection-variables.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/themes/base/card/variations/fluid-variables.css b/src/themes/base/card/variations/fluid-variables.css
new file mode 100644
index 000000000..5a4260fca
--- /dev/null
+++ b/src/themes/base/card/variations/fluid-variables.css
@@ -0,0 +1,2 @@
+:root {
+}