suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Calendar

A month-grid date picker for selecting a single date, several dates or a date range.

Calendar is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

function SingleCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarDemo() {
  return <SingleCalendar buttonVariant="plain" />
}

Calendar is a styled wrapper around DayPicker from react-day-picker, so it accepts the full DayPicker API, including mode, selected, onSelect, numberOfMonths, captionLayout, disabled and showWeekNumber. It replaces the day cells with CalendarDayButton, which renders each day as a Button and styles today, single selections and range start, middle and end.

Use a calendar when people need to see dates in context, such as availability or a stay range. Place it inside a Popover to build a compact date picker; the calendar's background becomes transparent inside popover and card content.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/calendar

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add calendar

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @phosphor-icons/react react-day-picker
  1. Add the pieces it builds on: Button.

  2. Copy the source into components/ui/calendar.tsx:

components/ui/calendar.tsx
"use client"

import {
  CaretDownIcon,
  CaretLeftIcon,
  CaretRightIcon,
} from "@phosphor-icons/react"
import * as React from "react"
import {
  type ChevronProps,
  type DayButtonProps,
  DayPicker,
  getDefaultClassNames,
  type RootProps,
  useDayPicker,
  type WeekNumberProps,
  1. Update the import paths to match your project.

Usage

import { Calendar, CalendarDayButton } from "@/components/ui/calendar"
<SingleCalendar buttonVariant="plain" />

Set mode to single, multiple or range, and control the selection with selected and onSelect. For ranges, numberOfMonths={2} shows two months side by side on wider screens.

Set captionLayout to dropdown, dropdown-months or dropdown-years to let people jump to a month or year directly; the default is label.

Unlike the DayPicker default, outside days are hidden unless you pass showOutsideDays. The default locale is enUS. Pass disabled with a matcher, such as { dayOfWeek: [0, 6] }, to block days.

Use CalendarDayButton when you supply your own components and still want the library's day styling.

Examples

Sizes

Compares the sm, default and lg sizes.

import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

function SingleCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarSizes() {
  return (
    <div className="flex flex-wrap items-start gap-6">
      <SingleCalendar buttonVariant="plain" size="sm" />
      <SingleCalendar buttonVariant="plain" size="default" />
      <SingleCalendar buttonVariant="plain" size="lg" />
    </div>
  )
}

Range

Selects a seven-day range across two months with mode="range".

import * as React from "react"
import type { DateRange } from "react-day-picker"
import { Calendar } from "@/components/ui/calendar"

function RangeCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [range, setRange] = React.useState<DateRange | undefined>({
    from: today,
    to: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 6),
  })
  return (
    <Calendar
      {...props}
      mode="range"
      selected={range}
      onSelect={setRange}
      numberOfMonths={2}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarRange() {
  return <RangeCalendar buttonVariant="plain" />
}

Dropdown caption

Replaces the month label with month and year dropdowns.

import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

function SingleCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarDropdownCaption() {
  return <SingleCalendar buttonVariant="plain" captionLayout="dropdown" />
}

Week numbers

Shows the week number at the start of each row.

import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

function SingleCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarWeekNumbers() {
  return <SingleCalendar buttonVariant="plain" showWeekNumber />
}

Disabled days

Disables weekends with a dayOfWeek matcher.

import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

function SingleCalendar(props: React.ComponentProps<typeof Calendar>) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border"
    />
  )
}

export default function CalendarDisabledDays() {
  return (
    <SingleCalendar buttonVariant="plain" disabled={{ dayOfWeek: [0, 6] }} />
  )
}

API Reference

01

Calendar

The date grid, with month navigation and caption.

PropTypeDefaultDescription
buttonVariantReact.ComponentProps<typeof Button>["variant"]"plain"Button variant used for the previous and next month buttons. Defaults to plain.
size"sm" | "default" | "lg""default"Cell size and type scale: sm, default or lg.

Also accepts the props of React.ComponentProps<typeof DayPicker>.

02

CalendarDayButton

The button rendered for each day, styled by its selection state.

Also accepts the props of DayButtonProps.

Accessibility

  • Each month renders as a grid labelled with its month and year; selected days set aria-selected on their cell.
  • Arrow keys move focus between days, and clicking or pressing a day selects it.
  • The navigation buttons are labelled for the next and previous month.
  • Disabled days render disabled buttons.
  • Caption dropdowns are native select elements labelled for month and year.

Notes

  • Styling hooks: data-slot="calendar" and data-size on the root; day buttons expose data-day, data-today, data-selected-single, data-range-start, data-range-middle and data-range-end.
  • Cell size and radius come from the --cell-size and --cell-radius custom properties, which size adjusts.
  • Pass classNames to override any react-day-picker class key; your values replace the defaults for that key.
  • Navigation chevrons flip in right-to-left layouts.
iOS
Calendar on iOS in light modeCalendar on iOS in dark mode
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarExample() {
  return (
    <SingleCalendar />
  )
}
import EsuissUI
import SwiftUI

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}

Calendar renders one or more month grids with a caption, previous and next buttons, weekday headers and pressable day cells. Selection mode is set with mode: single, multiple or range. Ranges are drawn as a continuous shaded band with filled start and end days.

Three sizes are available: sm, default and lg. The caption can be a plain label or month and year dropdowns built on Select, which suits birth dates and other dates far from today.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/calendar

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add calendar
$ pnpm dlx @esuiss/ui@latest add calendar --platform swift
$ pnpm dlx @esuiss/ui@latest add calendar --platform kotlin

The CLI also adds what it builds on: Button, Icon, Select, Text.

Manual

  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-svg
  1. Add the pieces it builds on: Button, Icon, Select, Text, and the primitives components/primitives/calendar.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/calendar.tsx:

uniwind/components/ui/calendar.tsx
import { cn } from "cn"
import { CaretLeftIcon, CaretRightIcon } from "phosphor-react-native"
import * as React from "react"
import { I18nManager, ScrollView, View } from "react-native"
import * as CalendarPrimitive from "@/components/primitives/calendar"
import { resolveClassName } from "@/components/primitives/use-render"
import { Button, type ButtonProps } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Text, TextClassContext } from "@/components/ui/text"
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-svg
  1. Add the pieces it builds on: Button, Icon, Select, Text, and the primitives components/primitives/calendar.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/calendar.tsx:

nativewind/components/ui/calendar.tsx
import { cn } from "cn"
import { CaretLeftIcon, CaretRightIcon } from "phosphor-react-native"
import * as React from "react"
import { I18nManager, ScrollView, View } from "react-native"
import * as CalendarPrimitive from "@/components/primitives/calendar"
import { resolveClassName } from "@/components/primitives/use-render"
import { Button, type ButtonProps } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Text, TextClassContext } from "@/components/ui/text"
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install phosphor-react-native react-native-svg
  1. Add the pieces it builds on: Button, Icon, Select, Text, and the primitives components/primitives/calendar.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/calendar.tsx:

stylesheet/components/ui/calendar.tsx
import { CaretLeftIcon, CaretRightIcon } from "phosphor-react-native"
import * as React from "react"
import {
  I18nManager,
  Pressable,
  ScrollView,
  type StyleProp,
  StyleSheet,
  type TextStyle,
  View,
  type ViewStyle,
} from "react-native"
import * as CalendarPrimitive from "@/components/primitives/calendar"
import { resolveStyle } from "@/components/primitives/use-render"
import { Button, type ButtonProps } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Add the Swift package to your app:

Package.swift
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
  1. Copy the theme files every view uses:
swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/Calendar.swift
import PhosphorSwift
import SwiftUI

public struct DateRange: Hashable, Sendable {
  public var from: Date
  public var to: Date?

  public init(from: Date, to: Date? = nil) {
    self.from = from
    self.to = to
  }

  public static func adding(_ day: Date, to range: DateRange?, calendar: Foundation.Calendar) -> DateRange? {
    guard let range else { return DateRange(from: day) }
    let from = range.from
    guard let to = range.to else {
swift/Components/Button.swift
import SwiftUI

public struct VariantButtonStyle: ButtonStyle {
  public enum Variant: CaseIterable, Sendable {
    case `default`, outline, secondary, tinted, ghost, plain, destructive, link
  }

  public enum Size: CaseIterable, Sendable {
    case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl

    var height: CGFloat {
      switch self {
      case .xs, .iconXs: 24
      case .sm, .iconSm: 32
      case .default, .icon: 36
      case .lg, .iconLg: 44
swift/Components/ButtonGroup.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var buttonGroup: Axis? = nil
  @Entry var buttonGroupSize: CGSize = .zero
}

enum ButtonGroupSpace: Hashable {
  case group
}

struct ButtonGroupFrame {
  let bounds: Anchor<CGRect>
  let outlined: Bool
}
swift/Components/Checkbox.swift
import PhosphorSwift
import SwiftUI

extension Shadow {
  static let control = Shadow(color: .black.opacity(0.08), radius: 0.5, y: 0.5)
  static let thumb = Shadow(color: .black.opacity(0.12), radius: 1.5, y: 1)
}

struct SelectionIndicator<Mark: View>: View {
  let checked: Bool
  let pressed: Bool
  let cornerRadius: CGFloat
  let mark: Mark

  @Environment(\.isEnabled) private var isEnabled
  @Environment(\.isInvalid) private var invalid
swift/Components/Field.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var isInvalid = false
  @Entry var fieldInset = false
  @Entry var fieldGroupInset = false
  @Entry var fieldSetInset = false
}

extension View {
  public func invalid(_ isInvalid: Bool = true) -> some View {
    environment(\.isInvalid, isInvalid)
  }
}

public enum FieldOrientation: CaseIterable, Sendable {
swift/Components/Haptics.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var hapticsEnabled = true
}

extension View {
  public func haptics(_ enabled: Bool) -> some View {
    environment(\.hapticsEnabled, enabled)
  }

  func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
    modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
  }

  func hapticFeedback<Trigger: Equatable>(
swift/Components/Separator.swift
import SwiftUI

public struct Separator: View {
  let orientation: Axis

  public init(_ orientation: Axis = .horizontal) {
    self.orientation = orientation
  }

  public var body: some View {
    Rectangle()
      .fill(EsuissColor.border)
      .frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
      .frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
      .accessibilityHidden(true)
  }
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source and every file it uses:
kotlin/components/Calendar.kt
package com.esuiss.ui.components

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
kotlin/components/Button.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
kotlin/components/ButtonGroup.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
kotlin/components/Checkbox.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.triStateToggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
kotlin/components/DropdownMenu.kt
package com.esuiss.ui.components

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
kotlin/components/Field.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
kotlin/components/FormSupport.kt
package com.esuiss.ui.components

import android.provider.Settings
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
kotlin/components/Haptics.kt
package com.esuiss.ui.components

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

val LocalHapticsEnabled = staticCompositionLocalOf { true }

@Composable
fun ProvideHaptics(enabled: Boolean, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalHapticsEnabled provides enabled, content = content)
}
kotlin/components/Icon.kt
package com.esuiss.ui.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Icon(
    imageVector: ImageVector,
kotlin/components/Label.kt
package com.esuiss.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
kotlin/components/Overlay.kt
package com.esuiss.ui.components

import android.os.Build
import android.view.Window
import android.view.WindowManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideIn
kotlin/components/Separator.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Separator(
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { CalendarDayButton, Calendar } from "@/components/ui/calendar"
<SingleCalendar />

Pass mode, selected and onSelect to control the selection; selected is a Date, a Date[] or a { from, to } range depending on the mode. Use month, defaultMonth and onMonthChange to control the visible month, and numberOfMonths to show several months.

Use disabled with date matchers, such as { dayOfWeek: [0, 6] }, to block days. Limit navigation with startMonth and endMonth; with a year dropdown they default to the last 100 years.

Set locale for month and weekday names, and weekStartsOn to change the first day of the week. CalendarDayButton is exported for custom day rendering.

Examples

Sizes

The sm, default and lg sizes.

iOS
Calendar (sizes) on iOS in light modeCalendar (sizes) on iOS in dark mode
import * as React from "react"
import { ScrollView } from "react-native"
import { Calendar } from "@/components/ui/calendar"

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarSizesExample() {
  return (
    <ScrollView contentContainerClassName="items-start gap-6">
      <SingleCalendar size="sm" />
      <SingleCalendar size="default" />
      <SingleCalendar size="lg" />
    </ScrollView>
  )
}
import EsuissUI
import SwiftUI

struct CalendarSizesExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 24) {
      CalendarSingleDemo(size: .sm)
      CalendarSingleDemo(size: .default)
      CalendarSingleDemo(size: .lg)
    }
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}

@Composable
fun CalendarSizes() {
    Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
        CalendarSingle(size = CalendarSize.Sm, key = "sm")
        CalendarSingle(size = CalendarSize.Default, key = "default")
        CalendarSingle(size = CalendarSize.Lg, key = "lg")
    }
}

Range

Range selection across two months.

iOS
Calendar (range) on iOS in light modeCalendar (range) on iOS in dark mode
import * as React from "react"
import { ScrollView } from "react-native"
import { DateRange } from "@/components/primitives/calendar"
import { Calendar } from "@/components/ui/calendar"

function RangeCalendar() {
  const today = new Date()
  const [range, setRange] = React.useState<DateRange | undefined>({
    from: today,
    to: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 6),
  })
  return (
    <ScrollView>
      <Calendar
        mode="range"
        selected={range}
        onSelect={setRange}
        numberOfMonths={2}
        className="rounded-2xl border border-border"
      />
    </ScrollView>
  )
}

export function CalendarRangeExample() {
  return (
    <RangeCalendar />
  )
}
import EsuissUI
import SwiftUI

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarRangeDemo: View {
  @State private var range: DateRange? = DateRange(from: CalendarDates.day(0), to: CalendarDates.day(6))

  var body: some View {
    DateCalendar(selection: $range, numberOfMonths: 2)
      .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.DateRange
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarRange() {
    var from by rememberSaveable { mutableStateOf<Date?>(calendarDay()) }
    var to by rememberSaveable { mutableStateOf<Date?>(calendarDay(offsetDays = 6)) }
    CalendarFrame {
        Calendar(
            selected = if (from == null) null else DateRange(from, to),
            onSelect = { range: DateRange? ->
                from = range?.from
                to = range?.to
            },
            numberOfMonths = 2,
        )
    }
}

Multiple

Several separate dates selected with mode="multiple".

iOS
Calendar (multiple) on iOS in light modeCalendar (multiple) on iOS in dark mode
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

function MultipleCalendar() {
  const today = new Date()
  const [dates, setDates] = React.useState<Date[] | undefined>([
    new Date(today.getFullYear(), today.getMonth(), 8),
    new Date(today.getFullYear(), today.getMonth(), 15),
    new Date(today.getFullYear(), today.getMonth(), 22),
  ])
  return (
    <Calendar
      mode="multiple"
      selected={dates}
      onSelect={setDates}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarMultipleExample() {
  return (
    <MultipleCalendar />
  )
}
import EsuissUI
import SwiftUI

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarMultipleDemo: View {
  @State private var dates: Set<Date> = [
    CalendarDates.dayOfMonth(8),
    CalendarDates.dayOfMonth(15),
    CalendarDates.dayOfMonth(22),
  ]

  var body: some View {
    DateCalendar(selection: $dates)
      .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarMultiple() {
    var dates by rememberSaveable {
        mutableStateOf(listOf(calendarDay(dayOfMonth = 8), calendarDay(dayOfMonth = 15), calendarDay(dayOfMonth = 22)))
    }
    CalendarFrame {
        Calendar(selected = dates, onSelect = { value: List<Date> -> dates = value })
    }
}

Dropdown caption

Month and year dropdowns in the caption.

iOS
Calendar (dropdown caption) on iOS in light modeCalendar (dropdown caption) on iOS in dark mode
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarDropdownCaptionExample() {
  return (
    <SingleCalendar captionLayout="dropdown" />
  )
}
import EsuissUI
import SwiftUI

struct CalendarDropdownCaptionExample: View {
  var body: some View {
    CalendarSingleDemo(captionLayout: .dropdown)
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

@Composable
fun CalendarDropdownCaptionExample() {
    CalendarSingle(captionLayout = CalendarCaptionLayout.Dropdown)
}

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}

Week numbers

Week numbers in a leading column.

iOS
Calendar (week numbers) on iOS in light modeCalendar (week numbers) on iOS in dark mode
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarWeekNumbersExample() {
  return (
    <SingleCalendar showWeekNumber />
  )
}
import EsuissUI
import SwiftUI

struct CalendarWeekNumbersExample: View {
  var body: some View {
    CalendarSingleDemo(showWeekNumber: true)
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

@Composable
fun CalendarWeekNumbersExample() {
    CalendarSingle(showWeekNumber = true)
}

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}

Disabled days

Weekends disabled with a dayOfWeek matcher.

iOS
Calendar (disabled days) on iOS in light modeCalendar (disabled days) on iOS in dark mode
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarDisabledDaysExample() {
  return (
    <SingleCalendar disabled={{ dayOfWeek: [0, 6] }} />
  )
}
import EsuissUI
import SwiftUI

struct CalendarDisabledDaysExample: View {
  var body: some View {
    CalendarSingleDemo(weekendsDisabled: true)
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

@Composable
fun CalendarDisabledDaysExample() {
    CalendarSingle(weekendsDisabled = true)
}

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}

Outside days

Days from adjacent months shown with showOutsideDays.

iOS
Calendar (outside days) on iOS in light modeCalendar (outside days) on iOS in dark mode
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarOutsideDaysExample() {
  return (
    <SingleCalendar showOutsideDays />
  )
}
import EsuissUI
import SwiftUI

struct CalendarOutsideDaysExample: View {
  var body: some View {
    CalendarSingleDemo(showOutsideDays: true)
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

@Composable
fun CalendarOutsideDaysExample() {
    CalendarSingle(showOutsideDays = true)
}

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}

Button variants

Navigation buttons in the ghost, secondary and tinted variants.

iOS
Calendar (button variants) on iOS in light modeCalendar (button variants) on iOS in dark mode
import * as React from "react"
import { ScrollView } from "react-native"
import { Calendar } from "@/components/ui/calendar"

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarButtonVariantsExample() {
  return (
    <ScrollView contentContainerClassName="items-start gap-6">
      <SingleCalendar buttonVariant="ghost" />
      <SingleCalendar buttonVariant="secondary" />
      <SingleCalendar buttonVariant="tinted" />
    </ScrollView>
  )
}
import EsuissUI
import SwiftUI

struct CalendarButtonVariantsExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 24) {
      CalendarSingleDemo(buttonVariant: .ghost)
      CalendarSingleDemo(buttonVariant: .secondary)
      CalendarSingleDemo(buttonVariant: .tinted)
    }
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}

@Composable
fun CalendarButtonVariants() {
    Column(verticalArrangement = Arrangement.spacedBy(24.dp)) {
        CalendarSingle(buttonVariant = ButtonVariant.Ghost, key = "ghost")
        CalendarSingle(buttonVariant = ButtonVariant.Secondary, key = "secondary")
        CalendarSingle(buttonVariant = ButtonVariant.Tinted, key = "tinted")
    }
}

Locale

Turkish month and weekday names with locale="tr-TR".

iOS
Calendar (locale) on iOS in light modeCalendar (locale) on iOS in dark mode
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"

type CalendarProps = React.ComponentProps<typeof Calendar>

function SingleCalendar(
  props: Omit<CalendarProps, "mode" | "selected" | "onSelect">
) {
  const today = new Date()
  const [date, setDate] = React.useState<Date | undefined>(
    new Date(today.getFullYear(), today.getMonth(), today.getDate() + 3)
  )
  return (
    <Calendar
      {...props}
      mode="single"
      selected={date}
      onSelect={setDate}
      className="rounded-2xl border border-border"
    />
  )
}

export function CalendarLocaleExample() {
  return (
    <SingleCalendar locale="tr-TR" />
  )
}
import EsuissUI
import SwiftUI

struct CalendarLocaleExample: View {
  var body: some View {
    CalendarSingleDemo()
      .environment(\.locale, Locale(identifier: "tr_TR"))
  }
}

enum CalendarDates {
  static func day(_ offset: Int) -> Date {
    let calendar = Foundation.Calendar.current
    let start = calendar.startOfDay(for: Date())
    return calendar.date(byAdding: .day, value: offset, to: start) ?? start
  }

  static func dayOfMonth(_ day: Int) -> Date {
    let calendar = Foundation.Calendar.current
    var components = calendar.dateComponents([.year, .month], from: Date())
    components.day = day
    return calendar.date(from: components) ?? Date()
  }
}

struct CalendarBordered: ViewModifier {
  func body(content: Content) -> some View {
    content
      .clipShape(RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous))
      .overlay {
        RoundedRectangle(cornerRadius: Radius.xl2, style: .continuous).strokeBorder(EsuissColor.border)
      }
  }
}

struct CalendarSingleDemo: View {
  var size: DateCalendar.Size = .default
  var buttonVariant: VariantButtonStyle.Variant = .plain
  var captionLayout: DateCalendar.CaptionLayout = .label
  var showOutsideDays = false
  var showWeekNumber = false
  var weekendsDisabled = false

  @State private var date: Date? = CalendarDates.day(3)

  var body: some View {
    DateCalendar(
      selection: $date,
      size: size,
      buttonVariant: buttonVariant,
      captionLayout: captionLayout,
      showOutsideDays: showOutsideDays,
      showWeekNumber: showWeekNumber,
      isDisabled: { day in
        guard weekendsDisabled else { return false }
        let weekday = Foundation.Calendar.current.component(.weekday, from: day)
        return weekday == 1 || weekday == 7
      }
    )
    .modifier(CalendarBordered())
  }
}

func day(_ offset: Int) -> Date {
  let calendar = Foundation.Calendar.current
  let start = calendar.startOfDay(for: Date())
  return calendar.date(byAdding: .day, value: offset, to: start) ?? start
}

func dayOfMonth(_ day: Int) -> Date {
  let calendar = Foundation.Calendar.current
  var components = calendar.dateComponents([.year, .month], from: Date())
  components.day = day
  return calendar.date(from: components) ?? Date()
}
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Calendar
import com.esuiss.ui.components.CalendarCaptionLayout
import com.esuiss.ui.components.CalendarSize
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale

@Composable
fun CalendarLocaleExample() {
    CalendarSingle(locale = Locale.forLanguageTag("tr-TR"))
}

fun calendarDay(offsetDays: Int = 0, dayOfMonth: Int? = null): Date {
    val now = GregorianCalendar()
    val result = GregorianCalendar(now.get(GregorianCalendar.YEAR), now.get(GregorianCalendar.MONTH), now.get(GregorianCalendar.DAY_OF_MONTH))
    if (dayOfMonth != null) result.set(GregorianCalendar.DAY_OF_MONTH, dayOfMonth)
    result.add(GregorianCalendar.DAY_OF_MONTH, offsetDays)
    return result.time
}

fun calendarIsWeekend(date: Date): Boolean {
    val day = GregorianCalendar().apply { time = date }.get(GregorianCalendar.DAY_OF_WEEK)
    return day == GregorianCalendar.SATURDAY || day == GregorianCalendar.SUNDAY
}

@Composable
fun CalendarFrame(content: @Composable () -> Unit) {
    Box(
        Modifier
            .border(1.dp, EsuissTheme.colors.border, RoundedCornerShape(Radius.xl2))
            .padding(1.dp),
    ) {
        content()
    }
}

@Composable
fun CalendarSingle(
    size: CalendarSize = CalendarSize.Default,
    captionLayout: CalendarCaptionLayout = CalendarCaptionLayout.Label,
    showWeekNumber: Boolean = false,
    showOutsideDays: Boolean = false,
    weekendsDisabled: Boolean = false,
    buttonVariant: ButtonVariant = ButtonVariant.Plain,
    locale: Locale = Locale.getDefault(),
    key: String = "",
) {
    var date by rememberSaveable(key) { mutableStateOf<Date?>(calendarDay(offsetDays = 3)) }
    CalendarFrame {
        Calendar(
            selected = date,
            onSelect = { date = it },
            size = size,
            captionLayout = captionLayout,
            showWeekNumber = showWeekNumber,
            showOutsideDays = showOutsideDays,
            disabled = { weekendsDisabled && calendarIsWeekend(it) },
            buttonVariant = buttonVariant,
            locale = locale,
        )
    }
}

API Reference

01

CalendarDayButton

Pressable day cell styled for each selection state.

Also accepts the props of React.ComponentProps<typeof CalendarPrimitive.DayButton>.

02

Calendar

Month grids with caption and navigation.

PropTypeDefaultDescription
buttonVariantButtonProps["variant"]"plain"Button variant for the navigation buttons. Defaults to plain.
sizeCalendarSize"default"sm, default or lg.
captionLayoutCaptionLayout"label"label, dropdown, dropdown-months or dropdown-years.
showWeekNumberbooleanfalseShows week numbers.

Also accepts the props of React.ComponentProps<typeof CalendarPrimitive.Root>.

Accessibility

  • Each month is a grid labelled with its caption.
  • Day cells have the button role, a full date label, and selected and disabled state.
  • On React Native and Android, the previous and next buttons are labelled with the relative month in the calendar's locale, such as "Last month" and "Next month" in English. On iOS they are labelled "Previous month" and "Next month".
  • Caption dropdowns are labelled "Choose the Month" and "Choose the Year".

Notes

  • Navigation arrows swap in right-to-left layouts.
  • Uses bg-primary, bg-primary/10 dark:bg-primary/20, bg-muted and text-primary tokens; light and dark follow the system appearance.
NextCardPreviousButton Group

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Sizes
    • Range
    • Dropdown caption
    • Week numbers
    • Disabled days
  4. 04API Reference
    • Calendar
    • CalendarDayButton
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • Sizes
    • Range
    • Multiple
    • Dropdown caption
    • Week numbers
    • Disabled days
    • Outside days
    • Button variants
    • Locale
  10. 10API Reference
    • CalendarDayButton
    • Calendar
  11. 11Accessibility
  12. 12Notes