Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                html
  head
    title Neon Clock (Using React Hooks)
    meta(name='viewport', content='initial-scale=2.0')
    
  body
    #root
              
            
!

CSS

              
                @dark-bg: #090710;
@muted-red: #792d2d;
@glowing-red: #ffc5b2;
@shadow-red: #ff4343;
@silver: #a8b0f3;

.center-children () {
  display: flex;
  align-items: center;
  justify-content: center;
}

body {
  max-width: 100vw;
  font-family: "Julius Sans One";
  background: @dark-bg;
}

#root {
  width: 100%;
  min-height: 100vh;
  .center-children();
}

.clock {
  width: 100%;
  max-width: 75vw;
  padding: 20px 0;
  flex-direction: column;
  .center-children();
}

.phrases {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  word-spacing: 12px;
  font-size: 36px;
  line-height: 1.618;
}

.timer {
  margin-top: 30px;
  margin-left: 10px;
  color: @silver;
  font-size: 20px;
  font-weight: bold;
}

span {
  color: @muted-red;
  margin: 0 10px;
  transition: all 0.5s cubic-bezier(0.6, -0.51, 0.5, 1.51);
  vertical-align: middle;
  filter: blur(2px);

  &.glow {
    color: @glowing-red;
    text-shadow: 0 0 20px @shadow-red;
    font-weight: bold;
    filter: none;
  }
}

.standard-clock {
  max-width: 300px;
  min-width: 300px;
  height: 300px;
  margin-bottom: 40px;
  border-radius: 50%;
  border: 5px double;
  color: @glowing-red;
  position: relative;
  text-shadow: 0 0 20px @shadow-red;
  box-shadow: 0 0 20px @shadow-red, inset 0 0 20px @shadow-red;
  transition: all 0.5s cubic-bezier(0.6, -0.51, 0.5, 1.51);
  
  &:before {
    content: ' ';
    position: absolute;
    top: -14px;
    left: -14px;
    display: block;
    width: 320px;
    height: 320px;
    border-radius: 50%;
    border: 4px dotted red;
    transition: all 0.5s cubic-bezier(0.6, -0.51, 0.5, 1.51);
  }
}

.inner-circle {
  position: absolute;
  top: 50px;
  left: 50px;
  width: 200px;
  height: 200px;
  border-radius: 50%;
  border: 2px solid;
  box-shadow: 0 0 20px @shadow-red, inset 0 0 20px @shadow-red;
  transition: all 0.5s cubic-bezier(0.6, -0.51, 0.5, 1.51);
}

.inner-circle-2 {
  position: absolute;
  top: 60px;
  left: 60px;
  width: 180px;
  height: 180px;
  border-radius: 50%;
  border: 2px solid;
  box-shadow: 0 0 20px @shadow-red, inset 0 0 20px @shadow-red;
  transition: all 0.5s cubic-bezier(0.6, -0.51, 0.5, 1.51);
}

.mark {
  position: absolute;
  display: inline-block;
  top: 10px;
  left: 115px;
  width: 50px;
  height: 30px;
  font-size: 30px;
  text-align: center;
  color: @glowing-red;
  filter: none;
  transform-origin: 25px 140px;
}

.generate-mark-rotation (@n) when (@n = 13) {}
.generate-mark-rotation (@n) when (@n < 13) {
  span.mark:nth-child(@{n}) {
    transform: rotate((@n - 1) * 30deg);
  }
  .generate-mark-rotation(@n + 1);
}
.generate-mark-rotation(2);

.tick {
  position: absolute;
  display: inline-block;
  top: 51px;
  left: 141px;
  width: 0px;
  outline: 1px @glowing-red solid;
  height: 8px;
  //background: @glowing-red;
  transform-origin: 0px 100px;
  filter: none;
}

.generate-tick-rotation (@n) when (@n = 61) {}
.generate-tick-rotation (@n) when (@n < 61) {
  span.tick:nth-child(@{n}) {
    //background: green;
    transform: rotate((@n - 1) * 6deg);
  }
  .generate-tick-rotation(@n + 1);
}
.generate-tick-rotation(2);

.generate-big-tick-rotation (@n) when (@n = 12) {}
.generate-big-tick-rotation (@n) when (@n < 12) {
  @m: (@n * 5) + 1;
  span.tick:nth-child(@{m}) {
    height: 20px;
  }
  .generate-big-tick-rotation(@n + 1);
}
.generate-big-tick-rotation(0);

.hour-hand,
.minute-hand,
.second-hand {
  width: 10px;
  height: 55px;
  background: @glowing-red;
  position: absolute;
  top: 100px;
  left: 145px;
  border-radius: 0 0 5px 5px;
  transform-origin: 5px 50px;
  box-shadow: 0 0 20px @shadow-red;
}

.hour-hand::before {
  content: " ";
  display: block;
  position: absolute;
  top: -15px;
  left: 0;
  width: 0;
  height: 0;
  border: 5px #0000 solid;
  border-bottom: 10px @glowing-red solid;
}

.minute-hand {
  width: 4px;
  height: 75px;
  top: 75px;
  left: 148px;
  border-radius: 2px;
  transform-origin: 2px 75px;
}

.second-hand {
  width: 2px;
  height: 90px;
  top: 60px;
  left: 149px;
  border-radius: 1px;
  transform-origin: 1px 90px;
}

.center {
  position: absolute;
  top: 135px;
  left: 135px;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: @glowing-red;
  box-shadow: 0 0 20px @shadow-red;
  transition: all 0.5s cubic-bezier(0.6, -0.51, 0.5, 1.51);
}

@media only screen and (max-width: 960px) {
  .clock {
    max-width: 90vw;
  }

  .phrases {
    font-size: 18px;
    word-spacing: 7px;
  }

  span {
    margin-right: 10px;
    filter: blur(1px);
  }

  .timer {
    font-size: 12px;
  }

  .standard-clock {
    max-width: 200px;
    min-width: 200px;
    height: 200px;
    margin-bottom: 20px;
    text-shadow: 0 0 10px @shadow-red;
    box-shadow: 0 0 10px @shadow-red, inset 0 0 10px @shadow-red;
    
    &:before {
      width: 220px;
      height: 220px;
    }
  }

  .inner-circle {
    top: 35px;
    left: 35px;
    width: 130px;
    height: 130px;
    box-shadow: 0 0 10px @shadow-red, inset 0 0 10px @shadow-red;
  }
  
  .inner-circle-2 {
    top: 45px;
    left: 45px;
    width: 110px;
    height: 110px;
  }
  
  .mark {
    top: 8px;
    left: 76px;
    width: 30px;
    height: 20px;
    font-size: 20px;
    transform-origin: 15px 92px;
  }

  .tick {
    top: 36px;
    left: 91px;
    transform-origin: 0px 65px;
  }
  
  .hour-hand,
  .minute-hand,
  .second-hand {
    height: 40px;
    top: 65px;
    left: 95px;
    transform-origin: 5px 35px;
    box-shadow: 0 0 10px @shadow-red;
  }

  .minute-hand {
    height: 55px;
    top: 50px;
    left: 98px;
    transform-origin: 2px 50px;
  }

  .second-hand {
    height: 55px;
    top: 45px;
    left: 100px;
    transform-origin: 1px 55px;
  }

  .center {
    top: 90px;
    left: 90px;
    width: 20px;
    height: 20px;
    box-shadow: 0 0 10px @shadow-red;
  }
}
              
            
!

JS

              
                const phrases = [
  'IT IS',
  'ABOUT',
  'NEARLY',
  'TEN',
  'QUARTER',
  'TWENTY',
  'FIVE',
  'HALF',
  'PAST',
  'TO',
  'ONE',
  'TWO',
  'THREE',
  'FOUR',
  'FIVE',
  'SIX',
  'SEVEN',
  'EIGHT',
  'NINE',
  'TEN',
  'ELEVEN',
  'NOON',
  'MIDNIGHT',
  'O\' CLOCK',
  'IN THE',
  'MORNING',
  'AFTERNOON',
  'EVENING',
];

function getNow () {
  const now = new Date(Date.now());
  const hour = now.getHours();
  const minute = now.getMinutes();
  const second = now.getSeconds();
  const display = now.toLocaleString();

  return {
    hour,
    minute,
    second,
    display,
  };
}

function getReadoutConfig ({ hour, minute }) {
  const lastMinuteMark = Math.floor(minute / 5) * 5;
  const distFromLast = minute - lastMinuteMark;
  const isExact = distFromLast === 0;
  const isNearly = !isExact && distFromLast > 2;
  const isAbout = !isExact && !isNearly;
  const nearestMinuteMark = isNearly
    ? (lastMinuteMark + 5) % 60
    : lastMinuteMark;
  const isOClock = nearestMinuteMark === 0;
  const isPast = !isOClock && nearestMinuteMark <= 30;
  const isTo = !isOClock && !isPast;
  const minuteMark = (isPast || isOClock)
    ? nearestMinuteMark
    : 60 - nearestMinuteMark;

  const nearestHour = (isTo || (isOClock && isNearly)) ? (hour + 1) % 24 : hour;
  const nearestHour12 = nearestHour > 12
    ? nearestHour - 12
    : nearestHour;
  const isNoon = nearestHour === 12;
  const isMidnight = nearestHour === 0;
  const isMorning = !isMidnight && nearestHour < 12;
  const isAfternoon = nearestHour > 12 && nearestHour <= 18;
  const isEvening = nearestHour > 18;

  return {
    isExact,
    isAbout,
    isNearly,

    minute: minuteMark,
    isOClock: isOClock && !isNoon && !isMidnight,
    isPast,
    isTo,

    hour: nearestHour12,
    isNoon,
    isMidnight,
    isMorning,
    isAfternoon,
    isEvening,
  };
}

function getHighlights (config) {
  return [
    true, // IT IS
    config.isAbout, // ABOUT
    config.isNearly, // NEARLY
    config.minute === 10, // TEN
    config.minute === 15, // QUARTER
    config.minute === 20 || config.minute === 25, // TWENTY
    config.minute === 5 || config.minute === 25, // FIVE
    config.minute === 30, // HALF
    config.isPast, // PAST
    config.isTo, // TO
    config.hour === 1, // ONE
    config.hour === 2, // TWO
    config.hour === 3, // THREE
    config.hour === 4, // FOUR
    config.hour === 5, // FIVE
    config.hour === 6, // SIX
    config.hour === 7, // SEVEN
    config.hour === 8, // EIGHT
    config.hour === 9, // NINE
    config.hour === 10, // TEN
    config.hour === 11, // ELEVEN
    config.isNoon, // NOON
    config.isMidnight, // MIDNIGHT
    config.isOClock, // O' CLOCK
    config.isMorning || config.isAfternoon || config.isEvening, // IN THE
    config.isMorning, // MORNING
    config.isAfternoon, // AFTERNOON
    config.isEvening, // EVENING
  ];
}

function speak (text) {
  const synth = window.speechSynthesis;
  const rate = 0.7;
  const pitch = 0.6;
  const voice = synth.getVoices().filter(v => v.lang === 'en-GB')[0];
  const utterance = new SpeechSynthesisUtterance(text);
  utterance.voice = voice;
  utterance.pitch = pitch;
  utterance.rate = rate;
  synth.speak(utterance);
}

function useClock () {
  const [timer, setTimer] = React.useState(null);
  const [time, setTime] = React.useState(getNow());

  React.useEffect(() => {
    if (!timer) initTimer();
    return (() => (timer && window.clearInterval(timer) && setTimer(null)));
  }, [timer]);

  function initTimer () {
    const now = Date.now();
    const nextSec = (Math.floor(now / 1000) + 1) * 1000;
    const timeLeft = nextSec - now;

    window.setTimeout(() => {
      const interval = window.setInterval(() => setTime(getNow()), 1000);
      setTimer(interval);
    }, timeLeft);
  }

  return time;
}

function StandardClock ({ time }) {
  const clockMarks = [ 'XII', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI' ];
  const hourAngle = ((time.hour % 12) * 60 + time.minute) / 2;
  const minuteAngle = (time.minute * 60 + time.second) / 10;
  const secondAngle = time.second * 6;

  return (
    <div className='standard-clock'>
      <div>
        { clockMarks.map(mark => <span className='mark'>{mark}</span>) }
      </div>
      <div>
        { Array(60).fill(1).map(tick => <span className='tick' />) }
      </div>
      <div className='inner-circle' />
      <div className='inner-circle-2' />
      <div className='hour-hand' style={{ transform: `rotate(${hourAngle}deg)` }} />
      <div className='minute-hand' style={{ transform: `rotate(${minuteAngle}deg)` }} />
      <div className='second-hand' style={{ transform: `rotate(${secondAngle}deg)` }} />
      <div className='center' />
    </div>
  );
}

function Speaker ({ active, text }) {
  if (!window.speechSynthesis) return null;
  React.useEffect (() => {
    if (active) speak(text);
  });
  return null;
}

function TimeReadout ({ time }) {
  const readoutConfig = getReadoutConfig(time);
  const highlighted = getHighlights(readoutConfig);
  const timeText = phrases.filter((phrase, index) => highlighted[index]).join(' ') + '.';
  const shouldSpeak = time.second === 0 && time.minute % 15 === 0;
  
  return (
    <div className='readout'>
      <p className='phrases'>
        { phrases.map((phrase, index) => (
          <span className={highlighted[index] ? 'glow' : ''}>
            {phrase}
          </span>
        ))}
      </p>
      <p className='timer'>{time.display}</p>
      <Speaker active={shouldSpeak} text={timeText} />
    </div>
  );
}

function NeonClock () {
  const time = useClock();
  return (
    <div className='clock'>
      <StandardClock time={time} />
      <TimeReadout time={time} />
    </div>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<NeonClock />, root);
              
            
!
999px

Console