diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ffdfba..4b02706 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
* Button component (first component, wee!)
* Input components
* InputField components
+* Basic Card component
### Changes
diff --git a/README.md b/README.md
index a357dbc..de63077 100644
--- a/README.md
+++ b/README.md
@@ -147,6 +147,33 @@ Add Flowbite to your Tailwind CSS configuration. In your `app/assets/tailwind/ap
) %>
```
+## How to customize components
+
+### Add specific CSS classes
+
+A common use case for customizing a component is to add more CSS classes when
+rendering it, fx to change the size or spacing. flowbite-components is optimized
+for this case and all you need to do is specify the extra classes:
+
+```erb
+<%= render(Flowbite::Card.new(class: "w-full my-8")) { "Content" } %>
+```
+renders
+```html
+
+```
+
+If you want to fully replace the existing classes, you can pass an entirely new
+`class` attribute via options:
+
+```erb
+<%= render(Flowbite::Card.new(options: {class: "w-full my-8"})) { "Content" } %>
+```
+renders
+```html
+
+```
+
## Available Components
### Form Components
diff --git a/app/components/flowbite/card.rb b/app/components/flowbite/card.rb
new file mode 100644
index 0000000..1035a5a
--- /dev/null
+++ b/app/components/flowbite/card.rb
@@ -0,0 +1,43 @@
+module Flowbite
+ # Renders a card element.
+ #
+ # See https://flowbite.com/docs/components/cards/
+ class Card < ViewComponent::Base
+ class << self
+ def classes(state: :default, style: :default)
+ style = styles.fetch(style)
+ style.fetch(state)
+ end
+
+ # rubocop:disable Layout/LineLength
+ def styles
+ {
+ default: Flowbite::Style.new(
+ default: ["max-w-sm", "p-6", "bg-white", "border", "border-gray-200", "rounded-lg", "shadow-sm", "dark:bg-gray-800", "dark:border-gray-700"]
+ )
+ }.freeze
+ end
+ # rubocop:enable Layout/LineLength
+ end
+
+ def call
+ card_options = {}
+ card_options[:class] = self.class.classes + @class
+
+ content_tag(:div, card_options.merge(@options)) do
+ concat(content_tag(:div, content, class: "font-normal text-gray-700 dark:text-gray-400"))
+ end
+ end
+
+ # @param class [Array] Additional CSS classes for the card
+ # container.
+ #
+ # @param options [Hash] Additional HTML options for the card container
+ # (e.g., custom classes, data attributes). These options are merged into
+ # the card's root element.
+ def initialize(class: [], options: {})
+ @class = Array(binding.local_variable_get(:class)) || []
+ @options = options || {}
+ end
+ end
+end
diff --git a/demo/app/views/pages/index.html.erb b/demo/app/views/pages/index.html.erb
index 9c77fc3..a52be2e 100644
--- a/demo/app/views/pages/index.html.erb
+++ b/demo/app/views/pages/index.html.erb
@@ -17,7 +17,7 @@
-
+ <%= render(Flowbite::Card.new(:class => "sm:flex-1")) do %>
@@ -30,9 +30,9 @@
<% end %>
-
+ <% end %>
-
+ <%= render(Flowbite::Card.new(:class => "sm:flex-1")) do %>
@@ -46,9 +46,9 @@
<% end %>
-
+ <% end %>
-
+ <%= render(Flowbite::Card.new(:class => "sm:flex-1")) do %>
<% end %>
-
+ <% end %>
diff --git a/demo/test/components/previews/card_preview.rb b/demo/test/components/previews/card_preview.rb
new file mode 100644
index 0000000..f2b44f0
--- /dev/null
+++ b/demo/test/components/previews/card_preview.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class CardPreview < Lookbook::Preview
+ # Use the following simple card component with a title and description.
+ def default
+ render(Flowbite::Card.new) { "Use the following simple card component with a title and description." }
+ end
+end
diff --git a/test/components/flowbite/card_test.rb b/test/components/flowbite/card_test.rb
new file mode 100644
index 0000000..e334dcd
--- /dev/null
+++ b/test/components/flowbite/card_test.rb
@@ -0,0 +1,30 @@
+require "test_helper"
+
+class Flowbite::CardTest < Minitest::Test
+ include ViewComponent::TestHelpers
+
+ def test_renders_a_default_card
+ render_inline(Flowbite::Card.new) { "Card Content" }
+
+ assert_selector("div.p-6.bg-white.border.border-gray-200.rounded-lg.shadow-sm")
+ end
+
+ def test_passes_options_to_the_card_as_attributes
+ render_inline(Flowbite::Card.new(options: {id: "card-1"})) { "Card Content" }
+
+ assert_selector("div#card-1")
+ end
+
+ def test_adds_the_classes_to_the_default_classes
+ render_inline(Flowbite::Card.new(class: "custom-class another")) { "Card Content" }
+
+ assert_selector("div.p-6.bg-white.border.border-gray-200.rounded-lg.shadow-sm.custom-class.another")
+ end
+
+ def test_overrides_the_default_classes
+ render_inline(Flowbite::Card.new(options: {class: "custom-class another"})) { "Card Content" }
+
+ assert_no_selector("div.p-6.bg-white.border.border-gray-200.rounded-lg.shadow-sm")
+ assert_selector("div.custom-class.another")
+ end
+end